diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 3915579c..00000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "params-validate"] - path = params-validate - url = git@github.com:CodingSoldier/params-validate.git -[submodule "test-params-validate"] - path = test-params-validate - url = git@github.com:CodingSoldier/test-params-validate.git diff --git "a/k8s\347\224\237\344\272\247\347\272\247\345\256\236\350\267\265\346\214\207\345\215\227/k8s\346\225\231\347\250\213.py" "b/k8s\347\224\237\344\272\247\347\272\247\345\256\236\350\267\265\346\214\207\345\215\227/k8s\346\225\231\347\250\213.py" index 512f7d48..2c655013 100644 --- "a/k8s\347\224\237\344\272\247\347\272\247\345\256\236\350\267\265\346\214\207\345\215\227/k8s\346\225\231\347\250\213.py" +++ "b/k8s\347\224\237\344\272\247\347\272\247\345\256\236\350\267\265\346\214\207\345\215\227/k8s\346\225\231\347\250\213.py" @@ -1152,3 +1152,5 @@ 如果pod状态是 0/1 , running未ready的状态 可使用命令查看状态 kubectl describe pod pod名字 + + diff --git a/note/pom.xml b/note/pom.xml index 3e327630..aa87ab23 100644 --- a/note/pom.xml +++ b/note/pom.xml @@ -2,14 +2,21 @@ - - java-learn - org.cpq - 1.0-SNAPSHOT - + 4.0.0 + + com.example note + 0.0.1-SNAPSHOT + note + + + UTF-8 + 4.12 + 5.0.1.RELEASE + 2.3.0 + @@ -56,6 +63,140 @@ 15.0 + + + + junit + junit + ${junit.version} + + + + org.slf4j + slf4j-log4j12 + 1.7.25 + + + org.apache.logging.log4j + log4j-web + 2.9.1 + + + + org.apache.commons + commons-lang3 + 3.7 + + + commons-beanutils + commons-beanutils + 1.9.3 + + + commons-io + commons-io + 2.6 + + + commons-fileupload + commons-fileupload + 1.3 + + + + commons-io + commons-io + + + + + org.apache.commons + commons-collections4 + 4.1 + + + + org.dom4j + dom4j + 2.1.1 + + + + + org.springframework + spring-aspects + ${spring.version} + + + + org.springframework + spring-webmvc + ${spring.version} + + + + + org.springframework + spring-orm + ${spring.version} + + + + org.springframework + spring-context-support + ${spring.version} + + + + org.springframework + spring-test + ${spring.version} + + + + + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + + com.fasterxml.jackson.core + jackson-databind + 2.9.2 + + + com.alibaba + fastjson + 1.2.41 + + + + + mysql + mysql-connector-java + 8.0.8-dmr + + + com.mchange + c3p0 + 0.9.5.2 + + + org.mybatis + mybatis + 3.4.5 + + + org.mybatis + mybatis-spring + 1.3.1 + + + diff --git a/note/src/main/java/com/datastructure/_5_BinarySearchTree.java b/note/src/main/java/com/datastructure/_5_BinarySearchTree.java index 5de6ad69..724ca2e8 100644 --- a/note/src/main/java/com/datastructure/_5_BinarySearchTree.java +++ b/note/src/main/java/com/datastructure/_5_BinarySearchTree.java @@ -245,16 +245,16 @@ public static void main(String[] args) { bst.add(num); } - // System.out.println("-------前序遍历------"); - // bst.preOrder(); - // System.out.println("------中序遍历-------"); - // bst.inOrder(); - // System.out.println("------后序遍历-------"); - // bst.postOrder(); - // System.out.println("------广度优先遍历-------"); - // bst.levelOrder(); - // - // + System.out.println("-------前序遍历------"); + bst.preOrder(); + System.out.println("------中序遍历-------"); + bst.inOrder(); + System.out.println("------后序遍历-------"); + bst.postOrder(); + System.out.println("------广度优先遍历-------"); + bst.levelOrder(); + + // ArrayList nums2 = new ArrayList<>(); //while(!bst.isEmpty()) @@ -266,7 +266,7 @@ public static void main(String[] args) { //System.out.println(nums2); // - bst.remove(16); + // bst.remove(16); //bst.preOrder(); } diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 1bc4e319..00000000 --- a/pom.xml +++ /dev/null @@ -1,256 +0,0 @@ - - - 4.0.0 - - - org.cpq - java-learn - pom - 1.0-SNAPSHOT - - - - - UTF-8 - 4.12 - 5.0.1.RELEASE - 2.3.0 - - - - - - - - - - - - - - - junit - junit - ${junit.version} - - - - org.slf4j - slf4j-log4j12 - 1.7.25 - - - org.apache.logging.log4j - log4j-web - 2.9.1 - - - - org.apache.commons - commons-lang3 - 3.7 - - - commons-beanutils - commons-beanutils - 1.9.3 - - - commons-io - commons-io - 2.6 - - - commons-fileupload - commons-fileupload - 1.3 - - - - commons-io - commons-io - - - - - org.apache.commons - commons-collections4 - 4.1 - - - - org.dom4j - dom4j - 2.1.1 - - - - - org.springframework - spring-aspects - ${spring.version} - - - - org.springframework - spring-webmvc - ${spring.version} - - - - - org.springframework - spring-orm - ${spring.version} - - - - org.springframework - spring-context-support - ${spring.version} - - - - org.springframework - spring-test - ${spring.version} - - - - - - - javax.servlet - javax.servlet-api - 3.0.1 - provided - - - - com.fasterxml.jackson.core - jackson-databind - 2.9.2 - - - com.alibaba - fastjson - 1.2.41 - - - - - mysql - mysql-connector-java - 8.0.8-dmr - - - com.mchange - c3p0 - 0.9.5.2 - - - org.mybatis - mybatis - 3.4.5 - - - org.mybatis - mybatis-spring - 1.3.1 - - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.7.0 - - 1.8 - 1.8 - UTF-8 - - - - - - org.mybatis.generator - mybatis-generator-maven-plugin - 1.3.6 - - false - true - ${basedir}/src/main/resources/mybatis-generator/generatorConfig.xml - - - - - - mysql - mysql-connector-java - - 5.1.44 - - - - - - - org.eclipse.jetty - jetty-maven-plugin - 9.4.8.v20171121 - - - - - - - - - - note - - - \ No newline at end of file diff --git a/project/Jenkins+Ansible+Gitlab/03-jenkins.groovy b/project/Jenkins+Ansible+Gitlab/03-jenkins.groovy index 567168ce..8b560791 100644 --- a/project/Jenkins+Ansible+Gitlab/03-jenkins.groovy +++ b/project/Jenkins+Ansible+Gitlab/03-jenkins.groovy @@ -5,6 +5,11 @@ DJENKINS_HOME=/usr/local/software/jenkins/jenkins_home nohup $JAVA_HOME/bin/java -DJENKINS_HOME=$DJENKINS_HOME -jar -Xms512m -Xmx512m -XX:+UseConcMarkSweepGC /usr/local/software/jenkins/jenkins.war --httpPort=8888 > /tmp/jenkins.log 2>&1 & +jenkins开机启动 +ll /etc/rc.d/init.d/jenkins.sh + +nohup java -DJENKINS_HOME=/var/lib/jenkins -jar /usr/lib/jenkins/jenkins.war --logfile=/var/log/jenkins/jenkins.log --webroot=/var/cache/jenkins/war --httpPort=8080 --debug=5 >/dev/null 2>&1 & + #################安装jenkins################# 官方地址:https://pkg.jenkins.io/redhat-stable/ 关闭防火墙、selinux diff --git a/project/apollo/apollo-01/.gitignore b/project/apollo/apollo-01/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/project/apollo/apollo-01/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/project/apollo/apollo-01/README.md b/project/apollo/apollo-01/README.md new file mode 100644 index 00000000..7e6f0fd1 --- /dev/null +++ b/project/apollo/apollo-01/README.md @@ -0,0 +1,46 @@ +## 官方文档 +https://www.apolloconfig.com/#/zh/deployment/quick-start + +## spring-boot整合方式 +1、导入依赖 +```xml + + com.ctrip.framework.apollo + apollo-client + 2.1.0 + +``` +2、添加配置文件 +```yaml +app: + # 应用id + id: SampleApp +apollo: + # 服务端地址 + meta: http://127.0.0.1:8080 + bootstrap: + enabled: true + eagerLoad: + enabled: true +``` +3、启动类Apollo01Application.java添加@EnableApolloConfig + +4、apollo服务端新增配置项:test.value01 +![](./file/服务端.png) + +5、代码获取配置项 +```java + @Value("${test.value01}") + private String value01; +``` + +## 使用方式补充 +1、只有使用分布式部署方式才能在配置文件中配置环境。web管理页面无法新建环境,Quick Start方式部署也不支持配置环境。 + +2、如果apollo服务端有多环境,可以加上JVM参数-Denv指定环境。例如: +```shell +java -jar test.jar -Denv=DEV +``` + + + diff --git "a/project/apollo/apollo-01/file/\346\234\215\345\212\241\347\253\257.png" "b/project/apollo/apollo-01/file/\346\234\215\345\212\241\347\253\257.png" new file mode 100644 index 00000000..1b064015 Binary files /dev/null and "b/project/apollo/apollo-01/file/\346\234\215\345\212\241\347\253\257.png" differ diff --git a/project/apollo/apollo-01/pom.xml b/project/apollo/apollo-01/pom.xml new file mode 100644 index 00000000..052490d0 --- /dev/null +++ b/project/apollo/apollo-01/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.16 + + + com.example + apollo-01 + 0.0.1-SNAPSHOT + apollo-01 + apollo-01 + + 1.8 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.ctrip.framework.apollo + apollo-client + 2.1.0 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/project/apollo/apollo-01/src/main/java/com/example/apollo01/Apollo01Application.java b/project/apollo/apollo-01/src/main/java/com/example/apollo01/Apollo01Application.java new file mode 100644 index 00000000..43ea63fb --- /dev/null +++ b/project/apollo/apollo-01/src/main/java/com/example/apollo01/Apollo01Application.java @@ -0,0 +1,14 @@ +package com.example.apollo01; + +import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@EnableApolloConfig +public class Apollo01Application { + public static void main(String[] args) { + SpringApplication.run(Apollo01Application.class, args); + } + +} diff --git a/project/apollo/apollo-01/src/main/java/com/example/apollo01/ValueController.java b/project/apollo/apollo-01/src/main/java/com/example/apollo01/ValueController.java new file mode 100644 index 00000000..cfad7ef1 --- /dev/null +++ b/project/apollo/apollo-01/src/main/java/com/example/apollo01/ValueController.java @@ -0,0 +1,35 @@ +package com.example.apollo01; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author chenpq05 + * @since 2023/11/14 15:59 + */ +@RestController +public class ValueController { + + @Value("${test.value01}") + private String value01; + + @Value("${test9}") + private String test9; + + @Value("${test.public1}") + private String testpublic1; + + //@Value("${cluster-value01}") + //private String clusterValue01; + + @GetMapping("/value01") + public String value01() { + System.out.println("#################value01="+value01); + System.out.println("#################test9="+test9); + System.out.println("#################testpublic1="+testpublic1); + //System.out.println("#################clusterValue01="+clusterValue01); + return ""; + } + +} diff --git a/project/apollo/apollo-01/src/main/resources/application.yml b/project/apollo/apollo-01/src/main/resources/application.yml new file mode 100644 index 00000000..bac886c9 --- /dev/null +++ b/project/apollo/apollo-01/src/main/resources/application.yml @@ -0,0 +1,22 @@ +server: + port: 8888 + +app: + id: SampleApp +apollo: + meta: http://127.0.0.1:8080 + # 指定cluster + cluster: default + bootstrap: + # 项目中的配置文件application.yml优先级最低 + # namespaces不填写application,apollo服务器上的application(Namespace)也会被加载 + # Namespace的优先级与public、private属性无关,与配置项的顺序有关。 + # 例如 namespaces: TEST1.public.common,apollo-01 。TEST1.public.common是公共配置但是优先级比apollo-01高 + namespaces: TEST1.public.common,apollo-01 + enabled: true + eagerLoad: + enabled: true + + + +test9: 999999 diff --git a/project/apollo/apollo-01/src/test/java/com/example/apollo01/Apollo01ApplicationTests.java b/project/apollo/apollo-01/src/test/java/com/example/apollo01/Apollo01ApplicationTests.java new file mode 100644 index 00000000..7a9ed130 --- /dev/null +++ b/project/apollo/apollo-01/src/test/java/com/example/apollo01/Apollo01ApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.apollo01; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Apollo01ApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/project/app3/k8s/entrypoint.sh b/project/app3/k8s/entrypoint.sh index 88ddea3d..daa43194 100644 --- a/project/app3/k8s/entrypoint.sh +++ b/project/app3/k8s/entrypoint.sh @@ -3,4 +3,4 @@ # 修改时间 cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime -exec java ${JAVA_OPS} -jar /app/app.jar +exec java ${JAVA_OPS} -jar /app/app.jar --spring.profiles.active=test diff --git a/project/ddd/README.md b/project/ddd/README.md new file mode 100644 index 00000000..00d36cd0 --- /dev/null +++ b/project/ddd/README.md @@ -0,0 +1,54 @@ +DDD核心思想: + 1、模型分解 + 领域划分、界限上下文 + 2、模型驱动设计 + 通过分层架构隔离领域层、仔细选择模型和设计方案等措施保持实现与模型的一致 + +模型是对领域的抽象,建模是针对特定问题建立领域的合理模型 +复杂性来源于业务本身的复杂性和设计引入的额外复杂性 +领域驱动设计通过分解模型和模型驱动设计控制复杂性 + +建模和设计的整体流程: +---------- 用户故事 ----------> + -------- 建立通用语言 ----> + ------ 战略设计 ------> + ---- 战术设计 ----> + +用戶故事:问题空间的描述、文字表达+图形表达、讨论 +用户故事可以理解为对业务、功能、问题的描述 + +通用语言: + 1、讨论模型、定义模型时使用同一种语言。例如:贩卖机、售卖机、售货机,团队统一使用“售货机” + 2、领域只是需要在团队高效流转,模型需要描述 + 3、通用语言体现在代码里。例如:下图的英文体现在 建表、代码、接口 中 + ![](通用语言.jpg) + +战略设计。 领域划分、寻找限界上下文(BC)、确定上下文映射 + +战术设计。编码 + + +常见建模方法: + 领域故事陈述法 + 事件风暴法 + 四色建模法 + + + + + + + + + + + + + + + + + + + + diff --git "a/project/ddd/\351\200\232\347\224\250\350\257\255\350\250\200.jpg" "b/project/ddd/\351\200\232\347\224\250\350\257\255\350\250\200.jpg" new file mode 100644 index 00000000..d7962e1e Binary files /dev/null and "b/project/ddd/\351\200\232\347\224\250\350\257\255\350\250\200.jpg" differ diff --git a/project/distributed-transaction/dtx-seata-demo/README.md b/project/distributed-transaction/dtx-seata-demo/README.md new file mode 100644 index 00000000..e2610511 --- /dev/null +++ b/project/distributed-transaction/dtx-seata-demo/README.md @@ -0,0 +1,2 @@ +Seata Saga 模式 +本地安装一个 状态机设计器,https://github.com/seata/seata/tree/2.x/saga/seata-saga-statemachine-designer \ No newline at end of file diff --git a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/service/impl/AccountInfoServiceImpl.java b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/service/impl/AccountInfoServiceImpl.java index 50077908..ca8148da 100644 --- a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/service/impl/AccountInfoServiceImpl.java +++ b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/service/impl/AccountInfoServiceImpl.java @@ -10,6 +10,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.concurrent.TimeUnit; + /** * @author Administrator * @version 1.0 @@ -21,21 +23,22 @@ public class AccountInfoServiceImpl implements AccountInfoService { @Autowired AccountInfoDao accountInfoDao; - @Autowired - Bank2Client bank2Client; - + /** + * @GlobalTransactional 开启全局事务, + * 有请求A、请求B,A一定比,请求B才能执行此方法 + */ @Transactional - @GlobalTransactional//开启全局事务 + @GlobalTransactional @Override public void updateAccountBalance(String accountNo, Double amount) { - log.info("bank1 service begin,XID:{}", RootContext.getXID()); + log.info("#########服务1开始, amount={}, XID:{}", amount, RootContext.getXID()); //扣减张三的金额 accountInfoDao.updateAccountBalance(accountNo,amount *-1); //调用李四微服务,转账 String transfer = bank2Client.transfer(amount); if("fallback".equals(transfer)){ //调用李四微服务异常 - throw new RuntimeException("调用李四微服务异常"); + throw new RuntimeException("#########调用李四微服务异常"); } /** @@ -49,8 +52,16 @@ public void updateAccountBalance(String accountNo, Double amount) { * 解决思路是使用select for update */ if(amount == 2){ + try { + TimeUnit.SECONDS.sleep(5L); + } catch (Exception e) { + log.error("#########", e); + } //人为制造异常 - throw new RuntimeException("bank1 make exception.."); + throw new RuntimeException("#########所有服务调用完成,服务抛出异常"); } } + + @Autowired + Bank2Client bank2Client; } diff --git a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/spring/Bank2Client.java b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/spring/Bank2Client.java index 9f4f0f95..4658f636 100644 --- a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/spring/Bank2Client.java +++ b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/spring/Bank2Client.java @@ -7,7 +7,8 @@ /** * Created by Administrator. */ -@FeignClient(value="seata-demo-bank2",fallback=Bank2ClientFallback.class) +// @FeignClient(value="seata-demo-bank2",fallback=Bank2ClientFallback.class) +@FeignClient(value="seata-demo-bank2") public interface Bank2Client { //远程调用李四的微服务 @GetMapping("/bank2/transfer") diff --git a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/spring/Bank2ClientFallback.java b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/spring/Bank2ClientFallback.java index 87b33219..9a245610 100644 --- a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/spring/Bank2ClientFallback.java +++ b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/java/com/pbteach/dtx/seatademo/bank1/spring/Bank2ClientFallback.java @@ -1,17 +1,15 @@ package com.pbteach.dtx.seatademo.bank1.spring; -import org.springframework.stereotype.Component; - /** * @author Administrator * @version 1.0 **/ -@Component -public class Bank2ClientFallback implements Bank2Client { - - @Override - public String transfer(Double amount) { - - return "fallback"; - } -} +// @Component +// public class Bank2ClientFallback implements Bank2Client { +// +// @Override +// public String transfer(Double amount) { +// +// return "fallback"; +// } +// } diff --git a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/resources/application-local.yml b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/resources/application-local.yml index e511a570..8610292f 100644 --- a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/resources/application-local.yml +++ b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/resources/application-local.yml @@ -40,12 +40,12 @@ logging: pbteach: debug root: INFO io: - seata: INFO + seata: debug org: springframework: cloud: alibaba: seata: - web: INFO + web: debug diff --git a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/resources/application.yml b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/resources/application.yml index c35b504c..ee78febf 100644 --- a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/resources/application.yml +++ b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank1/src/main/resources/application.yml @@ -24,8 +24,8 @@ eureka: instance: preferIpAddress: true instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}} - lease-renewal-interval-in-seconds: 5 # 续约更新时间间隔(默认30秒) - lease-expiration-duration-in-seconds: 10 # 续约到期时间(默认90秒) + lease-renewal-interval-in-seconds: 5000 # 续约更新时间间隔(默认30秒) + lease-expiration-duration-in-seconds: 10000 # 续约到期时间(默认90秒) client: registry-fetch-interval-seconds: 5 # 抓取服务列表 @@ -53,15 +53,15 @@ hystrix: execution: isolation: thread: - timeoutInMilliseconds: 1000 # 设置熔断超时时间 default 1000 + timeoutInMilliseconds: 100000000 # 设置熔断超时时间 default 1000 timeout: enabled: true # 打开超时熔断功能 default true ribbon: - ConnectTimeout: 600 # 设置连接超时时间 default 2000 - ReadTimeout: 6000 # 设置读取超时时间 default 5000 - OkToRetryOnAllOperations: true # 对所有操作请求都进行重试 default false - MaxAutoRetriesNextServer: 2 # 切换实例的重试次数 default 1 - MaxAutoRetries: 1 # 对当前实例的重试次数 default 0 + ConnectTimeout: 6000000 # 设置连接超时时间 default 2000 + ReadTimeout: 60000000 # 设置读取超时时间 default 5000 +# OkToRetryOnAllOperations: true # 对所有操作请求都进行重试 default false +# MaxAutoRetriesNextServer: 2 # 切换实例的重试次数 default 1 +# MaxAutoRetries: 1 # 对当前实例的重试次数 default 0 diff --git a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/java/com/pbteach/seatademo/bank2/service/impl/AccountInfoServiceImpl.java b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/java/com/pbteach/seatademo/bank2/service/impl/AccountInfoServiceImpl.java index 8171caff..b0df0d57 100644 --- a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/java/com/pbteach/seatademo/bank2/service/impl/AccountInfoServiceImpl.java +++ b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/java/com/pbteach/seatademo/bank2/service/impl/AccountInfoServiceImpl.java @@ -23,7 +23,7 @@ public class AccountInfoServiceImpl implements AccountInfoService { @Transactional @Override public void updateAccountBalance(String accountNo, Double amount) { - log.info("bank2 service begin,XID:{}",RootContext.getXID()); + log.info("#########服务2开始, amount={}, XID:{}", amount, RootContext.getXID()); //李四增加金额 accountInfoDao.updateAccountBalance(accountNo,amount); if(amount==3){ diff --git a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/resources/application-local.yml b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/resources/application-local.yml index 048c4b13..24ac7a02 100644 --- a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/resources/application-local.yml +++ b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/resources/application-local.yml @@ -40,11 +40,11 @@ logging: pbteach: debug root: INFO io: - seata: INFO + seata: debug org: springframework: cloud: alibaba: seata: - web: INFO + web: debug diff --git a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/resources/application.yml b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/resources/application.yml index d3a944eb..46d4699c 100644 --- a/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/resources/application.yml +++ b/project/distributed-transaction/dtx-seata-demo/dtx-seata-demo-bank2/src/main/resources/application.yml @@ -24,8 +24,8 @@ eureka: instance: preferIpAddress: true instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}} - lease-renewal-interval-in-seconds: 5 # 续约更新时间间隔(默认30秒) - lease-expiration-duration-in-seconds: 10 # 续约到期时间(默认90秒) + lease-renewal-interval-in-seconds: 5000 # 续约更新时间间隔(默认30秒) + lease-expiration-duration-in-seconds: 10000 # 续约到期时间(默认90秒) client: registry-fetch-interval-seconds: 5 # 抓取服务列表 @@ -53,15 +53,15 @@ hystrix: execution: isolation: thread: - timeoutInMilliseconds: 1000 # 设置熔断超时时间 default 1000 + timeoutInMilliseconds: 100000000 # 设置熔断超时时间 default 1000 timeout: enabled: true # 打开超时熔断功能 default true ribbon: - ConnectTimeout: 600 # 设置连接超时时间 default 2000 - ReadTimeout: 6000 # 设置读取超时时间 default 5000 - OkToRetryOnAllOperations: true # 对所有操作请求都进行重试 default false - MaxAutoRetriesNextServer: 2 # 切换实例的重试次数 default 1 - MaxAutoRetries: 1 # 对当前实例的重试次数 default 0 + ConnectTimeout: 6000000 # 设置连接超时时间 default 2000 + ReadTimeout: 60000000 # 设置读取超时时间 default 5000 +# OkToRetryOnAllOperations: true # 对所有操作请求都进行重试 default false +# MaxAutoRetriesNextServer: 2 # 切换实例的重试次数 default 1 +# MaxAutoRetries: 1 # 对当前实例的重试次数 default 0 diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/controller/Bank1Controller.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/controller/Bank1Controller.java index 6eec9cb1..8ffe37a9 100644 --- a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/controller/Bank1Controller.java +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/controller/Bank1Controller.java @@ -1,6 +1,6 @@ package com.pbteach.dtx.tccdemo.bank1.controller; -import com.pbteach.dtx.tccdemo.bank1.service.AccountInfoService; +import com.pbteach.dtx.tccdemo.bank1.service.Bank1Service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -13,11 +13,11 @@ @RestController public class Bank1Controller { @Autowired - AccountInfoService accountInfoService; + Bank1Service accountInfoService; @RequestMapping("/transfer") - public Boolean transfer(@RequestParam("amount") Double amount) { - this.accountInfoService.updateAccountBalance("1", amount); + public Boolean transfer(@RequestParam("msg") String msg, @RequestParam("amount") Double amount) { + this.accountInfoService.updateAccountBalance(msg, amount); return true; } diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/dao/AccountInfoDao.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/dao/AccountInfoDao.java index b97d3daa..d8f12a9d 100644 --- a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/dao/AccountInfoDao.java +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/dao/AccountInfoDao.java @@ -12,42 +12,4 @@ public interface AccountInfoDao { @Update("update account_info set account_balance=account_balance + #{amount} where account_no=#{accountNo} ") int addAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount); - - /** - * 增加某分支事务try执行记录 - * @param localTradeNo 本地事务编号 - * @return - */ - @Insert("insert into local_try_log values(#{txNo},now());") - int addTry(String localTradeNo); - - @Insert("insert into local_confirm_log values(#{txNo},now());") - int addConfirm(String localTradeNo); - - @Insert("insert into local_cancel_log values(#{txNo},now());") - int addCancel(String localTradeNo); - - /** - * 查询分支事务try是否已执行 - * @param localTradeNo 本地事务编号 - * @return - */ - @Select("select count(1) from local_try_log where tx_no = #{txNo} ") - int isExistTry(String localTradeNo); - /** - * 查询分支事务confirm是否已执行 - * @param localTradeNo 本地事务编号 - * @return - */ - @Select("select count(1) from local_confirm_log where tx_no = #{txNo} ") - int isExistConfirm(String localTradeNo); - - /** - * 查询分支事务cancel是否已执行 - * @param localTradeNo 本地事务编号 - * @return - */ - @Select("select count(1) from local_cancel_log where tx_no = #{txNo} ") - int isExistCancel(String localTradeNo); - } diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/dao/HmilyLogDao.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/dao/HmilyLogDao.java new file mode 100644 index 00000000..5c082292 --- /dev/null +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/dao/HmilyLogDao.java @@ -0,0 +1,46 @@ +package com.pbteach.dtx.tccdemo.bank1.dao; + +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +@Mapper +@Component +public interface HmilyLogDao { + /** + * 增加某分支事务try执行记录 + * @param localTradeNo 本地事务编号 + * @return + */ + @Insert("insert into local_try_log values(#{txNo},now());") + int addTry(String localTradeNo); + + @Insert("insert into local_confirm_log values(#{txNo},now());") + int addConfirm(String localTradeNo); + + @Insert("insert into local_cancel_log values(#{txNo},now());") + int addCancel(String localTradeNo); + + /** + * 查询分支事务try是否已执行 + * @param localTradeNo 本地事务编号 + * @return + */ + @Select("select count(1) from local_try_log where tx_no = #{txNo} ") + int isExistTry(String localTradeNo); + /** + * 查询分支事务confirm是否已执行 + * @param localTradeNo 本地事务编号 + * @return + */ + @Select("select count(1) from local_confirm_log where tx_no = #{txNo} ") + int isExistConfirm(String localTradeNo); + + /** + * 查询分支事务cancel是否已执行 + * @param localTradeNo 本地事务编号 + * @return + */ + @Select("select count(1) from local_cancel_log where tx_no = #{txNo} ") + int isExistCancel(String localTradeNo); + +} diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/AccountInfoService.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/AccountInfoService.java deleted file mode 100644 index ddb2c13b..00000000 --- a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/AccountInfoService.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.pbteach.dtx.tccdemo.bank1.service; - -/** - * Created by Administrator. - */ -public interface AccountInfoService { - - //账户扣款 - public void updateAccountBalance(String accountNo, Double amount); -} diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/Bank1Service.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/Bank1Service.java new file mode 100644 index 00000000..faa27871 --- /dev/null +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/Bank1Service.java @@ -0,0 +1,10 @@ +package com.pbteach.dtx.tccdemo.bank1.service; + +/** + * Created by Administrator. + */ +public interface Bank1Service { + + //账户扣款 + public void updateAccountBalance(String msg, Double amount); +} diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/impl/AccountInfoServiceImpl.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/impl/AccountInfoServiceImpl.java deleted file mode 100644 index 6797828f..00000000 --- a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/impl/AccountInfoServiceImpl.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.pbteach.dtx.tccdemo.bank1.service.impl; - -import com.pbteach.dtx.tccdemo.bank1.dao.AccountInfoDao; -import com.pbteach.dtx.tccdemo.bank1.service.AccountInfoService; -import com.pbteach.dtx.tccdemo.bank1.spring.Bank2Client; -import lombok.extern.slf4j.Slf4j; -import org.dromara.hmily.annotation.Hmily; -import org.dromara.hmily.core.concurrent.threadlocal.HmilyTransactionContextLocal; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * @author Administrator - * @version 1.0 - **/ -@Service -@Slf4j -public class AccountInfoServiceImpl implements AccountInfoService { - - @Autowired - AccountInfoDao accountInfoDao; - - @Autowired - Bank2Client bank2Client; - - // 账户扣款,就是tcc的try方法 - - /** - * try幂等校验 - * try悬挂处理 - * 检查余额是够扣减金额 - * 扣减金额 - * @param accountNo - * @param amount - */ - @Override - @Transactional - //只要标记@Hmily就是try方法,在注解中指定confirm、cancel两个方法的名字 - @Hmily(confirmMethod="commit",cancelMethod="rollback") - public void updateAccountBalance(String accountNo, Double amount) { - //获取全局事务id - String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); - log.info("bank1 try begin 开始执行...xid:{}",transId); - - /** - * 幂等判断 - * 在try之后加一条try-modify日志,日志记录全局事务ID - * 在try之前判断日志是否存在,如果已经存在,则证明已经执行try-modify,不再往下执行 - * 被调用端必须也用这个判断 - */ - //幂等判断 判断local_try_log表中是否有try日志记录,如果有则不再执行 - if(accountInfoDao.isExistTry(transId)>0){ - log.info("bank1 try 已经执行,无需重复执行,xid:{}",transId); - return ; - } - - /** - * 悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。 - * 解决办法同样是在try之前判断Cancel是否已经执行 - */ - // try悬挂处理,如果cancel、confirm有一个已经执行了,try不再执行 - // 本案例中confirm为空,所以isExistConfirm(transId)在本案例中是没必要的 - if(accountInfoDao.isExistConfirm(transId)>0 || accountInfoDao.isExistCancel(transId)>0){ - log.info("bank1 try悬挂处理 cancel或confirm已经执行,不允许执行try,xid:{}",transId); - return ; - } - - /** - * 空回滚 - * - * 解决办法,在修改操作后,加一行try-modify日志。 - * cancel方法判断try-modify日志是否存在,如果存在,cancel才执行回滚修改 - */ - if(amount == 0){ - throw new RuntimeException("没执行try数据库修改操作就抛出异常,cancel方法会执行,形成空回滚,xid:{}"+transId); - } - - //扣减金额 - if(accountInfoDao.subtractAccountBalance(accountNo, amount)<=0){ - //扣减失败 - throw new RuntimeException("bank1 try 扣减金额失败,xid:{}"+transId); - } - //插入try执行记录,用于幂等判断 - accountInfoDao.addTry(transId); - - //远程调用李四,转账 - if(!bank2Client.transfer(amount)){ - throw new RuntimeException("bank1 远程调用李四微服务失败,xid:{}"+transId); - } - if(amount == 2){ - throw new RuntimeException("人为制造异常,xid:{}"+transId); - } - log.info("bank1 try end 结束执行...xid:{}",transId); - } - - //confirm方法 - @Transactional - public void commit(String accountNo, Double amount){ - //获取全局事务id - String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); - log.info("bank1 confirm begin 开始执行...xid:{},accountNo:{},amount:{}",transId,accountNo,amount); - } - - - - /** cancel方法 - * cancel幂等校验 - * cancel空回滚处理 - * 增加可用余额 - * @param accountNo - * @param amount - */ - @Transactional - public void rollback(String accountNo, Double amount){ - //获取全局事务id - String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); - log.info("bank1 cancel begin 开始执行...xid:{}",transId); - // cancel幂等校验 - if(accountInfoDao.isExistCancel(transId)>0){ - log.info("bank1 cancel 已经执行,无需重复执行,xid:{}",transId); - return ; - } - //cancel空回滚处理,如果try没有执行,cancel不允许执行 - if(accountInfoDao.isExistTry(transId)<=0){ - log.info("bank1 空回滚处理,try没有执行,不允许cancel执行,xid:{}",transId); - return ; - } - // 增加可用余额 - accountInfoDao.addAccountBalance(accountNo,amount); - //插入一条cancel的执行记录 - accountInfoDao.addCancel(transId); - log.info("bank1 cancel end 结束执行...xid:{}",transId); - - } - -} diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/impl/Bank1ServiceImpl.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/impl/Bank1ServiceImpl.java new file mode 100644 index 00000000..2a11b019 --- /dev/null +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/service/impl/Bank1ServiceImpl.java @@ -0,0 +1,111 @@ +package com.pbteach.dtx.tccdemo.bank1.service.impl; + +import com.pbteach.dtx.tccdemo.bank1.dao.AccountInfoDao; +import com.pbteach.dtx.tccdemo.bank1.dao.HmilyLogDao; +import com.pbteach.dtx.tccdemo.bank1.service.Bank1Service; +import com.pbteach.dtx.tccdemo.bank1.spring.Bank2Client; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.dromara.hmily.annotation.Hmily; +import org.dromara.hmily.core.concurrent.threadlocal.HmilyTransactionContextLocal; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Administrator + * @version 1.0 + **/ +@Service +@Slf4j +public class Bank1ServiceImpl implements Bank1Service { + + @Autowired + AccountInfoDao accountInfoDao; + @Autowired + HmilyLogDao hmilyLogDao; + + @Autowired + Bank2Client bank2Client; + + @Override + @Transactional(rollbackFor = Exception.class) + @Hmily(confirmMethod = "confirm", cancelMethod = "cancel") + public void updateAccountBalance(String msg, Double amount) { + // 全局事务id + String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); + log.info("bank1 try 开始,transId={}", transId); + + // 幂等判断 + int existTry = hmilyLogDao.isExistTry(transId); + // 通故全局事务id查找到try日志,表明已经只执行过try + if (existTry > 0) { + log.info("已经执行过try,无需重复执行try,transId={}", transId); + return; + } + + // 悬挂处理 + int existConfirm = hmilyLogDao.isExistConfirm(transId); + int existCancel = hmilyLogDao.isExistCancel(transId); + // 通故全局事务id查找到confirm、cancel日志,表明已经只执行过confirm、cancel + if (existConfirm > 0 || existCancel > 0) { + log.info("confirm,cancel有一个已经执行过,try不能再次执行,transId={}", transId); + return; + } + + // 制造空回滚 + if (StringUtils.equals("制造空回滚", msg)) { + throw new RuntimeException("try方法没修改数据库就抛出异常,cancel方法会执行,形成空回滚,transId=" + transId); + } + + // blank1减金额 + accountInfoDao.subtractAccountBalance("1", amount); + + // 添加try日志记录,try日志和扣减余额在同一个本地事务中,要么都成功,要么都失败 + // 日志的组件id必须是全局事务id,如果同一个事物重复调用try,到这一步会报主键重复 + hmilyLogDao.addTry(transId); + + // 远程调用 + Boolean result = bank2Client.transfer(msg, amount); + if (!result) { + throw new RuntimeException("调用bank2失败"); + } + + // bank1调用bank2成功后,发生异常,模拟回滚 + if (StringUtils.equals("bank1调用bank2成功后,发生异常,模拟回滚", msg)) { + throw new RuntimeException("bank1调用bank2成功后,发生异常,模拟回滚,transId=" + transId); + } + } + + public void confirm(String accountNo, Double amount) { + String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); + log.info("bank1 confirm 开始执行,transId={}", transId); + } + + @Transactional(rollbackFor = Exception.class) + public void cancel(String msg, Double amount) { + // 全局事务id + String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); + log.info("bank1 cancel 开始执行,transId={}", transId); + + // 幂等判断 + int existCancel = hmilyLogDao.isExistCancel(transId); + if (existCancel > 0) { + log.info("cancel已经执行过,无需重复执行,transId={}", transId); + return; + } + + // 处理空回滚 + int existTry = hmilyLogDao.isExistTry(transId); + if (existTry == 0) { + log.info("try未执行过,不能执行cancel,transId={}", transId); + return; + } + + // bank1回滚,加钱 + accountInfoDao.addAccountBalance(msg, amount); + // 添加日志 + hmilyLogDao.addCancel(transId); + } + +} diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/spring/Bank2Client.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/spring/Bank2Client.java index e1c192ed..ffa4d111 100644 --- a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/spring/Bank2Client.java +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/spring/Bank2Client.java @@ -13,5 +13,5 @@ public interface Bank2Client { //远程调用李四的微服务 @GetMapping("/bank2/transfer") @Hmily - public Boolean transfer(@RequestParam("amount") Double amount); + Boolean transfer(@RequestParam("msg") String msg, @RequestParam("amount") Double amount); } diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/spring/Bank2ClientFallback.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/spring/Bank2ClientFallback.java index 06303c4b..a1415d67 100644 --- a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/spring/Bank2ClientFallback.java +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank1/src/main/java/com/pbteach/dtx/tccdemo/bank1/spring/Bank2ClientFallback.java @@ -1,6 +1,7 @@ package com.pbteach.dtx.tccdemo.bank1.spring; import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestParam; /** * @author Administrator @@ -10,7 +11,7 @@ public class Bank2ClientFallback implements Bank2Client { @Override - public Boolean transfer(Double amount) { + public Boolean transfer(String msg, Double amount) { return false; } diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/controller/Bank2Controller.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/controller/Bank2Controller.java index 85204725..955fcb6c 100644 --- a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/controller/Bank2Controller.java +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/controller/Bank2Controller.java @@ -1,14 +1,12 @@ package com.pbteach.dtx.tccdemo.bank2.controller; -import com.pbteach.dtx.tccdemo.bank2.service.AccountInfoService; +import com.pbteach.dtx.tccdemo.bank2.service.Bank2Service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.util.concurrent.TimeUnit; - /** * @author Administrator * @version 1.0 @@ -17,19 +15,19 @@ @Slf4j public class Bank2Controller { @Autowired - AccountInfoService accountInfoService; + Bank2Service accountInfoService; @RequestMapping("/transfer") - public Boolean transfer(@RequestParam("amount") Double amount) { - log.info("超时模拟"); - try { - - TimeUnit.SECONDS.sleep(10L); - }catch (Exception e){ - e.printStackTrace(); - } + public Boolean transfer(@RequestParam("msg") String msg, @RequestParam("amount") Double amount) { + // log.info("超时模拟"); + // try { + // + // TimeUnit.SECONDS.sleep(10L); + // }catch (Exception e){ + // e.printStackTrace(); + // } - this.accountInfoService.updateAccountBalance("2", amount); + this.accountInfoService.updateAccountBalance(msg, amount); return true; } diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/dao/AccountInfoDao.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/dao/AccountInfoDao.java index a010c638..aa569c8f 100644 --- a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/dao/AccountInfoDao.java +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/dao/AccountInfoDao.java @@ -10,42 +10,4 @@ public interface AccountInfoDao { @Update("update account_info set account_balance=account_balance + #{amount} where account_no=#{accountNo} ") int addAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount); - - /** - * 增加某分支事务try执行记录 - * @param localTradeNo 本地事务编号 - * @return - */ - @Insert("insert into local_try_log values(#{txNo},now());") - int addTry(String localTradeNo); - - @Insert("insert into local_confirm_log values(#{txNo},now());") - int addConfirm(String localTradeNo); - - @Insert("insert into local_cancel_log values(#{txNo},now());") - int addCancel(String localTradeNo); - - /** - * 查询分支事务try是否已执行 - * @param localTradeNo 本地事务编号 - * @return - */ - @Select("select count(1) from local_try_log where tx_no = #{txNo} ") - int isExistTry(String localTradeNo); - /** - * 查询分支事务confirm是否已执行 - * @param localTradeNo 本地事务编号 - * @return - */ - @Select("select count(1) from local_confirm_log where tx_no = #{txNo} ") - int isExistConfirm(String localTradeNo); - - /** - * 查询分支事务cancel是否已执行 - * @param localTradeNo 本地事务编号 - * @return - */ - @Select("select count(1) from local_cancel_log where tx_no = #{txNo} ") - int isExistCancel(String localTradeNo); - } diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/dao/HmilyLogDao.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/dao/HmilyLogDao.java new file mode 100644 index 00000000..cd873693 --- /dev/null +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/dao/HmilyLogDao.java @@ -0,0 +1,47 @@ +package com.pbteach.dtx.tccdemo.bank2.dao; + +import org.apache.ibatis.annotations.*; +import org.springframework.stereotype.Component; + +@Component +@Mapper +public interface HmilyLogDao { + + /** + * 增加某分支事务try执行记录 + * @param localTradeNo 本地事务编号 + * @return + */ + @Insert("insert into local_try_log values(#{txNo},now());") + int addTry(String localTradeNo); + + @Insert("insert into local_confirm_log values(#{txNo},now());") + int addConfirm(String localTradeNo); + + @Insert("insert into local_cancel_log values(#{txNo},now());") + int addCancel(String localTradeNo); + + /** + * 查询分支事务try是否已执行 + * @param localTradeNo 本地事务编号 + * @return + */ + @Select("select count(1) from local_try_log where tx_no = #{txNo} ") + int isExistTry(String localTradeNo); + /** + * 查询分支事务confirm是否已执行 + * @param localTradeNo 本地事务编号 + * @return + */ + @Select("select count(1) from local_confirm_log where tx_no = #{txNo} ") + int isExistConfirm(String localTradeNo); + + /** + * 查询分支事务cancel是否已执行 + * @param localTradeNo 本地事务编号 + * @return + */ + @Select("select count(1) from local_cancel_log where tx_no = #{txNo} ") + int isExistCancel(String localTradeNo); + +} diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/AccountInfoService.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/AccountInfoService.java deleted file mode 100644 index 2613cca2..00000000 --- a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/AccountInfoService.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.pbteach.dtx.tccdemo.bank2.service; - -/** - * Created by Administrator. - */ -public interface AccountInfoService { - - //账户扣款 - public void updateAccountBalance(String accountNo, Double amount); -} diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/Bank2Service.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/Bank2Service.java new file mode 100644 index 00000000..d8b4e533 --- /dev/null +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/Bank2Service.java @@ -0,0 +1,10 @@ +package com.pbteach.dtx.tccdemo.bank2.service; + +/** + * Created by Administrator. + */ +public interface Bank2Service { + + //账户扣款 + public void updateAccountBalance(String msg, Double amount); +} diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/impl/AccountInfoServiceImpl.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/impl/AccountInfoServiceImpl.java deleted file mode 100644 index 410bd672..00000000 --- a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/impl/AccountInfoServiceImpl.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.pbteach.dtx.tccdemo.bank2.service.impl; - -import com.pbteach.dtx.tccdemo.bank2.dao.AccountInfoDao; -import com.pbteach.dtx.tccdemo.bank2.service.AccountInfoService; -import lombok.extern.slf4j.Slf4j; -import org.dromara.hmily.annotation.Hmily; -import org.dromara.hmily.core.concurrent.threadlocal.HmilyTransactionContextLocal; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -/** - * @author Administrator - * @version 1.0 - **/ -@Service -@Slf4j -public class AccountInfoServiceImpl implements AccountInfoService { - - @Autowired - AccountInfoDao accountInfoDao; - - @Override - @Hmily(confirmMethod="confirmMethod", cancelMethod="cancelMethod") - public void updateAccountBalance(String accountNo, Double amount) { - //获取全局事务id - String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); - log.info("bank2 try begin 开始执行...xid:{}",transId); - } - - /** - * confirm方法 - * confirm幂等校验 - * 正式增加金额 - * @param accountNo - * @param amount - */ - @Transactional - public void confirmMethod(String accountNo, Double amount){ - //获取全局事务id - String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); - log.info("bank2 confirm begin 开始执行...xid:{}",transId); - - /** - * 幂等判断 - */ - if(accountInfoDao.isExistConfirm(transId)>0){ - log.info("bank2 confirm 已经执行,无需重复执行...xid:{}",transId); - return ; - } - - //增加金额 - accountInfoDao.addAccountBalance(accountNo,amount); - - /** - * 加入幂等记录 - */ - //增加一条confirm日志,用于幂等 - accountInfoDao.addConfirm(transId); - log.info("bank2 confirm end 结束执行...xid:{}",transId); - } - - - - /** - * @param accountNo - * @param amount - */ - public void cancelMethod(String accountNo, Double amount){ - //获取全局事务id - String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); - log.info("bank2 cancel begin 开始执行...xid:{}",transId); - - } - -} diff --git a/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/impl/Bank2ServiceImpl.java b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/impl/Bank2ServiceImpl.java new file mode 100644 index 00000000..2b0103cd --- /dev/null +++ b/project/distributed-transaction/dtx-tcc-demo/dtx-tcc-demo-bank2/src/main/java/com/pbteach/dtx/tccdemo/bank2/service/impl/Bank2ServiceImpl.java @@ -0,0 +1,63 @@ +package com.pbteach.dtx.tccdemo.bank2.service.impl; + +import com.pbteach.dtx.tccdemo.bank2.dao.AccountInfoDao; +import com.pbteach.dtx.tccdemo.bank2.dao.HmilyLogDao; +import com.pbteach.dtx.tccdemo.bank2.service.Bank2Service; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.dromara.hmily.annotation.Hmily; +import org.dromara.hmily.core.concurrent.threadlocal.HmilyTransactionContextLocal; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Administrator + * @version 1.0 + **/ +@Service +@Slf4j +public class Bank2ServiceImpl implements Bank2Service { + + @Autowired + AccountInfoDao accountInfoDao; + + @Autowired + HmilyLogDao hmilyLogDao; + + @Override + @Hmily(confirmMethod = "confirm", cancelMethod = "cancel") + public void updateAccountBalance(String msg, Double amount) { + String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); + log.info("bank2 try 开始执行,transId:{}",transId); + } + + @Transactional(rollbackFor = Exception.class) + public void confirm(String msg, Double amount) { + // 全局事务id + String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); + log.info("bank2 confirm 开始执行,transId:{}",transId); + + int existConfirm = hmilyLogDao.isExistConfirm(transId); + if (existConfirm > 0) { + log.info("bank2 confirm 已经执行过,无需再次执行,transId", transId); + return; + } + + // bank2加钱 + accountInfoDao.addAccountBalance("2", amount); + // 添加confirm日志 + hmilyLogDao.addConfirm(transId); + + // bank2 confirm,抛出异常,会重试 + if (StringUtils.equals("confirm抛出异常会重试", msg)) { + throw new RuntimeException("confirm抛出异常会重试,transId=" + transId); + } + } + + public void cancel(String msg, Double amount) { + String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); + log.info("bank2 cancel 开始执行,transId:{}",transId); + } + +} diff --git "a/project/distributed-transaction/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\344\270\223\351\242\230.pdf" "b/project/distributed-transaction/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\344\270\223\351\242\230.pdf" new file mode 100644 index 00000000..53a5705a Binary files /dev/null and "b/project/distributed-transaction/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\344\270\223\351\242\230.pdf" differ diff --git "a/project/docker+k8s/\345\256\211\350\243\205k8s.txt" "b/project/docker+k8s/\345\256\211\350\243\205k8s.txt" new file mode 100644 index 00000000..e1cf8786 --- /dev/null +++ "b/project/docker+k8s/\345\256\211\350\243\205k8s.txt" @@ -0,0 +1 @@ +https://kuboard.cn/install/history-k8s/install-k8s-1.19.x.html \ No newline at end of file diff --git "a/project/docker+k8s/\346\205\225\350\257\276\347\275\221docker/docker.txt" "b/project/docker+k8s/\346\205\225\350\257\276\347\275\221docker/docker.txt" index af4f8769..3a9a46f9 100644 --- "a/project/docker+k8s/\346\205\225\350\257\276\347\275\221docker/docker.txt" +++ "b/project/docker+k8s/\346\205\225\350\257\276\347\275\221docker/docker.txt" @@ -99,7 +99,7 @@ ADD hello / # 把hello添加到根目录 CMD ["/hello"] # 运行根目录下的hello 创建image,后面的 . 表示在当前目录找Dockerfile -docker build -t xiaopeng163/hello-world . +docker build -t vanruiedge/easygo-edge . 运行容器 docker run xiaopeng163/hello-world diff --git a/project/generic/pom.xml b/project/generic/pom.xml new file mode 100644 index 00000000..8c6ee095 --- /dev/null +++ b/project/generic/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + + org.cpq + generic + 1.0-SNAPSHOT + + + + org.apache.maven.plugins + maven-compiler-plugin + + 7 + 7 + + + + + pom + + + http://www.example.com + + + + website + scp://webhost.company.com/www/website + + + + + UTF-8 + + + + + + + com.fasterxml.jackson.core + jackson-core + 2.14.2 + + + + com.alibaba + fastjson + 2.0.26 + + + + diff --git a/project/generic/src/main/java/com/itheima/demo1/MainClass.java b/project/generic/src/main/java/com/itheima/demo1/MainClass.java new file mode 100644 index 00000000..89d06a71 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo1/MainClass.java @@ -0,0 +1,45 @@ +package com.itheima.demo1; + +import java.util.ArrayList; + +/** + * 泛型产生的背景 + */ +public class MainClass { + public static void main(String[] args) { +// ArrayList list = new ArrayList(); +// list.add("java"); +// list.add(100); +// list.add(true); +// +// for (int i = 0; i < list.size(); i++) { +// Object o = list.get(i); +// String str = (String)o; +// System.out.println(str); +// } + + // //泛型: + // // 编译期间检查类型 + // //减少了数据类型转换 + // ArrayList strList = new ArrayList<>(); + // strList.add("a"); + // strList.add("b"); + // strList.add("c"); + + // for (int i = 0; i < strList.size(); i++) { + // String s = strList.get(i); + // System.out.println(s); + // } + + System.out.println("------------------------------------"); + ArrayList intList = new ArrayList<>(); + intList.add(100); + intList.add(200); + intList.add(300); + + for (int i = 0; i < intList.size(); i++) { + int num = intList.get(i); + System.out.println(num); + } + } +} \ No newline at end of file diff --git a/project/generic/src/main/java/com/itheima/demo10/Fruit.java b/project/generic/src/main/java/com/itheima/demo10/Fruit.java new file mode 100644 index 00000000..f8521006 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo10/Fruit.java @@ -0,0 +1,36 @@ +package com.itheima.demo10; + +import java.lang.reflect.Array; + +public class Fruit { + + private T[] array; + + public Fruit(Class clz, int length){ + //通过Array.newInstance创建泛型数组 + array = (T[])Array.newInstance(clz, length); + } + + /** + * 填充数组 + * @param index + * @param item + */ + public void put(int index, T item) { + array[index] = item; + } + + /** + * 获取数组元素 + * @param index + * @return + */ + public T get(int index) { + return array[index]; + } + + public T[] getArray() { + return array; + } + +} diff --git a/project/generic/src/main/java/com/itheima/demo10/Test02.java b/project/generic/src/main/java/com/itheima/demo10/Test02.java new file mode 100644 index 00000000..b8a63215 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo10/Test02.java @@ -0,0 +1,43 @@ +package com.itheima.demo10; + +import java.lang.reflect.Array; +import java.util.Arrays; + +/** + * @author chenpq05 + * @since 2023/3/23 9:46 + */ +public class Test02 { + + private T[] arr; + + public Test02(Class clz, int length) { + this.arr = (T[])Array.newInstance(clz, length); + } + + public T get(int length) { + return arr[length]; + } + + public void put(int index, T e) { + arr[index] = e; + } + + public T[] getArr() { + return arr; + } + + public static void main(String[] args) { + Test02 test02 = new Test02(String.class, 5); + test02.put(0, "aa0000a"); + test02.put(1, "aaa"); + test02.put(2, "bbb"); + + String s = test02.get(2); + System.out.println(s); + + String[] arr = test02.getArr(); + System.out.println(Arrays.toString(arr)); + } + +} diff --git a/project/generic/src/main/java/com/itheima/demo10/Test10.java b/project/generic/src/main/java/com/itheima/demo10/Test10.java new file mode 100644 index 00000000..278a6f1c --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo10/Test10.java @@ -0,0 +1,38 @@ +package com.itheima.demo10; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * 泛型与数组 + */ +public class Test10 { + public static void main(String[] args) { +// ArrayList[] list = new ArrayList[5]; +// ArrayList[] listArr = list; + ArrayList[] listArr = new ArrayList[5]; + + ArrayList[] arrayListArr = new ArrayList[5]; + + ArrayList intList = new ArrayList(); + intList.add(100); + + ArrayList strList = new ArrayList(); + strList.add("abc"); + + //listArr[0] = intList; + listArr[0] = strList; + //list[0] = intList; + String s = listArr[0].get(0); + System.out.println(s); + System.out.println("-------------------------------------------------------"); + Fruit fruit = new Fruit<>(String.class,3); + fruit.put(0,"苹果"); + fruit.put(1,"西瓜"); + fruit.put(2,"香蕉"); + + System.out.println(Arrays.toString(fruit.getArray())); + String s1 = fruit.get(2); + System.out.println(s1); + } +} diff --git a/project/generic/src/main/java/com/itheima/demo11/Person.java b/project/generic/src/main/java/com/itheima/demo11/Person.java new file mode 100644 index 00000000..d10e77df --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo11/Person.java @@ -0,0 +1,13 @@ +package com.itheima.demo11; + +public class Person { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/project/generic/src/main/java/com/itheima/demo11/Test11.java b/project/generic/src/main/java/com/itheima/demo11/Test11.java new file mode 100644 index 00000000..3265f8dc --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo11/Test11.java @@ -0,0 +1,27 @@ +package com.itheima.demo11; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * 泛型与反射 + */ +public class Test11 { + public static void main(String[] args) throws Exception { +// Class personClass = Person.class; +// Constructor constructor = personClass.getConstructor(); +// Person person = constructor.newInstance(); +// Class personClass = Person.class; +// Constructor constructor = personClass.getConstructor(); +// Object o = constructor.newInstance(); + + Class personClass = Person.class; + Constructor constructor = personClass.getConstructor(); + Person p = constructor.newInstance(); + + Class clz = Person.class; + Constructor con = clz.getConstructor(); + Object o = con.newInstance(); + + } +} diff --git a/project/generic/src/main/java/com/itheima/demo2/Generic.java b/project/generic/src/main/java/com/itheima/demo2/Generic.java new file mode 100644 index 00000000..3d5c77bf --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo2/Generic.java @@ -0,0 +1,30 @@ +package com.itheima.demo2; + +/** + * 泛型类的定义 + * @param 泛型标识--类型形参 + * T 创建对象的时候里指定具体的数据类型。 + */ +public class Generic { + //T,是由外部使用类的时候来指定。 + private T key; + + public Generic(T key) { + this.key = key; + } + + public T getKey() { + return key; + } + + public void setKey(T key) { + this.key = key; + } + + @Override + public String toString() { + return "Generic{" + + "key=" + key + + '}'; + } +} diff --git a/project/generic/src/main/java/com/itheima/demo2/MainClass.java b/project/generic/src/main/java/com/itheima/demo2/MainClass.java new file mode 100644 index 00000000..041b291b --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo2/MainClass.java @@ -0,0 +1,51 @@ +package com.itheima.demo2; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; + +/** + * 泛型类 + */ +public class MainClass { + public static void main(String[] args) { + // //泛型类在创建对象的时候,来指定操作的具体数据类型。 + Generic strGeneric = new Generic<>("abc"); + // String key1 = strGeneric.getKey(); + // System.out.println("key1:" + key1); + // + // System.out.println("-----------------------------------"); + // Generic intGeneric = new Generic<>(100); + // int key2 = intGeneric.getKey(); + // System.out.println("key2:" + key2); + // + // System.out.println("-----------------------------------"); + // //泛型类在创建对象的时候,没有指定类型,将按照Object类型来操作。 + // Generic generic = new Generic("ABC"); + // Object key3 = generic.getKey(); + // System.out.println("key3:" + key3); + // + // //泛型类,不支持基本数据类型。 + // //Generic generic1 = new Generic(100); + // + // System.out.println("-----------------------------------"); + // //同一泛型类,根据不同的数据类型创建的对象,本质上是同一类型。 + // System.out.println(intGeneric.getClass()); + // System.out.println(strGeneric.getClass()); + // System.out.println(intGeneric.getClass() == strGeneric.getClass()); + + + Type genericSuperclass = strGeneric.getClass().getGenericSuperclass(); + System.out.println(genericSuperclass.getTypeName()); + + + System.out.println( genericSuperclass instanceof ParameterizedType); // true + System.out.println( genericSuperclass instanceof Class); // false + System.out.println( genericSuperclass instanceof WildcardType); // false + System.out.println( genericSuperclass instanceof GenericArrayType); // false + + // ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + // System.out.println(parameterizedType.getActualTypeArguments()[0]); + } +} diff --git a/project/generic/src/main/java/com/itheima/demo3/MainClass.java b/project/generic/src/main/java/com/itheima/demo3/MainClass.java new file mode 100644 index 00000000..78e068e5 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo3/MainClass.java @@ -0,0 +1,26 @@ +package com.itheima.demo3; + +public class MainClass { + public static void main(String[] args) { + //创建抽奖器对象,指定数据类型 + ProductGetter stringProductGetter = new ProductGetter<>(); + String[] strProducts = {"苹果手机","华为手机","扫地机器人","咖啡机"}; + //给抽奖器中,填充奖品 + for (int i = 0; i < strProducts.length; i++) { + stringProductGetter.addProduct(strProducts[i]); + } + //抽奖 + String product1 = stringProductGetter.getProduct(); + System.out.println("恭喜您,你抽中了:" + product1); + + System.out.println("----------------------------------------"); + ProductGetter integerProductGetter = new ProductGetter<>(); + int[] intProducts = {10000,5000,3000,500,300000}; + for (int i = 0; i < intProducts.length; i++) { + integerProductGetter.addProduct(intProducts[i]); + } + + Integer product2 = integerProductGetter.getProduct(); + System.out.println("恭喜您,你抽中了:" + product2); + } +} diff --git a/project/generic/src/main/java/com/itheima/demo3/ProductGetter.java b/project/generic/src/main/java/com/itheima/demo3/ProductGetter.java new file mode 100644 index 00000000..bc977280 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo3/ProductGetter.java @@ -0,0 +1,71 @@ +package com.itheima.demo3; + +import java.util.ArrayList; +import java.util.Random; + +/** + * 抽奖器 + * @param + */ +public class ProductGetter { + static Random random = new Random(); + //奖品 + private T product; + + //奖品池 + ArrayList list = new ArrayList<>(); + + /** + * 添加奖品 + * @param t 奖品 + */ + public void addProduct(T t) { + list.add(t); + } + + /** + * 抽奖 + * @return + */ + public T getProduct() { + product = list.get(random.nextInt(list.size())); + return product; + } + + /** + * 定义泛型方法 + * @param list 参数 + * @param 泛型标识,具体类型,由调用方法的时候来指定。 + * @return + */ + public T getProduct(ArrayList list) { + return list.get(random.nextInt(list.size())); + } + + /** + * 静态的泛型方法,采用多个泛型类型 + * @param t + * @param e + * @param k + * @param + * @param + * @param + */ + public static void printType(T t, E e, K k) { + System.out.println(t + "\t" + t.getClass().getSimpleName()); + System.out.println(e + "\t" + e.getClass().getSimpleName()); + System.out.println(k + "\t" + k.getClass().getSimpleName()); + } + + /** + * 泛型可变参数的定义 + * @param e + * @param + */ + public static void print(E... e){ + for (int i = 0; i < e.length; i++) { + System.out.println(e[i]); + } + } + +} diff --git a/project/generic/src/main/java/com/itheima/demo4/ChildFirst.java b/project/generic/src/main/java/com/itheima/demo4/ChildFirst.java new file mode 100644 index 00000000..81734b0e --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo4/ChildFirst.java @@ -0,0 +1,12 @@ +package com.itheima.demo4; + +/** + * 泛型类派生子类,子类也是泛型类,那么子类的泛型标识要和父类一致。 + * @param + */ +public class ChildFirst extends Parent { + @Override + public T getValue() { + return super.getValue(); + } +} diff --git a/project/generic/src/main/java/com/itheima/demo4/ChildSecond.java b/project/generic/src/main/java/com/itheima/demo4/ChildSecond.java new file mode 100644 index 00000000..49fc7269 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo4/ChildSecond.java @@ -0,0 +1,16 @@ +package com.itheima.demo4; + +/** + * 泛型类派生子类,如果子类不是泛型类,那么父类要明确数据类型 + */ +public class ChildSecond extends Parent { + @Override + public Integer getValue() { + return super.getValue(); + } + + @Override + public void setValue(Integer value) { + super.setValue(value); + } +} diff --git a/project/generic/src/main/java/com/itheima/demo4/Parent.java b/project/generic/src/main/java/com/itheima/demo4/Parent.java new file mode 100644 index 00000000..382a9a03 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo4/Parent.java @@ -0,0 +1,13 @@ +package com.itheima.demo4; + +public class Parent { + private E value; + + public E getValue() { + return value; + } + + public void setValue(E value) { + this.value = value; + } +} diff --git a/project/generic/src/main/java/com/itheima/demo4/Test04.java b/project/generic/src/main/java/com/itheima/demo4/Test04.java new file mode 100644 index 00000000..4191b5a3 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo4/Test04.java @@ -0,0 +1,15 @@ +package com.itheima.demo4; + +public class Test04 { + public static void main(String[] args) { + ChildFirst childFirst = new ChildFirst<>(); + childFirst.setValue("abc"); + String value = childFirst.getValue(); + System.out.println(value); + System.out.println("---------------------------------"); + ChildSecond childSecond = new ChildSecond(); + childSecond.setValue(100); + Integer value1 = childSecond.getValue(); + System.out.println(value1); + } +} diff --git a/project/generic/src/main/java/com/itheima/demo5/Apple.java b/project/generic/src/main/java/com/itheima/demo5/Apple.java new file mode 100644 index 00000000..d2e816bd --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo5/Apple.java @@ -0,0 +1,11 @@ +package com.itheima.demo5; + +/** + * 实现泛型接口的类,不是泛型类,需要明确实现泛型接口的数据类型。 + */ +public class Apple implements Generator { + @Override + public String getKey() { + return "hello generic"; + } +} diff --git a/project/generic/src/main/java/com/itheima/demo5/Generator.java b/project/generic/src/main/java/com/itheima/demo5/Generator.java new file mode 100644 index 00000000..3aba2249 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo5/Generator.java @@ -0,0 +1,11 @@ +package com.itheima.demo5; + +/** + * 泛型接口 + * @param + */ +public interface Generator { + T getKey(); +} + + diff --git a/project/generic/src/main/java/com/itheima/demo5/Pair.java b/project/generic/src/main/java/com/itheima/demo5/Pair.java new file mode 100644 index 00000000..6f9b2ec1 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo5/Pair.java @@ -0,0 +1,26 @@ +package com.itheima.demo5; + +/** + * 泛型接口的实现类,是一个泛型类,那么要保证实现接口的泛型类泛型标识包含泛型接口的泛型标识 + * @param + * @param + */ +public class Pair implements Generator { + + private T key; + private E value; + + public Pair(T key, E value) { + this.key = key; + this.value = value; + } + + @Override + public T getKey() { + return key; + } + + public E getValue() { + return value; + } +} diff --git a/project/generic/src/main/java/com/itheima/demo5/Test05.java b/project/generic/src/main/java/com/itheima/demo5/Test05.java new file mode 100644 index 00000000..eaf1135e --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo5/Test05.java @@ -0,0 +1,18 @@ +package com.itheima.demo5; + +/** + * 泛型接口 + */ +public class Test05 { + public static void main(String[] args) { + Apple apple = new Apple(); + String key = apple.getKey(); + System.out.println(key); + + System.out.println("---------------------------------"); + Pair pair = new Pair<>("count",100); + String key1 = pair.getKey(); + Integer value = pair.getValue(); + System.out.println(key1 + "=" + value); + } +} diff --git a/project/generic/src/main/java/com/itheima/demo6/Test06.java b/project/generic/src/main/java/com/itheima/demo6/Test06.java new file mode 100644 index 00000000..0f49badc --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo6/Test06.java @@ -0,0 +1,46 @@ +package com.itheima.demo6; + +import com.itheima.demo3.ProductGetter; + +import java.util.ArrayList; + +/** + * 泛型方法的使用 + */ +public class Test06 { + public static void main(String[] args) { + ProductGetter productGetter = new ProductGetter<>(); + int[] products = {100,200,400}; + for (int i = 0; i < products.length; i++) { + productGetter.addProduct(products[i]); + } + //泛型类的成员方法的调用 + Integer product = productGetter.getProduct(); + System.out.println(product + "\t" + product.getClass().getSimpleName()); + System.out.println("---------------------------------------------------"); + ArrayList strList = new ArrayList<>(); + strList.add("笔记本电脑"); + strList.add("苹果手机"); + strList.add("扫地机器人"); + //泛型方法的调用,类型是通过调用方法的时候来指定。 + String product1 = productGetter.getProduct(strList); + System.out.println(product1 + "\t" + product1.getClass().getSimpleName()); + System.out.println("---------------------------------------------------"); + ArrayList intList = new ArrayList<>(); + intList.add(1000); + intList.add(5000); + intList.add(3000); + Integer product2 = productGetter.getProduct(intList); + System.out.println(product2 + "\t" + product2.getClass().getSimpleName()); + System.out.println("---------------------------------------------------"); + //调用多个泛型类型的静态泛型方法 + ProductGetter.printType(100,"java",true); + ProductGetter.printType(false,false,true); + System.out.println("---------------------------------------------------"); + //可变参数的泛型方法的调用 + ProductGetter.print(1,2,3,4,5); + System.out.println("---------------------------------------------------"); + ProductGetter.print("a","b","c"); + + } +} diff --git a/project/generic/src/main/java/com/itheima/demo7/Box.java b/project/generic/src/main/java/com/itheima/demo7/Box.java new file mode 100644 index 00000000..2cff3bb8 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo7/Box.java @@ -0,0 +1,13 @@ +package com.itheima.demo7; + +public class Box { + private E first; + + public E getFirst() { + return first; + } + + public void setFirst(E first) { + this.first = first; + } +} diff --git a/project/generic/src/main/java/com/itheima/demo7/Test07.java b/project/generic/src/main/java/com/itheima/demo7/Test07.java new file mode 100644 index 00000000..d0f30e9c --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo7/Test07.java @@ -0,0 +1,26 @@ +package com.itheima.demo7; + +/** + * 类型通配符 + */ +public class Test07 { + public static void main(String[] args) { + Box box1 = new Box<>(); + box1.setFirst(100); + showBox(box1); + + Box box2 = new Box<>(); + box2.setFirst(200); + showBox(box2); + } + + public static void showBox(Box box) { + Number first = box.getFirst(); + System.out.println(first); + } + +// public static void showBox(Box box) { +// Number first = box.getFirst(); +// System.out.println(first); +// } +} diff --git a/project/generic/src/main/java/com/itheima/demo8/Animal.java b/project/generic/src/main/java/com/itheima/demo8/Animal.java new file mode 100644 index 00000000..e39ddd99 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo8/Animal.java @@ -0,0 +1,16 @@ +package com.itheima.demo8; + +public class Animal { + public String name; + + public Animal(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Animal{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/project/generic/src/main/java/com/itheima/demo8/Cat.java b/project/generic/src/main/java/com/itheima/demo8/Cat.java new file mode 100644 index 00000000..aa3426ea --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo8/Cat.java @@ -0,0 +1,18 @@ +package com.itheima.demo8; + +public class Cat extends Animal { + public int age; + + public Cat(String name, int age) { + super(name); + this.age = age; + } + + @Override + public String toString() { + return "Cat{" + + "age=" + age + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/project/generic/src/main/java/com/itheima/demo8/MiniCat.java b/project/generic/src/main/java/com/itheima/demo8/MiniCat.java new file mode 100644 index 00000000..74795274 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo8/MiniCat.java @@ -0,0 +1,19 @@ +package com.itheima.demo8; + +public class MiniCat extends Cat { + public int level; + + public MiniCat(String name, int age, int level) { + super(name, age); + this.level = level; + } + + @Override + public String toString() { + return "MiniCat{" + + "level=" + level + + ", age=" + age + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/project/generic/src/main/java/com/itheima/demo8/Test08.java b/project/generic/src/main/java/com/itheima/demo8/Test08.java new file mode 100644 index 00000000..ba867e18 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo8/Test08.java @@ -0,0 +1,43 @@ +package com.itheima.demo8; + +import java.util.Comparator; +import java.util.TreeSet; + +/** + * 类型通配符下限的使用 + */ +public class Test08 { + public static void main(String[] args) { + //TreeSet treeSet = new TreeSet<>(new Comparator2()); + TreeSet treeSet = new TreeSet(new Comparator1()); + treeSet.add(new Cat("jerry",20)); + treeSet.add(new Cat("amy",22)); + treeSet.add(new Cat("frank",35)); + treeSet.add(new Cat("jim",15)); + for (Cat cat : treeSet) { + System.out.println(cat); + } + } +} + +class Comparator1 implements Comparator { + @Override + public int compare(Animal o1, Animal o2) { + return o1.name.compareTo(o2.name); + } +} + +class Comparator2 implements Comparator { + @Override + public int compare(Cat o1, Cat o2) { + return o1.age - o2.age; + } +} + +class Comparator3 implements Comparator { + @Override + public int compare(MiniCat o1, MiniCat o2) { + return o1.level - o2.level; + } +} + diff --git a/project/generic/src/main/java/com/itheima/demo8/TestDown.java b/project/generic/src/main/java/com/itheima/demo8/TestDown.java new file mode 100644 index 00000000..7235bc2c --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo8/TestDown.java @@ -0,0 +1,30 @@ +package com.itheima.demo8; + +import java.util.ArrayList; +import java.util.List; + +/** + * 类型通配符下限 + */ +public class TestDown { + public static void main(String[] args) { + ArrayList animals = new ArrayList<>(); + ArrayList cats = new ArrayList<>(); + ArrayList miniCats = new ArrayList<>(); + showAnimal(animals); + showAnimal(cats); + //showAnimal(miniCats); + } + + /** + * 类型通配符下限,要求集合只能是Cat或Cat的父类类型 + * @param list + */ + public static void showAnimal(List list) { +// list.add(new Cat()); +// list.add(new MiniCat()); + for (Object o : list) { + System.out.println(o); + } + } +} diff --git a/project/generic/src/main/java/com/itheima/demo8/TestUp.java b/project/generic/src/main/java/com/itheima/demo8/TestUp.java new file mode 100644 index 00000000..1a03226d --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo8/TestUp.java @@ -0,0 +1,38 @@ +package com.itheima.demo8; + +import java.util.ArrayList; + +/** + * 泛型通配符上限 + */ +public class TestUp { + public static void main(String[] args) { + ArrayList animals = new ArrayList<>(); + ArrayList cats = new ArrayList<>(); + ArrayList miniCats = new ArrayList<>(); + + cats.addAll(cats); + cats.addAll(miniCats); + //showAnimal(animals); + showAnimal(cats); + showAnimal(miniCats); + + System.out.println("--------------------------------------------"); + + } + + /** + * 泛型上限通配符,传递的集合类型,只能是Cat或Cat的子类类型。 + * @param list + */ + public static void showAnimal(ArrayList list) { + //list.add(new Animal()); + //list.add(new Cat()); + //list.add(new MiniCat()); + for (int i = 0; i < list.size(); i++) { + Cat cat = list.get(i); + System.out.println(cat); + } + } + +} diff --git a/project/generic/src/main/java/com/itheima/demo9/Erasure.java b/project/generic/src/main/java/com/itheima/demo9/Erasure.java new file mode 100644 index 00000000..6da4d5c7 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo9/Erasure.java @@ -0,0 +1,25 @@ +package com.itheima.demo9; + +import java.util.List; + +public class Erasure { + private T key; + + public T getKey() { + return key; + } + + public void setKey(T key) { + this.key = key; + } + + /** + * 泛型方法 + * @param t + * @param + * @return + */ + public T show(T t) { + return t; + } +} diff --git a/project/generic/src/main/java/com/itheima/demo9/Info.java b/project/generic/src/main/java/com/itheima/demo9/Info.java new file mode 100644 index 00000000..747a9a73 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo9/Info.java @@ -0,0 +1,9 @@ +package com.itheima.demo9; + +/** + * 泛型接口 + * @param + */ +public interface Info { + T info(T t); +} diff --git a/project/generic/src/main/java/com/itheima/demo9/InfoImpl.java b/project/generic/src/main/java/com/itheima/demo9/InfoImpl.java new file mode 100644 index 00000000..06d157e1 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo9/InfoImpl.java @@ -0,0 +1,8 @@ +package com.itheima.demo9; + +public class InfoImpl implements Info { + @Override + public Integer info(Integer value) { + return value; + } +} diff --git a/project/generic/src/main/java/com/itheima/demo9/Test09.java b/project/generic/src/main/java/com/itheima/demo9/Test09.java new file mode 100644 index 00000000..d12e5b35 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/demo9/Test09.java @@ -0,0 +1,44 @@ +package com.itheima.demo9; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; + +/** + * 泛型擦除 + */ +public class Test09 { + public static void main(String[] args) { +// ArrayList intList = new ArrayList<>(); +// ArrayList strList = new ArrayList<>(); +// +// System.out.println(intList.getClass().getSimpleName()); +// System.out.println(strList.getClass().getSimpleName()); +// +// System.out.println(intList.getClass() == strList.getClass()); + Erasure erasure = new Erasure<>(); + //利用反射,获取Erasure类的字节码文件的Class类对象 + Class clz = erasure.getClass(); + //获取所有的成员变量 + Field[] declaredFields = clz.getDeclaredFields(); + for (Field declaredField : declaredFields) { + //打印成员变量的名称和类型 + System.out.println(declaredField.getName() + ":" + declaredField.getType().getSimpleName()); + } + System.out.println("----------------------------------------------------------------"); + //获取所有的方法 + Method[] declaredMethods = clz.getDeclaredMethods(); + for (Method declaredMethod : declaredMethods) { + //打印方法名和方法的返回值类型。 + System.out.println(declaredMethod.getName() + ":" + declaredMethod.getReturnType().getSimpleName()); + } + System.out.println("----------------------------------------------------------------"); + Class infoClass = InfoImpl.class; + //获取所有的方法 + Method[] infoImplMethods = infoClass.getDeclaredMethods(); + for (Method method : infoImplMethods) { + //打印方法名和方法的返回值类型。 + System.out.println(method.getName() + ":" + method.getReturnType().getSimpleName()); + } + } +} diff --git a/project/generic/src/main/java/com/itheima/typereference/Test01.java b/project/generic/src/main/java/com/itheima/typereference/Test01.java new file mode 100644 index 00000000..f69c47fe --- /dev/null +++ b/project/generic/src/main/java/com/itheima/typereference/Test01.java @@ -0,0 +1,40 @@ +package com.itheima.typereference; + +import com.fasterxml.jackson.core.type.TypeReference; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; + +/** + * @author chenpq05 + * @since 2023/3/23 13:57 + */ +public class Test01 { + + public static void main(String[] args) { + + // new TypeReference<>() + + TypeRef02> typeRef02 = new TypeRef02>() { + }; + + Type type02 = typeRef02.getType(); + System.out.println(type02); + + ParameterizedType parameterizedType02 = (ParameterizedType) typeRef02.getClass().getGenericSuperclass(); + System.out.println(parameterizedType02); + + ParameterizedType parameterizedTypeMap = (ParameterizedType) parameterizedType02.getActualTypeArguments()[0]; + System.out.println(parameterizedTypeMap); + + Class clz1 = (Class) parameterizedTypeMap.getActualTypeArguments()[0]; + Class clz2 = (Class) parameterizedTypeMap.getActualTypeArguments()[1]; + System.out.println(clz1); + System.out.println(clz2); + + System.out.println(""); + + } + +} diff --git a/project/generic/src/main/java/com/itheima/typereference/TypeRef.java b/project/generic/src/main/java/com/itheima/typereference/TypeRef.java new file mode 100644 index 00000000..432881c2 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/typereference/TypeRef.java @@ -0,0 +1,24 @@ +package com.itheima.typereference; + +import com.itheima.demo11.Person; + +import java.lang.reflect.ParameterizedType; + +/** + * @author chenpq05 + * @since 2023/3/23 10:49 + */ +abstract class TypeRef { + + public Class getGenericClass() { + ParameterizedType parameterizedType = (ParameterizedType) getClass().getGenericSuperclass(); + return (Class) parameterizedType.getActualTypeArguments()[0]; + } + + public static void main(String[] args) { + TypeRef superClass = new TypeRef() { + }; + System.out.println(superClass.getGenericClass()); + } + +} diff --git a/project/generic/src/main/java/com/itheima/typereference/TypeRef02.java b/project/generic/src/main/java/com/itheima/typereference/TypeRef02.java new file mode 100644 index 00000000..d82b8cd5 --- /dev/null +++ b/project/generic/src/main/java/com/itheima/typereference/TypeRef02.java @@ -0,0 +1,31 @@ +package com.itheima.typereference; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * @author chenpq05 + * @since 2023/3/23 14:00 + */ +public abstract class TypeRef02 implements Comparable> { + + protected final Type _type; + + protected TypeRef02() { + Type superClass = getClass().getGenericSuperclass(); + if (superClass instanceof Class) { + throw new IllegalArgumentException("非法参数"); + } + _type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + } + + public Type getType() { + return _type; + } + + @Override + public int compareTo(TypeRef02 o) { + return 0; + } + +} diff --git a/project/gradle-learn/runGroovy.groovy b/project/gradle-learn/runGroovy.groovy new file mode 100644 index 00000000..791d720c --- /dev/null +++ b/project/gradle-learn/runGroovy.groovy @@ -0,0 +1,27 @@ +/** + * 可选的类型定义 + */ +def version = 1 + +/** + * assert语句 + */ +assert version == 1 + +/** + * 括号可选。 + */ +println(version) +println version + +/** + * 字符串 + */ +def s1 = '单引号中就纯粹的字符串' +def s2 = "双引号中通过\$后取变量${version}" +def s3 = '''三个单引号 +可以 +换行''' +println s1 +println s2 +println s3 diff --git a/project/java21/pom.xml b/project/java21/pom.xml new file mode 100644 index 00000000..b3f4bd51 --- /dev/null +++ b/project/java21/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + java21 + + + 21 + 21 + UTF-8 + + + \ No newline at end of file diff --git a/project/java21/src/main/java/org/cpq/JDK21.java b/project/java21/src/main/java/org/cpq/JDK21.java new file mode 100644 index 00000000..d613d914 --- /dev/null +++ b/project/java21/src/main/java/org/cpq/JDK21.java @@ -0,0 +1,86 @@ +package org.cpq; + + +public class JDK21 { + public static void main(String[] args) { + + /** + * 序列化集合 + */ + // ArrayList al = new ArrayList<>(); + // al.add(1); + // al.addFirst(0); + // al.addLast(2); + // System.out.println(al.getFirst()); + // System.out.println(al.getLast()); + // System.out.println(al.reversed()); + + // LinkedHashMap map = new LinkedHashMap<>(); + // map.put(2, "two"); + // map.putFirst(1, "one"); + // map.putLast(3, "three"); + // System.out.println(map); + // + // Map.Entry entry = map.pollFirstEntry(); + // System.out.println(entry); + // System.out.println(map); + // + // System.out.println(map.reversed()); + // System.out.println(map); + + /** + * 记录模式 + */ + // 记录模式 与 instanceof 结合使用 + // Shape circle = new Shape("circle", 1); + // if (circle instanceof Shape(String type, long unit)) { + // System.out.println("type = " + type + "," + "unit = " + unit ); + // } + + // 记录模式 与 switch 结合使用 + // IShape shape = new Square(11); + // IShape shape = new Rectangle(11, 2); + // switch (shape) { + // case Circle(double radius): + // System.out.println("shape is circle radius = " + radius); + // break; + // case Square(double s): + // System.out.println("shape is square side = " + s); + // break; + // case Rectangle(double length, double width): + // System.out.println("shape is Rectangle length = " + length); + // break; + // default: + // System.out.println("default....."); + // break; + // } + + + /** + * switch 的模式匹配 + */ + System.out.println(formatterPatternSwitch(1)); + + + + } + + static String formatterPatternSwitch(Object obj) { + return switch (obj) { + case Integer i -> String.format("int %d", i); + case Long l -> String.format("long %d", l); + case Double d -> String.format("double %f", d); + case String s -> String.format("String %s", s); + default -> String.format("unsupported type %s", obj.getClass().getName()); + }; + } + +} + +// 定义记录类 +record Shape(String type, long unit){} + +interface IShape{} +record Circle(double radius) implements IShape{} +record Square(double side) implements IShape{} +record Rectangle(double length, double width) implements IShape{} \ No newline at end of file diff --git a/project/java21/src/main/java/org/cpq/myvirtualthread/Demo.java b/project/java21/src/main/java/org/cpq/myvirtualthread/Demo.java new file mode 100644 index 00000000..61d9c016 --- /dev/null +++ b/project/java21/src/main/java/org/cpq/myvirtualthread/Demo.java @@ -0,0 +1,38 @@ +package org.cpq.myvirtualthread; + +public class Demo { + public static final VirtualThreadScheduler SCHEDULER = new VirtualThreadScheduler(); + + /** + * 模拟虚拟线程: + * + * 创建虚拟线程类 MyVirtualThread + * 创建调度器 VirtualThreadScheduler + * 使用queue保存虚拟线程 + * 使用平台线程运行虚拟线程 + * 使用ScopedValue替代线程的ThreadLocal + * 创建WaitingOperation实现虚拟线程等待一段时间后继续运行 + * Continuation.yield(MyVirtualThread.SCOPE); 实现虚拟线程等待 + * Timer实现虚拟线程重新加入调度器VirtualThreadScheduler,继续运行Continuation.yield(MyVirtualThread.SCOPE)中断点后面的代码 + * + * main方法会创建100个虚拟线程,每个虚拟线程运行到一半会调用WaitingOperation.perform()模拟IO等待, + * 等待一段时间后继续运行,此时虚拟线程可能会在其他平台线程中运行 + */ + public static void main(String[] args) { + new Thread(SCHEDULER::start).start(); + for (int i = 0; i < 100; i++) { + MyVirtualThread vt1 = new MyVirtualThread(() -> { + System.out.println("1.1"); + WaitingOperation.perform("发送HTTP请求,需要时间", 200); + System.out.println("1.2"); + }); + MyVirtualThread vt2 = new MyVirtualThread(() -> { + System.out.println("2.1"); + WaitingOperation.perform("查询数据库,需要时间", 300); + System.out.println("2.2"); + }); + SCHEDULER.schedule(vt1); + SCHEDULER.schedule(vt2); + } + } +} diff --git a/project/java21/src/main/java/org/cpq/myvirtualthread/MyVirtualThread.java b/project/java21/src/main/java/org/cpq/myvirtualthread/MyVirtualThread.java new file mode 100644 index 00000000..0d47a60f --- /dev/null +++ b/project/java21/src/main/java/org/cpq/myvirtualthread/MyVirtualThread.java @@ -0,0 +1,25 @@ +package org.cpq.myvirtualthread; + +import jdk.internal.vm.Continuation; +import jdk.internal.vm.ContinuationScope; + +import java.util.concurrent.atomic.AtomicInteger; + +public class MyVirtualThread { + private static final AtomicInteger COUNTER = new AtomicInteger(1); + public static final ContinuationScope SCOPE = new ContinuationScope("我的虚拟线程"); + + private Continuation cont; + private int id; + + public MyVirtualThread(Runnable runnable) { + id = COUNTER.getAndIncrement(); + cont = new Continuation(SCOPE, runnable); + } + + public void run() { + System.out.println("MyVirtualThread " + id + " is running on" + Thread.currentThread()); + cont.run(); + } + +} diff --git a/project/java21/src/main/java/org/cpq/myvirtualthread/VirtualThreadScheduler.java b/project/java21/src/main/java/org/cpq/myvirtualthread/VirtualThreadScheduler.java new file mode 100644 index 00000000..7e72d10e --- /dev/null +++ b/project/java21/src/main/java/org/cpq/myvirtualthread/VirtualThreadScheduler.java @@ -0,0 +1,31 @@ +package org.cpq.myvirtualthread; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class VirtualThreadScheduler { + + public static ScopedValue MY_VIRTUAL_THREAD = ScopedValue.newInstance(); + + private Queue queue = new ConcurrentLinkedQueue<>(); + private ExecutorService executor = Executors.newFixedThreadPool(10); + + public void start() { + while (true) { + if (!queue.isEmpty()) { + MyVirtualThread virtualThread = queue.remove(); + executor.submit(() -> ScopedValue + .where(MY_VIRTUAL_THREAD, virtualThread) + .run(virtualThread::run) + ); + + } + } + } + + public void schedule(MyVirtualThread virtualThread) { + queue.add(virtualThread); + } +} diff --git a/project/java21/src/main/java/org/cpq/myvirtualthread/WaitingOperation.java b/project/java21/src/main/java/org/cpq/myvirtualthread/WaitingOperation.java new file mode 100644 index 00000000..75039c18 --- /dev/null +++ b/project/java21/src/main/java/org/cpq/myvirtualthread/WaitingOperation.java @@ -0,0 +1,26 @@ +package org.cpq.myvirtualthread; + +import jdk.internal.vm.Continuation; + +import java.util.Timer; +import java.util.TimerTask; + +public class WaitingOperation { + public static void perform(String name, int duration) { + MyVirtualThread myVirtualThread = VirtualThreadScheduler.MY_VIRTUAL_THREAD.get(); + System.out.println("Waiting for "+name+" for "+duration+" milliseconds"); + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + // 等待duration毫秒后,将虚拟线程重新加入调度器队列,再次运行, + // 再次运行的点是Continuation.yield之后的代码,即WaitingOperation.perform()之后的代码 + Demo.SCHEDULER.schedule(myVirtualThread); + timer.cancel(); + } + }, duration); + + // 虚拟线程进入等待 + Continuation.yield(MyVirtualThread.SCOPE); + } +} diff --git a/project/java21/src/main/java/org/cpq/virtualthread/ContinuationLearn.java b/project/java21/src/main/java/org/cpq/virtualthread/ContinuationLearn.java new file mode 100644 index 00000000..0fad2f97 --- /dev/null +++ b/project/java21/src/main/java/org/cpq/virtualthread/ContinuationLearn.java @@ -0,0 +1,60 @@ +package org.cpq.virtualthread; + +import jdk.internal.vm.Continuation; +import jdk.internal.vm.ContinuationScope; + +public class ContinuationLearn { + + public static void main(String[] args) throws Exception { + // Continuation continuation = getContinuation01(); + // continuation.run(); + + Continuation continuation = getContinuation02(); + continuation.run(); + System.out.println("do some thing after yield A"); + continuation.run(); + System.out.println("do some thing after yield B"); + continuation.run(); + + boolean done = continuation.isDone(); + if (done){ + System.out.println("continuation terminated , you can not run continuation.run()"); + } + } + + + /** + * Continuation是java底层内部api对象, + * 1、需要在idea配置 -> java Compiler -> xxx line parameters 填写: + * --enable-preview --add-exports java.base/jdk.internal.vm=ALL-UNNAMED + * 2、还需要在idea本类配置VM参数 + * --enable-preview + --add-exports java.base/jdk.internal.vm=ALL-UNNAMED + * 才能使用 + */ + private static Continuation getContinuation01() { + var scope = new ContinuationScope("test"); + /** + * Runnable被包装在Continuation中,但Continuation不是线程,用Continuation运行线程,更加节省内存 + */ + var continuation = new Continuation(scope, () -> { + System.out.println("A"); + System.out.println("B"); + System.out.println("C"); + }); + return continuation; + } + + private static Continuation getContinuation02() { + var scope = new ContinuationScope("test"); + var continuation = new Continuation(scope, () -> { + System.out.println("A"); + // 暂停Continuation的Runnable。程序控制权将返回到调用continuation.run()的线程。。 + Continuation.yield(scope); + System.out.println("B"); + Continuation.yield(scope); + System.out.println("C"); + }); + return continuation; + } +} diff --git a/project/java21/src/main/java/org/cpq/virtualthread/VirtualThread.java b/project/java21/src/main/java/org/cpq/virtualthread/VirtualThread.java new file mode 100644 index 00000000..90e8ece9 --- /dev/null +++ b/project/java21/src/main/java/org/cpq/virtualthread/VirtualThread.java @@ -0,0 +1,54 @@ +package org.cpq.virtualthread; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +public class VirtualThread { + + public static void main(String[] args) throws Exception { + /** + * 虚拟线程与平台线程执行速度比较 + */ + // List threadList = IntStream.range(0, 10000).mapToObj(new IntFunction() { + // @Override + // public Thread apply(int value) { + // // 创建虚拟线程 + // return Thread.ofVirtual().unstarted(() -> { + // System.out.println(Thread.currentThread()); + // }); + // // // 创建物理线程 + // // return Thread.ofPlatform().unstarted(() -> { + // // System.out.println(Thread.currentThread()); + // // }); + // } + // }).toList(); + // long begin = System.currentTimeMillis(); + // for (Thread thread : threadList) { + // thread.start(); + // thread.join(); + // } + // long end = System.currentTimeMillis(); + // System.out.println("耗时:" + (end - begin)); + + // 创建虚拟线程的4种方法 + Thread.startVirtualThread(() -> System.out.println("创建虚拟线程1")); + + Thread.ofVirtual().name("虚拟线程2").start(() -> System.out.println("创建虚拟线程2")); + Thread thread21 = Thread.ofVirtual().name("虚拟线程2-1").unstarted(() -> System.out.println("创建虚拟线程2-1")); + thread21.start(); + + ThreadFactory factory = Thread.ofVirtual().factory(); + factory.newThread(() -> System.out.println("通过ThreadFactory创建虚拟线程")).start(); + + try (ExecutorService service = Executors.newVirtualThreadPerTaskExecutor()) { + service.submit(() -> System.out.println("通过ExecutorService创建虚拟线程")); + } catch (Exception e) { + e.printStackTrace(); + } + + TimeUnit.SECONDS.sleep(10); + } + +} diff --git a/project/java9-17/.gitignore b/project/java9-17/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/project/java9-17/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/project/java9-17/pom.xml b/project/java9-17/pom.xml new file mode 100644 index 00000000..27f0b8ea --- /dev/null +++ b/project/java9-17/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.5 + + + com.example + java9-17 + 0.0.1-SNAPSHOT + java9-17 + java9-17 + + 17 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/project/java9-17/src/main/java/com/example/java917/Jdk9_17.java b/project/java9-17/src/main/java/com/example/java917/Jdk9_17.java new file mode 100644 index 00000000..2e114c78 --- /dev/null +++ b/project/java9-17/src/main/java/com/example/java917/Jdk9_17.java @@ -0,0 +1,144 @@ +package com.example.java917; + + +public class Jdk9_17 { + + public static void main(String[] args) throws Exception { + + // HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + // HttpRequest req = HttpRequest.newBuilder(URI.create("https://www.baidu.com")).build(); + // HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString()); + // System.out.println(resp.body()); + + // var list = new ArrayList(); + // list.add(11122); + // list.add(111); + // Integer i = list.get(0); + // System.out.println(i); + + // var simple = switch (lang) { + // case "java", "scale", "groory" -> "jvm"; + // case "c", "cqp" -> "c"; + // case "go" -> "g"; + // default -> { + // if (lang == null) { + // yield "unknown"; + // } else yield "non"; + // } + // } + + // var block = """ + // lang: java + // version: 13 + // dbname: mysql + // ip: 192.168.140.2 + // """; + // long count = block.lines().count(); + // System.out.println(count); + // + // var b = """ + // lang: java \ + // version: 12 \ + // ip: 22344234 + // """; + // System.out.println(b.lines().count()); + + // Object o = new Random().nextInt() % 2 == 0 ? "java16" : 1000.0d; + // if (o instanceof String s) { + // System.out.println(s.length()); + // } else { + // System.out.println(o); + // } + + + + } +} + +@FunctionalInterface +interface Mapper { + B map(A a); +} + +class MathUtilImpl implements MathUtil { + public static void main(String[] args) throws Exception { + Mapper aa = (var a) -> a.length(); + System.out.println(aa); + } + +} + +interface MathUtil { + // implements 该接口的类将自动获取该方法。 - jdk 8. + default void h(){ + g(); + } + + // 在接口定义的属性直接被视为 static。 -jdk 8. + double Pi = 3.1415; + + // 可以在接口直接定义静态方法。 - jdk 8. + static void f(){} + + // 可以在接口内直接定义私有方法。 -jdk 9. + private void g(){} +} + +@FunctionalInterface +interface Mapper11 { + B map(A a); +} +class JdkMapper11 { + public static void main(String[] args) throws Exception { + Mapper11 string2int = (var a) -> a.length(); + Integer i = string2int.map("sdf sf"); + System.out.println(i); + } +} + +record Student(String name, Integer age) { + + public static void main(String[] args) { + var student = new Student("名字", 11); + System.out.println(student.age); + System.out.println(student.name); + } + +} + + +sealed class People {} + +non-sealed class Teacher extends People{} + +sealed class Driver extends People{} + +non-sealed class TruckDriver extends Driver{} + +sealed class People1 permits Teacher1, Driver1{} +non-sealed class Teacher1 extends People1 {} +non-sealed class Driver1 extends People1{} + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project/java9-17/src/main/resources/application.properties b/project/java9-17/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/project/java9-17/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/project/java9-module/README.md b/project/java9-module/README.md new file mode 100644 index 00000000..4d67d75d --- /dev/null +++ b/project/java9-module/README.md @@ -0,0 +1,20 @@ +module-a/src/main/java/module-info.java + + module中的包默认是私有的 + 使用exports声明模块的包对其他模块开放,exports只能导出包,不支持导出类。如果对外导出的类少,推荐放到一个包下 + +module-b 导入 maven依赖 + + + org.cpq + module-a + 1.0-SNAPSHOT + +由于module-a只开放了org.cpq.a.a1,所以module-b只能使用org.cpq.a.a1包下的类 +不能使用org.cpq.a.a2包下的类 + +module默认是没有传递性的,module-b必须声明requires transitive org.cpq.a;才能让module-c使用module-a的包 + +module-a使用opens org.cpq.a.reflex;声明reflex包内的类可以反射访问 + +module-a使用exports + provides提供服务。module-b使用uses声明需要使用的服务,module-b使用ServiceLoader loads = ServiceLoader.load(Print.class);获取实现类 diff --git a/project/java9-module/module-a/pom.xml b/project/java9-module/module-a/pom.xml new file mode 100644 index 00000000..1eb20d41 --- /dev/null +++ b/project/java9-module/module-a/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + org.cpq + module-a + 1.0-SNAPSHOT + jar + + module-a + http://maven.apache.org + + + 21 + 21 + 21 + UTF-8 + + + diff --git a/project/java9-module/module-a/src/main/java/module-info.java b/project/java9-module/module-a/src/main/java/module-info.java new file mode 100644 index 00000000..12dbb6ef --- /dev/null +++ b/project/java9-module/module-a/src/main/java/module-info.java @@ -0,0 +1,21 @@ +module org.cpq.a { + + // exports只能导出一个包,不支持使用,拼接导出多个包 + exports org.cpq.a.a1; + + // to表示向org.cpq.c模块开放org.cpq.a.a2包 + // to 多个模块可以使用,分割 + // exports org.cpq.a.a2 to org.cpq.c; + + // opens导出的包,可以反射访问。 + // 但不能实例化,如果需要实例化,需要配合exports使用 + // open module org.cpq.a {} 表示模块org.cpq.a的所有包都可以被反射访问。 + opens org.cpq.a.reflex; + + // 为接口 Print 提供了两个具体实现类PrintImpl01、PrintImpl02 + // 还需要exports service包 + exports org.cpq.a.service; + provides org.cpq.a.service.Print + with org.cpq.a.impl.PrintImpl01, + org.cpq.a.impl.PrintImpl02; +} \ No newline at end of file diff --git a/project/java9-module/module-a/src/main/java/org/cpq/a/a1/HelloA1.java b/project/java9-module/module-a/src/main/java/org/cpq/a/a1/HelloA1.java new file mode 100644 index 00000000..24729437 --- /dev/null +++ b/project/java9-module/module-a/src/main/java/org/cpq/a/a1/HelloA1.java @@ -0,0 +1,9 @@ +package org.cpq.a.a1; + +public class HelloA1 { + + public void hello(){ + System.out.println("######HelloA1"); + } + +} diff --git a/project/java9-module/module-a/src/main/java/org/cpq/a/a2/HelloA2.java b/project/java9-module/module-a/src/main/java/org/cpq/a/a2/HelloA2.java new file mode 100644 index 00000000..651f39b0 --- /dev/null +++ b/project/java9-module/module-a/src/main/java/org/cpq/a/a2/HelloA2.java @@ -0,0 +1,9 @@ +package org.cpq.a.a2; + +public class HelloA2 { + + public void hello(){ + System.out.println("HelloA2"); + } + +} diff --git a/project/java9-module/module-a/src/main/java/org/cpq/a/impl/PrintImpl01.java b/project/java9-module/module-a/src/main/java/org/cpq/a/impl/PrintImpl01.java new file mode 100644 index 00000000..ec92e1a3 --- /dev/null +++ b/project/java9-module/module-a/src/main/java/org/cpq/a/impl/PrintImpl01.java @@ -0,0 +1,13 @@ +package org.cpq.a.impl; + +import org.cpq.a.service.Print; + +public class PrintImpl01 implements Print { + + // 第一个实现类默认的构造函数是无参构造函数 + @Override + public void printMsg() { + System.out.println("PrintImpl01"); + } + +} diff --git a/project/java9-module/module-a/src/main/java/org/cpq/a/impl/PrintImpl02.java b/project/java9-module/module-a/src/main/java/org/cpq/a/impl/PrintImpl02.java new file mode 100644 index 00000000..ebe7ac45 --- /dev/null +++ b/project/java9-module/module-a/src/main/java/org/cpq/a/impl/PrintImpl02.java @@ -0,0 +1,22 @@ +package org.cpq.a.impl; + +import org.cpq.a.service.Print; + +public class PrintImpl02 implements Print { + + private String name; + + public PrintImpl02(String name) { + this.name = name; + } + + public static Print provider() { + return new PrintImpl02("实现类没有无参构造函数,则规定使用public static Print provider()返回PrintImpl02实例"); + } + + @Override + public void printMsg() { + System.out.println("PrintImpl02 : " + this.name); + } + +} diff --git a/project/java9-module/module-a/src/main/java/org/cpq/a/reflex/Reflex01.java b/project/java9-module/module-a/src/main/java/org/cpq/a/reflex/Reflex01.java new file mode 100644 index 00000000..e17f3d0f --- /dev/null +++ b/project/java9-module/module-a/src/main/java/org/cpq/a/reflex/Reflex01.java @@ -0,0 +1,9 @@ +package org.cpq.a.reflex; + +public class Reflex01 { + + private void reflect(){ + System.out.println("Reflex01.reflect"); + } + +} diff --git a/project/java9-module/module-a/src/main/java/org/cpq/a/service/Print.java b/project/java9-module/module-a/src/main/java/org/cpq/a/service/Print.java new file mode 100644 index 00000000..598a9e22 --- /dev/null +++ b/project/java9-module/module-a/src/main/java/org/cpq/a/service/Print.java @@ -0,0 +1,7 @@ +package org.cpq.a.service; + +public interface Print { + + void printMsg(); + +} diff --git a/project/java9-module/module-b/pom.xml b/project/java9-module/module-b/pom.xml new file mode 100644 index 00000000..56632897 --- /dev/null +++ b/project/java9-module/module-b/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + org.cpq + module-b + 1.0-SNAPSHOT + jar + + module-b + http://maven.apache.org + + + 21 + 21 + 21 + UTF-8 + + + + + org.cpq + module-a + 1.0-SNAPSHOT + + + + diff --git a/project/java9-module/module-b/src/main/java/module-info.java b/project/java9-module/module-b/src/main/java/module-info.java new file mode 100644 index 00000000..99f17a60 --- /dev/null +++ b/project/java9-module/module-b/src/main/java/module-info.java @@ -0,0 +1,8 @@ +module org.cpq.b { + // requires org.cpq.a; + + // transitive 传递依赖,导入了module-b的模块,能使用module-a的模块 + requires transitive org.cpq.a; + + uses org.cpq.a.service.Print; +} \ No newline at end of file diff --git a/project/java9-module/module-b/src/main/java/org/cpq/b/MainB.java b/project/java9-module/module-b/src/main/java/org/cpq/b/MainB.java new file mode 100644 index 00000000..7e3d90f7 --- /dev/null +++ b/project/java9-module/module-b/src/main/java/org/cpq/b/MainB.java @@ -0,0 +1,31 @@ +package org.cpq.b; + +import org.cpq.a.service.Print; + +import java.util.ServiceLoader; + +/** + * Hello world! + * + */ +public class MainB +{ + public static void main( String[] args ) throws Exception + { + // 由于 module org.cpq.a 只开放了 org.cpq.a.a1 ,所以只能使用HelloA1, + // 无法使用org.cpq.a.a2.HelloA2 + // new HelloA1().hello(); + + // Class c = Class.forName("org.cpq.a.reflex.Reflex01"); + // Object instance = c.getDeclaredConstructor().newInstance(); + // Method method = c.getDeclaredMethods()[0]; + // method.setAccessible(true); + // method.invoke(instance, null); + + ServiceLoader loads = ServiceLoader.load(Print.class); + for (Print load : loads) { + load.printMsg(); + } + + } +} diff --git a/project/java9-module/module-b/src/main/java/org/cpq/b/b1/HelloB1.java b/project/java9-module/module-b/src/main/java/org/cpq/b/b1/HelloB1.java new file mode 100644 index 00000000..865d58a5 --- /dev/null +++ b/project/java9-module/module-b/src/main/java/org/cpq/b/b1/HelloB1.java @@ -0,0 +1,9 @@ +package org.cpq.b.b1; + +public class HelloB1 { + + public void hello(){ + System.out.println("HelloBBB1"); + } + +} diff --git a/project/java9-module/module-c/pom.xml b/project/java9-module/module-c/pom.xml new file mode 100644 index 00000000..ba003ac7 --- /dev/null +++ b/project/java9-module/module-c/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + org.cpq + module-c + 1.0-SNAPSHOT + jar + + module-c + http://maven.apache.org + + + 21 + 21 + 21 + UTF-8 + + + + + + + + + + org.cpq + module-b + 1.0-SNAPSHOT + + + + diff --git a/project/java9-module/module-c/src/main/java/module-info.java b/project/java9-module/module-c/src/main/java/module-info.java new file mode 100644 index 00000000..cc4fde18 --- /dev/null +++ b/project/java9-module/module-c/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module org.cpq.c { + requires org.cpq.b; +} \ No newline at end of file diff --git a/project/java9-module/module-c/src/main/java/org/cpq/c/MainC.java b/project/java9-module/module-c/src/main/java/org/cpq/c/MainC.java new file mode 100644 index 00000000..2969fb09 --- /dev/null +++ b/project/java9-module/module-c/src/main/java/org/cpq/c/MainC.java @@ -0,0 +1,19 @@ +package org.cpq.c; + +import org.cpq.a.a1.HelloA1; +// import org.cpq.a.a2.HelloA2; + +/** + * Hello world! + * + */ +public class MainC +{ + public static void main( String[] args ) + { + // module-b使用声明了传递依赖requires transitive org.cpq.a; 在module-c也能使用module-a导出的包 + new HelloA1().hello(); + + // new HelloA2().hello(); + } +} diff --git a/spring-cloud-2/app001/.gitignore b/project/jpa/jpa-01/.gitignore similarity index 100% rename from spring-cloud-2/app001/.gitignore rename to project/jpa/jpa-01/.gitignore diff --git a/project/jpa/jpa-01/pom.xml b/project/jpa/jpa-01/pom.xml new file mode 100644 index 00000000..da683207 --- /dev/null +++ b/project/jpa/jpa-01/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.16 + + + com.cpq + jpa-01 + 0.0.1-SNAPSHOT + jpa-01 + Demo project for Spring Boot + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + mysql + mysql-connector-java + 5.1.46 + runtime + + + + org.projectlombok + lombok + provided + + + + + jpa-01 + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/Jpa01Application.java b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/Jpa01Application.java new file mode 100644 index 00000000..fd52c596 --- /dev/null +++ b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/Jpa01Application.java @@ -0,0 +1,13 @@ +package com.cpq.jpa01; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Jpa01Application { + + public static void main(String[] args) { + SpringApplication.run(Jpa01Application.class, args); + } + +} diff --git a/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/ThreadPoolConfig.java b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/ThreadPoolConfig.java new file mode 100644 index 00000000..d644ea8a --- /dev/null +++ b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/ThreadPoolConfig.java @@ -0,0 +1,86 @@ +package com.cpq.jpa01; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadPoolExecutor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + *@ClassName: ThreadPoolConfig + * @author v-xuk19 + * + */ +@Configuration +@EnableAsync +public class ThreadPoolConfig { + + @Bean + public TaskExecutor taskExecutor() { + //ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + MdcThreadPoolTaskExecutor executor = new MdcThreadPoolTaskExecutor(); + // 设置核心线程数 + executor.setCorePoolSize(8); + // 设置最大线程数 + executor.setMaxPoolSize(100); + // 设置队列容量 + executor.setQueueCapacity(30); + // 设置线程活跃时间(秒) + executor.setKeepAliveSeconds(300); + // 设置默认线程名称 + executor.setThreadNamePrefix("thread-obs"); + // 设置拒绝策略rejection-policy:当pool已经达到max size的时候,如何处理新任务 CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.initialize(); + return executor; + } + + /** + * 异步线程池submit时,传递父线程的trackId + * + */ + @Slf4j + public static class MdcThreadPoolTaskExecutor extends ThreadPoolTaskExecutor{ + + @Override + public Future submit(Callable task) { + String trackIdKey = "mdc_key"; + Map context = MDC.getCopyOfContextMap(); + // 将RequestAttributes对象设置为子线程共享 + ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + RequestContextHolder.setRequestAttributes(sra, true); + return super.submit(() -> { + // 将父线程的MDC内容传给子线程 + T result = null; + if (context != null && !StringUtils.isEmpty(context.get(trackIdKey))) { + MDC.setContextMap(context); + } else { + MDC.put(trackIdKey, UUID.randomUUID().toString().replace("-", "")); + } + try { + result = task.call(); + } finally { + try { + MDC.clear(); + } catch (Exception e2) { + log.warn("mdc clear exception.", e2); + } + } + return result; + }); + } + + } + + +} diff --git a/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/controller/Test01Controller.java b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/controller/Test01Controller.java new file mode 100644 index 00000000..fbc9448c --- /dev/null +++ b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/controller/Test01Controller.java @@ -0,0 +1,45 @@ +package com.cpq.jpa01.controller; + +import com.cpq.jpa01.dao.Test01Dao; +import com.cpq.jpa01.model.Test01; +import com.cpq.jpa01.service.Test01Service; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.task.TaskExecutor; +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; + +/** + * @author chenpq05 + * @since 2023/12/4 13:49 + */ +@Slf4j +@RestController +@RequestMapping("/test01") +public class Test01Controller { + + @Autowired + private Test01Service test01Service; + @Autowired + private Test01Dao test01Dao; + @Autowired + private TaskExecutor taskExecutor; + + + @PostMapping("/add") + public Test01 add(@RequestBody Test01 saveDTO) { + Test01 dbSave = test01Service.save(saveDTO); + return dbSave; + } + + @PostMapping("/test") + public Object test(@RequestBody Test01 test01) { + taskExecutor.execute(() -> { + test01Service.test(test01); + }); + return "page"; + } + +} diff --git a/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/dao/Test01Dao.java b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/dao/Test01Dao.java new file mode 100644 index 00000000..5de53d62 --- /dev/null +++ b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/dao/Test01Dao.java @@ -0,0 +1,28 @@ +package com.cpq.jpa01.dao; + +import com.cpq.jpa01.model.Test01; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * @author chenpq05 + * @since 2023/12/4 13:44 + */ +public interface Test01Dao extends JpaRepository { + + + + Logger LOGGER = LoggerFactory.getLogger(Test01Dao.class); + + List findByCompanyCodeInAndCompanyNameLike(List companyCode, String companyName); + + default int getMy() { + Test01 dbOne = this.getById(1L); + LOGGER.info("^^^^^^^^^^^^^^^^^^^^^^{}", dbOne); + return 1; + } + +} diff --git a/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/model/Test01.java b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/model/Test01.java new file mode 100644 index 00000000..9ec9cd4e --- /dev/null +++ b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/model/Test01.java @@ -0,0 +1,55 @@ +package com.cpq.jpa01.model; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +/** + *

+ * 业务-公司 + *

+ * + * @author chenpq05 + * @since 2023-05-31 15:57:44 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@Entity +@DynamicUpdate +@DynamicInsert +public class Test01 implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String companyCode; + + private String companyName; + + private Integer peopleNumber; + + private Double area; + + private LocalDateTime createDate; + + private Integer isDel; + + private BigDecimal actualCapital; + +} diff --git a/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/service/Test01Service.java b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/service/Test01Service.java new file mode 100644 index 00000000..60067141 --- /dev/null +++ b/project/jpa/jpa-01/src/main/java/com/cpq/jpa01/service/Test01Service.java @@ -0,0 +1,63 @@ +package com.cpq.jpa01.service; + +import com.cpq.jpa01.dao.Test01Dao; +import com.cpq.jpa01.model.Test01; +import java.util.Arrays; +import java.util.List; +import javax.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.stereotype.Service; + +/** + *

+ * 业务-公司 + *

+ * + * @author chenpq05 + * @since 2023-05-31 15:57:44 + */ +@Service +@Slf4j +public class Test01Service { + @Autowired + private Test01Dao test01Dao; + + @Transactional(rollbackOn = Exception.class) + public Test01 save(Test01 test01) { + Test01 dbSave = test01Dao.save(test01); + log.info("######{}", dbSave); + return dbSave; + } + + @Transactional + public void test(Test01 test01) { + int my = test01Dao.getMy(); + log.info("##############{}", my); + + List all = test01Dao.findAll(); + log.info("#############all={}", all); + + List list = test01Dao.findByCompanyCodeInAndCompanyNameLike( + Arrays.asList("7112336ssfdf3334354545", "711233633646dfs3474688"), test01.getCompanyName()); + log.info("#############list={}", list); + + Test01 db = test01Dao.getById(test01.getId()); + + test01Dao.save(test01); + log.info("#############{}", test01); + + // page从0开始,跟limit一样 + PageRequest pageRequest = PageRequest.of(1, 2, Sort.by(Direction.DESC, "id")); + Example test01Example = Example.of(test01); + Page page = test01Dao.findAll(test01Example, pageRequest); + + + } + +} diff --git a/project/jpa/jpa-01/src/main/resources/application.properties b/project/jpa/jpa-01/src/main/resources/application.properties new file mode 100644 index 00000000..646ffdc2 --- /dev/null +++ b/project/jpa/jpa-01/src/main/resources/application.properties @@ -0,0 +1,11 @@ +spring.application.name=jpa-01 +server.port=9999 + +spring.datasource.url= jdbc:mysql://10.39.174.41:3306/cpq_01?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false +spring.datasource.username=root +spring.datasource.password=123456 +spring.datasource.driver-class-name = com.mysql.jdbc.Driver + +#显示 sql 语句 +spring.jpa.show-sql=true +#spring.jpa.open-in-view=true diff --git a/project/jvm/pom.xml b/project/jvm/pom.xml new file mode 100644 index 00000000..43e05763 --- /dev/null +++ b/project/jvm/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + org.cpqjvm + jvm + 1.0-SNAPSHOT + jar + + jvm + http://maven.apache.org + + + UTF-8 + + + + + junit + junit + 3.8.1 + test + + + diff --git a/project/jvm/src/main/java/org/cpqjvm/g1/Demo01.java b/project/jvm/src/main/java/org/cpqjvm/g1/Demo01.java new file mode 100644 index 00000000..d1df1fad --- /dev/null +++ b/project/jvm/src/main/java/org/cpqjvm/g1/Demo01.java @@ -0,0 +1,111 @@ +package org.cpqjvm.g1; + +public class Demo01 { + + /** + * G1最大的特征:将堆分为若干小区域,每个小区域称为region + * 如图片:G1垃圾收集与传统垃圾收集对比.jpg + * + * G1对象管理: + * 新生代对象分配在新生代Eden区 + * 运行一段时间后启动YoungGC,将Eden区中的对象复制到Survivor区 + * 继续运行一段时间后Eden区再次被填满,Eden区和Survivor区对象复制到一个新的Survivor区 + * 如果老的Survivor区的对象复制次数超过规定值,直接将对象复制到Old区 + * 如图片:G1对象管理过程.jpg + * 如果一个对象大于等于分区的一半,则直接将对象放到H区(Humongous区) + * Eden区、Survivor区、Old区、H区的内存地址是动态变化的,一个region为Eden区,Eden区清空后,后面可能会变成Survivor区、Old区、H区 + * + */ + + /** + * 配置VM参数再运行 -Xmx128M -XX:+UseG1GC -Xlog:gc* + * 最后输出 region size 1024K,说明G1GC默认的region大小为1024K + */ + // public static void main(String[] args) { + // byte[] data = new byte[1024 * 256]; + // for (int i = 0; i < 1000; i++) { + // data = new byte[1024 * 256]; + // } + // + // } + + // /** + // * region的个数和大小都是动态可变的 + // * G1默认region最大个数为2048个,region大小的取值范围是2的指数倍:1、2、4、8、16、32M + // * region最大值 = max( (Xmx + Xms) 除以2再除以2048 , 1MB) + // * 配置-Xmx4096M -Xms4096M,通过公式得出region size为2048K + // * 配置VM参数再运行main方法 -Xmx4096M -Xms4096M -XX:+UseG1GC -Xlog:gc* + // * 最后输出 region size 2048K + // * + // * 在启动时设置region的大小会影响JVM调优, + // * 不建议手动指定region的大小,而应该通过调整Xmx和Xms让JVM自动调整region的大小 + // * + // * G1只有YoungGC、MixedGC和FullGC + // * YoungGC:只回收新生代区域,代价低/频率高 + // * MixGC:回收新生代 + 部分老年代,频率一般 + // * FullGC:回收堆空间,代价高/频率低 + // */ + // public static void main(String[] args) { + // byte[] data = new byte[1024 * 256]; + // for (int i = 0; i < 1000; i++) { + // data = new byte[1024 * 256]; + // } + // + // } + + /** + * YGC的基本过程: + * 1、从GC roots出发标记存活对象。GC roots有哪些:栈对象、静态变量、运行时常量池、程序计数器、锁 + * 2、复制对象到S区,复制过程是最慢的 + * 3、释放垃圾集合,回收region + * 4、动态调整(增加或减少)新生代region数量 + * 4、判断是否开启并发标记,为下一步执行混合回收做准备。如果堆空间使用率超过45%则会进行混合回收 + * + * Eden区、Survivor区采用标记-复制算法。标记和复制过程是同时进行的,找到一个存活对象就复制一个 + * + * -XX:MaxGCPauseMillis 用于配置GC停顿时间 + * -Xmx128M -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xlog:gc* + */ + // public static void main(String[] args) { + // byte[] data = new byte[1024 * 256]; + // for (int i = 0; i < 100; i++) { + // data = new byte[1024 * 256]; + // } + // + // } + + /** + * 默认情况下,YGC之后,已分配内存超过内存总容量的45%会触发混合回收MixGC + * 混合回收基本步骤: + * 第一步:初始标记阶段。标记所有由GCRoot等直接引用的对象,会暂停用户程序的运行 + * 第二步:并发标记。标记上一步中标记的所有引用对象,执行时间略长,不会暂停用户程序的运行,不会STW + * 第三步:再标记阶段。标记出上一个阶段没有被标记的对象,执行速度非常快,会STW + * 第四步:存活对象计数阶段。统计出每个region存活对象的数量 + * 第五步:垃圾回收阶段。选择回收价值高的区域,把存活对象复制到新分区,然后回收老分区 + * + * 图片:G1混合回收完整过程.jpg + * + * YGC是MixedGC的前奏,YGC完成,就代表mixedGC已经走完了初始标记阶段,YGC已经帮MixedGC完成了初始化的活 + */ + // 触发混合回收 + // public static List list = new ArrayList<>(); + // public static void main(String[] args) throws Exception{ + // while (true) { + // byte[] data = new byte[1024 * 256]; + // for (int i = 0; i < 50; i++) { + // data = new byte[1024 * 256]; + // list.add(data); + // } + // TimeUnit.SECONDS.sleep(1L); + // } + // } + + /** + * 老年代的空间已经不足以放下新对象,会触发FullGC + * FullGC可能执行两次,第二次是回收软引用,如果堆空间还是不足以存放新对象,会触发OOM + * YGC、MixedGC采用标记-复制算法。 + * FullGC采用标记-压缩算法,FullGC是没有堆空间可用了才触发的,因为没有可用的region了,就不能采用复制算法了 + */ + + +} diff --git "a/project/jvm/src/main/java/org/cpqjvm/g1/G1\345\236\203\345\234\276\346\224\266\351\233\206\344\270\216\344\274\240\347\273\237\345\236\203\345\234\276\346\224\266\351\233\206\345\257\271\346\257\224.jpg" "b/project/jvm/src/main/java/org/cpqjvm/g1/G1\345\236\203\345\234\276\346\224\266\351\233\206\344\270\216\344\274\240\347\273\237\345\236\203\345\234\276\346\224\266\351\233\206\345\257\271\346\257\224.jpg" new file mode 100644 index 00000000..81e621df Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/g1/G1\345\236\203\345\234\276\346\224\266\351\233\206\344\270\216\344\274\240\347\273\237\345\236\203\345\234\276\346\224\266\351\233\206\345\257\271\346\257\224.jpg" differ diff --git "a/project/jvm/src/main/java/org/cpqjvm/g1/G1\345\257\271\350\261\241\347\256\241\347\220\206\350\277\207\347\250\213.jpg" "b/project/jvm/src/main/java/org/cpqjvm/g1/G1\345\257\271\350\261\241\347\256\241\347\220\206\350\277\207\347\250\213.jpg" new file mode 100644 index 00000000..42210754 Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/g1/G1\345\257\271\350\261\241\347\256\241\347\220\206\350\277\207\347\250\213.jpg" differ diff --git "a/project/jvm/src/main/java/org/cpqjvm/g1/G1\346\267\267\345\220\210\345\233\236\346\224\266\345\256\214\346\225\264\350\277\207\347\250\213.jpg" "b/project/jvm/src/main/java/org/cpqjvm/g1/G1\346\267\267\345\220\210\345\233\236\346\224\266\345\256\214\346\225\264\350\277\207\347\250\213.jpg" new file mode 100644 index 00000000..50ee4303 Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/g1/G1\346\267\267\345\220\210\345\233\236\346\224\266\345\256\214\346\225\264\350\277\207\347\250\213.jpg" differ diff --git a/project/jvm/src/main/java/org/cpqjvm/zgc/ZGC.java b/project/jvm/src/main/java/org/cpqjvm/zgc/ZGC.java new file mode 100644 index 00000000..fa479c55 --- /dev/null +++ b/project/jvm/src/main/java/org/cpqjvm/zgc/ZGC.java @@ -0,0 +1,42 @@ +package org.cpqjvm.zgc; + +public class ZGC { + + /** + * ZGC的特征: + * 1、停顿时间(STW)不超过10ms,JDK16不超过1ms,且不会随着堆的大小增加而增加 + * 2、理论上最大支持16TB的大堆,最小支持8MB的小堆 + * 3、跟G1相比,对应用程序的吞吐量的影响小于15% + * + * ZGC的堆内存采用分页模型,分为:小页面、中页面、大页面。图片:ZGC堆空间分页模型.jpg + * Linux2.6引入了标准大页,默认是2M,所以ZGC队内存的小页面默认是2M + * + * ZGC的指针着色技术 + * ZGC只支持64位系统,使用低42表示使用的堆空间地址,借高几位来做GC相关的事情 + * 图片:指针着色.jpg,42~45位使用不同的颜色表示Java对象在GC时的状态 + * 图片:指针着色2使用44表示对象地址.jpg,45~48位使用不同的颜色表示Java对象在GC时的状态 + * + * ZGC分为标记阶段、转移阶段。 + * 图片:ZGC的过程.jpg + * 图片:标记前的状态.jpg。新创建的对象使用蓝色指针表示。 + * 1、初始标记会STW,只会标记GCRoot,停顿时间非常短 + * 2、并发标记,从GCRoot开始,找打被引用的对象,此阶段与业务线程同时运行 + * 图片:初始标记和再标记.jpg。本次GC,将蓝色的被引用的对象标记为绿色 + * 下一次垃圾回收的并发标记阶段,将绿色的对象标记为红色 + * 3、再标记阶段,会STW,标记漏标的对象 + * 重新标记并发标记阶段产生的新对象,会STW + * 4、并发转移准备。计算region的垃圾占比,以便确定回收哪个region收益最高 + * 5、初始转移。 + * 图片:初始转移.jpg。把GCRoot转移走,例如把图中的A转移到新region + * 初始转移后,绿色的A对象指针会还原为蓝色 + * 6、并发转移 + * 图片:并发转移时候使用转发表.jpg。 + * GCRoot关联的对象,在并发转移阶段迁移,迁移过程中会将对象的老地址、新地址记录到转发表中。 + * 在转发表里的对象是程序通过转发表里面的新地址,就可以找到对象了。 + * 下一次垃圾回收的并发标记阶段,发现指针式绿色,则将把对象地址设置为转发表的新地址。并删除转发表中的记录 + * + * + * 图片:标记阶段.jpg + */ + +} diff --git "a/project/jvm/src/main/java/org/cpqjvm/zgc/ZGC\345\240\206\347\251\272\351\227\264\345\210\206\351\241\265\346\250\241\345\236\213.jpg" "b/project/jvm/src/main/java/org/cpqjvm/zgc/ZGC\345\240\206\347\251\272\351\227\264\345\210\206\351\241\265\346\250\241\345\236\213.jpg" new file mode 100644 index 00000000..9ad08882 Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/zgc/ZGC\345\240\206\347\251\272\351\227\264\345\210\206\351\241\265\346\250\241\345\236\213.jpg" differ diff --git "a/project/jvm/src/main/java/org/cpqjvm/zgc/ZGC\347\232\204\350\277\207\347\250\213.jpg" "b/project/jvm/src/main/java/org/cpqjvm/zgc/ZGC\347\232\204\350\277\207\347\250\213.jpg" new file mode 100644 index 00000000..1644d681 Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/zgc/ZGC\347\232\204\350\277\207\347\250\213.jpg" differ diff --git "a/project/jvm/src/main/java/org/cpqjvm/zgc/\345\210\235\345\247\213\346\240\207\350\256\260\345\222\214\345\206\215\346\240\207\350\256\260.jpg" "b/project/jvm/src/main/java/org/cpqjvm/zgc/\345\210\235\345\247\213\346\240\207\350\256\260\345\222\214\345\206\215\346\240\207\350\256\260.jpg" new file mode 100644 index 00000000..f0f5aa78 Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/zgc/\345\210\235\345\247\213\346\240\207\350\256\260\345\222\214\345\206\215\346\240\207\350\256\260.jpg" differ diff --git "a/project/jvm/src/main/java/org/cpqjvm/zgc/\345\210\235\345\247\213\350\275\254\347\247\273.jpg" "b/project/jvm/src/main/java/org/cpqjvm/zgc/\345\210\235\345\247\213\350\275\254\347\247\273.jpg" new file mode 100644 index 00000000..272be951 Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/zgc/\345\210\235\345\247\213\350\275\254\347\247\273.jpg" differ diff --git "a/project/jvm/src/main/java/org/cpqjvm/zgc/\345\271\266\345\217\221\350\275\254\347\247\273\346\227\266\345\200\231\344\275\277\347\224\250\350\275\254\345\217\221\350\241\250.jpg" "b/project/jvm/src/main/java/org/cpqjvm/zgc/\345\271\266\345\217\221\350\275\254\347\247\273\346\227\266\345\200\231\344\275\277\347\224\250\350\275\254\345\217\221\350\241\250.jpg" new file mode 100644 index 00000000..54d2c35a Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/zgc/\345\271\266\345\217\221\350\275\254\347\247\273\346\227\266\345\200\231\344\275\277\347\224\250\350\275\254\345\217\221\350\241\250.jpg" differ diff --git "a/project/jvm/src/main/java/org/cpqjvm/zgc/\346\214\207\351\222\210\347\235\200\350\211\262.jpg" "b/project/jvm/src/main/java/org/cpqjvm/zgc/\346\214\207\351\222\210\347\235\200\350\211\262.jpg" new file mode 100644 index 00000000..a318eba7 Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/zgc/\346\214\207\351\222\210\347\235\200\350\211\262.jpg" differ diff --git "a/project/jvm/src/main/java/org/cpqjvm/zgc/\346\214\207\351\222\210\347\235\200\350\211\2622\344\275\277\347\224\25044\350\241\250\347\244\272\345\257\271\350\261\241\345\234\260\345\235\200.jpg" "b/project/jvm/src/main/java/org/cpqjvm/zgc/\346\214\207\351\222\210\347\235\200\350\211\2622\344\275\277\347\224\25044\350\241\250\347\244\272\345\257\271\350\261\241\345\234\260\345\235\200.jpg" new file mode 100644 index 00000000..f8c4171b Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/zgc/\346\214\207\351\222\210\347\235\200\350\211\2622\344\275\277\347\224\25044\350\241\250\347\244\272\345\257\271\350\261\241\345\234\260\345\235\200.jpg" differ diff --git "a/project/jvm/src/main/java/org/cpqjvm/zgc/\346\240\207\350\256\260\345\211\215\347\232\204\347\212\266\346\200\201.jpg" "b/project/jvm/src/main/java/org/cpqjvm/zgc/\346\240\207\350\256\260\345\211\215\347\232\204\347\212\266\346\200\201.jpg" new file mode 100644 index 00000000..5b335536 Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/zgc/\346\240\207\350\256\260\345\211\215\347\232\204\347\212\266\346\200\201.jpg" differ diff --git "a/project/jvm/src/main/java/org/cpqjvm/zgc/\346\240\207\350\256\260\351\230\266\346\256\265.jpg" "b/project/jvm/src/main/java/org/cpqjvm/zgc/\346\240\207\350\256\260\351\230\266\346\256\265.jpg" new file mode 100644 index 00000000..96bc4689 Binary files /dev/null and "b/project/jvm/src/main/java/org/cpqjvm/zgc/\346\240\207\350\256\260\351\230\266\346\256\265.jpg" differ diff --git a/project/kafka/README.md b/project/kafka/README.md new file mode 100644 index 00000000..bc461976 --- /dev/null +++ b/project/kafka/README.md @@ -0,0 +1,23 @@ +下载kafka_2.13-3.6.1 +https://downloads.apache.org/kafka/3.6.1/kafka_2.13-3.6.1.tgz + +kafka_2.13-3.6.1\config\zookeeper.properties 修改 +maxClientCnxns=10000 + +kafka_2.13-3.6.1\config\server.properties 修改 +listeners=PLAINTEXT://:9092 +zookeeper.connect=127.0.0.1:2181 + +新建 kafka_2.13-3.6.1\zookerper-start.bat +.\bin\windows\zookeeper-server-start.bat .\config\zookeeper.properties + +新建 kafka_2.13-3.6.1\kafka-start.bat +.\bin\windows\kafka-server-start.bat .\config\server.properties + +启动zookerper-start.bat、kafka-start.bat + +进入:kafka_2.13-3.6.1\bin\windows> +创建topic +.\kafka-topics.bat --bootstrap-server 127.0.0.1:9092 --create --partitions 1 --replication-factor 1 --topic first + + diff --git a/project/kafka/kafka01/.gitignore b/project/kafka/kafka01/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/project/kafka/kafka01/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/project/kafka/kafka01/pom.xml b/project/kafka/kafka01/pom.xml new file mode 100644 index 00000000..de46cb9d --- /dev/null +++ b/project/kafka/kafka01/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.0 + + + com.example + kafka01 + 0.0.1-SNAPSHOT + kafka01 + Demo project for Spring Boot + + 1.8 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.kafka + spring-kafka + + + org.apache.kafka + kafka-streams + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.kafka + spring-kafka-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/Consumer01.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/Consumer01.java new file mode 100644 index 00000000..36b366fd --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/Consumer01.java @@ -0,0 +1,14 @@ +package com.example.kafka01; + +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +public class Consumer01 { + + @KafkaListener(topics = "test01") + public void consumerTopic(String msg){ + System.out.println("收到消息:" + msg); + } + +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/Kafka01Application.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/Kafka01Application.java new file mode 100644 index 00000000..98240492 --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/Kafka01Application.java @@ -0,0 +1,13 @@ +package com.example.kafka01; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Kafka01Application { + + public static void main(String[] args) { + SpringApplication.run(Kafka01Application.class, args); + } + +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/Producer01.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/Producer01.java new file mode 100644 index 00000000..63a80352 --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/Producer01.java @@ -0,0 +1,23 @@ +package com.example.kafka01; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class Producer01 { + + + @Autowired + KafkaTemplate kafka; + + @RequestMapping("/send01") + public String data(@RequestParam("msg") String msg){ + // 通过kafka发送出去 + kafka.send("test01", msg); + return "ok"; + } + +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/a_simple/SimpleConsumer.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/a_simple/SimpleConsumer.java new file mode 100644 index 00000000..0338b333 --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/a_simple/SimpleConsumer.java @@ -0,0 +1,55 @@ +package com.example.kafka01.a_simple; + +import java.util.Arrays; +import java.util.Properties; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author chenpq05 + * @since 2024/2/2 14:00 + */ +public class SimpleConsumer { + + public static void main(String[] args) throws Exception { + + // 设置日志级别 + Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + root.setLevel(Level.INFO); + + String topicName = "first"; + Properties props = new Properties(); + + props.put("bootstrap.servers", "localhost:9092"); + props.put("group.id", "test"); + props.put("enable.auto.commit", "true"); + props.put("auto.commit.interval.ms", "1000"); + props.put("session.timeout.ms", "30000"); + props.put("key.deserializer", + "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("value.deserializer", + "org.apache.kafka.common.serialization.StringDeserializer"); + KafkaConsumer consumer = new KafkaConsumer(props); + // Kafka Consumer subscribes list of topics here. + consumer.subscribe(Arrays.asList(topicName)); + + // print the topic name + System.out.println("Subscribed to topic " + topicName); + int i = 0; + + while (true) { + ConsumerRecords records = consumer.poll(100); + for (ConsumerRecord record : records) { + // print the offset,key and value for the consumer records. + System.out.printf("offset = %d, key = %s, value = %s\n", + record.offset(), record.key(), record.value()); + } + } + } + +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/a_simple/SimpleGroupConsumer.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/a_simple/SimpleGroupConsumer.java new file mode 100644 index 00000000..1b7b16e6 --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/a_simple/SimpleGroupConsumer.java @@ -0,0 +1,42 @@ +package com.example.kafka01.a_simple; + +import java.util.Arrays; +import java.util.Properties; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; + +/** + * @author chenpq05 + * @since 2024/2/2 14:00 + */ +public class SimpleGroupConsumer { + + public static void main(String[] args) throws Exception { + String topic = "first-p"; + String group = "my-group"; + Properties props = new Properties(); + props.put("bootstrap.servers", "localhost:9092"); + props.put("group.id", group); + props.put("enable.auto.commit", "true"); + props.put("auto.commit.interval.ms", "1000"); + props.put("session.timeout.ms", "30000"); + props.put("key.deserializer", + "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("value.deserializer", + "org.apache.kafka.common.serialization.StringDeserializer"); + KafkaConsumer consumer = new KafkaConsumer(props); + + consumer.subscribe(Arrays.asList(topic)); + System.out.println("Subscribed to topic " + topic); + int i = 0; + + while (true) { + ConsumerRecords records = consumer.poll(100); + for (ConsumerRecord record : records) + System.out.printf("offset = %d, key = %s, value = %s\n", + record.offset(), record.key(), record.value()); + } + } + +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/a_simple/SimpleProducer.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/a_simple/SimpleProducer.java new file mode 100644 index 00000000..36e46059 --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/a_simple/SimpleProducer.java @@ -0,0 +1,54 @@ +package com.example.kafka01.a_simple; + +import java.util.Properties; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerRecord; + +/** + * @author chenpq05 + * @since 2024/2/1 16:31 + */ +public class SimpleProducer { + + public static void main(String[] args) { + // create instance for properties to access producer configs + Properties props = new Properties(); + + //Assign localhost id + props.put("bootstrap.servers", "localhost:9092"); + + //Set acknowledgements for producer requests. + props.put("acks", "all"); + + //If the request fails, the producer can automatically retry, + props.put("retries", 0); + + //Specify buffer size in config + props.put("batch.size", 16384); + + //Reduce the no of requests less than 0 + props.put("linger.ms", 1); + + //The buffer.memory controls the total amount of memory available to the producer for buffering. + props.put("buffer.memory", 33554432); + + props.put("key.serializer", + "org.apache.kafka.common.serialization.StringSerializer"); + + props.put("value.serializer", + "org.apache.kafka.common.serialization.StringSerializer"); + + Producer producer = new KafkaProducer + (props); + + for(int i = 0; i < 10; i++) { + producer.send(new ProducerRecord("first", Integer.toString(i), Integer.toString(i))); + } + + producer.flush(); + System.out.println("Message sent successfully"); + producer.close(); + } + +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderCommitSync02OffsetConsumer.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderCommitSync02OffsetConsumer.java new file mode 100644 index 00000000..405964a8 --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderCommitSync02OffsetConsumer.java @@ -0,0 +1,53 @@ +package com.example.kafka01.b_commit; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.TopicPartition; +import org.slf4j.LoggerFactory; + +/** + * @author chenpq05 + * @since 2024/2/19 13:53 + */ +public class OrderCommitSync02OffsetConsumer { + public static void main(String[] args) { + // 设置日志级别 + Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + root.setLevel(Level.INFO); + + Properties props = new Properties(); + props.put("bootstrap.servers", "127.0.0.1:9092"); + props.put("group.id", "test"); + //关闭自动提交确认选项 + props.put("enable.auto.commit", "false"); + props.put("key.deserializer", + "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("value.deserializer", + "org.apache.kafka.common.serialization.StringDeserializer"); + KafkaConsumer consumer = new KafkaConsumer<>(props); + consumer.subscribe(Arrays.asList("order-p")); + try { + while(true) { + ConsumerRecords records = consumer.poll(100); + for (TopicPartition partition : records.partitions()) { + List> partitionRecords = records.records(partition); + for (ConsumerRecord record : partitionRecords) { + System.out.println("消费:" + record.offset() + " : " + record.value()); + } + long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset(); + consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1))); + } + } + } finally { + consumer.close(); + } + } +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderCommitSyncConsumer.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderCommitSyncConsumer.java new file mode 100644 index 00000000..0c1ec72b --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderCommitSyncConsumer.java @@ -0,0 +1,50 @@ +package com.example.kafka01.b_commit; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.slf4j.LoggerFactory; + +/** + * @author chenpq05 + * @since 2024/2/19 13:53 + */ +public class OrderCommitSyncConsumer { + public static void main(String[] args) { + // 设置日志级别 + Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + root.setLevel(Level.INFO); + + Properties props = new Properties(); + props.put("bootstrap.servers", "127.0.0.1:9092"); + props.put("group.id", "test"); +//关闭自动提交确认选项 + props.put("enable.auto.commit", "false"); + props.put("key.deserializer", + "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("value.deserializer", + "org.apache.kafka.common.serialization.StringDeserializer"); + KafkaConsumer consumer = new KafkaConsumer<>(props); + consumer.subscribe(Arrays.asList("order")); + final int minBatchSize = 6; + List> buffer = new ArrayList<>(); + while (true) { + ConsumerRecords records = consumer.poll(100); + for (ConsumerRecord record : records) { + buffer.add(record); + } + if (buffer.size() >= minBatchSize) { + buffer.forEach(e -> System.out.println("消费:"+e.value())); + // 手动提交offset值 + consumer.commitSync(); + buffer.clear(); + } + } + } +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderConsumer.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderConsumer.java new file mode 100644 index 00000000..c0929bd0 --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderConsumer.java @@ -0,0 +1,41 @@ +package com.example.kafka01.b_commit; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import java.util.Arrays; +import java.util.Properties; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.slf4j.LoggerFactory; + +/** + * @author chenpq05 + * @since 2024/2/19 13:53 + */ +public class OrderConsumer { + public static void main(String[] args) { + // 设置日志级别 + Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + root.setLevel(Level.INFO); + // 1、连接集群 + Properties props = new Properties(); + props.put("bootstrap.servers", "127.0.0.1:9092"); + props.put("group.id", "test2"); + //以下两行代码 ---消费者自动提交offset值 + props.put("enable.auto.commit", "true"); + props.put("auto.commit.interval.ms", "1000"); + props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + KafkaConsumer kafkaConsumer = new KafkaConsumer<>(props); + // 2、发送数据 发送数据需要,订阅下要消费的topic:order + kafkaConsumer.subscribe(Arrays.asList("order-p")); + while (true) { + // jdk queue offer插入、poll获取元素。 blockingqueue put插入原生, take获取元素 + ConsumerRecords consumerRecords = kafkaConsumer.poll(100); + for (ConsumerRecord record : consumerRecords) { + System.out.println("消费的数据为:" + record.value()); + } + } + } +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderPartitionConsumer.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderPartitionConsumer.java new file mode 100644 index 00000000..6345ca35 --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderPartitionConsumer.java @@ -0,0 +1,51 @@ +package com.example.kafka01.b_commit; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import java.util.Arrays; +import java.util.Properties; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.TopicPartition; +import org.slf4j.LoggerFactory; + +/** + * @author chenpq05 + * @since 2024/2/19 13:53 + */ +public class OrderPartitionConsumer { + public static void main(String[] args) { + // 设置日志级别 + Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + root.setLevel(Level.INFO); + + Properties props = new Properties(); + props.put("bootstrap.servers", "localhost:9092"); + props.put("group.id", "test"); + props.put("enable.auto.commit", "true"); + props.put("auto.commit.interval.ms", "1000"); + props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + KafkaConsumer consumer = new KafkaConsumer<>(props); + + + //手动指定消费指定分区的数据---start + String topic = "order"; + TopicPartition partition0 = new TopicPartition(topic, 0); + TopicPartition partition1 = new TopicPartition(topic, 1); + consumer.assign(Arrays.asList(partition0, partition1)); + + //要使用此模式,只需使用要使用的分区的完整列表调用assign(Collection),而不是使用subscribe订阅主题。 + //主题与分区订阅只能二选一。 + //consumer.subscribe(Arrays.asList("order")); + + //手动指定消费指定分区的数据---end + while (true) { + ConsumerRecords records = consumer.poll(100); + for (ConsumerRecord record : records) { + System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value()); + } + } + } +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderProducer.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderProducer.java new file mode 100644 index 00000000..eca0ecec --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/b_commit/OrderProducer.java @@ -0,0 +1,61 @@ +package com.example.kafka01.b_commit; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import java.util.Properties; +import java.util.concurrent.Future; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.clients.producer.RecordMetadata; +import org.slf4j.LoggerFactory; + +/** + * @author chenpq05 + * @since 2024/2/19 13:42 + */ +public class OrderProducer { + public static void main(String[] args) throws InterruptedException { + // 设置日志级别 + Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + root.setLevel(Level.INFO); + /* 1、连接集群,通过配置文件的方式 + * 2、发送数据-topic:order,value + */ + Properties props = new Properties(); + props.put("bootstrap.servers", "127.0.0.1:9092"); + props.put("acks", "all"); + props.put("retries", 0); + props.put("batch.size", 16384); + props.put("linger.ms", 1); + props.put("buffer.memory", 33554432); + props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + KafkaProducer kafkaProducer = new KafkaProducer<>(props); + for (int i = 0; i < 30; i++) { + + // 发送数据,需要一个producerRecord对象,最少参数 String topic, V value + ProducerRecord record = new ProducerRecord<>("order-p", "订单信息!" + i); + Future future = kafkaProducer.send(record); + //future.get().pa + + ////第一种分区策略,如果既没有指定分区号,也没有指定数据key,那么就会使用轮询的方式将数据均匀的发送到不同的分区里面去 + //ProducerRecord producerRecord1 = new ProducerRecord<>("mypartition", "mymessage" + i); + //kafkaProducer.send(producerRecord1); + // + ////第二种分区策略 如果没有指定分区号,指定了数据key,通过key.hashCode % numPartitions来计算数据究竟会保存在哪一个分区里面 + ////注意:如果数据key,没有变化 key.hashCode % numPartitions = 固定值 所有的数据都会写入到某一个分区里面去 + //ProducerRecord producerRecord2 = new ProducerRecord<>("mypartition", "mykey", "mymessage" + i); + //kafkaProducer.send(producerRecord2); + // + ////第三种分区策略:如果指定了分区号,那么就会将数据直接写入到对应的分区里面去 + //ProducerRecord producerRecord3 = new ProducerRecord<>("mypartition", 0, "mykey", "mymessage" + i); + // kafkaProducer.send(producerRecord3); + // + ////第四种分区策略:自定义分区策略。如果不自定义分区规则,那么会将数据使用轮询的方式均匀的发送到各个分区里面去 + //ProducerRecord producerRecord4 = new ProducerRecord<>("mypartition", "mymessage" + i); + //kafkaProducer.send(producerRecord4); + + Thread.sleep(100); + } + } +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/c_stream/StreamAPI.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/c_stream/StreamAPI.java new file mode 100644 index 00000000..b731ddc5 --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/c_stream/StreamAPI.java @@ -0,0 +1,27 @@ +package com.example.kafka01.c_stream; + +import java.util.Properties; +import org.apache.kafka.common.serialization.Serdes; +import org.apache.kafka.streams.KafkaStreams; +import org.apache.kafka.streams.StreamsBuilder; +import org.apache.kafka.streams.StreamsConfig; + +/** + * @author chenpq05 + * @since 2024/2/19 15:27 + */ +public class StreamAPI { + + public static void main(String[] args) { + Properties props = new Properties(); + props.put(StreamsConfig.APPLICATION_ID_CONFIG, "kafka-stream-test"); + props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092"); + props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass()); + props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass()); + StreamsBuilder streamsBuilder = new StreamsBuilder(); + streamsBuilder.stream("test").mapValues(line -> line.toString().toUpperCase()).to("test2"); + KafkaStreams streams = new KafkaStreams(streamsBuilder.build(), props); + streams.start(); + } + +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/d_callback/CallbackProducer.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/d_callback/CallbackProducer.java new file mode 100644 index 00000000..cba773a5 --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/d_callback/CallbackProducer.java @@ -0,0 +1,41 @@ +package com.example.kafka01.d_callback; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import java.util.Properties; +import java.util.concurrent.Future; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.clients.producer.RecordMetadata; +import org.slf4j.LoggerFactory; + +/** + * @author chenpq05 + * @since 2024/2/19 13:42 + */ +public class CallbackProducer { + public static void main(String[] args) throws InterruptedException { + // 设置日志级别 + Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + root.setLevel(Level.INFO); + /* 1、连接集群,通过配置文件的方式 + * 2、发送数据-topic:order,value + */ + Properties props = new Properties(); + props.put("bootstrap.servers", "127.0.0.1:9092"); + props.put("acks", "all"); + props.put("retries", 0); + props.put("batch.size", 16384); + props.put("linger.ms", 1); + props.put("buffer.memory", 33554432); + props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + KafkaProducer kafkaProducer = new KafkaProducer<>(props); + for (int i = 0; i < 30; i++) { + // 发送数据,需要一个producerRecord对象,最少参数 String topic, V value + ProducerRecord record = new ProducerRecord<>("order-p", "订单信息!" + i); + Future future = kafkaProducer.send(record, new DemoProducerCallback()); + Thread.sleep(100); + } + } +} diff --git a/project/kafka/kafka01/src/main/java/com/example/kafka01/d_callback/DemoProducerCallback.java b/project/kafka/kafka01/src/main/java/com/example/kafka01/d_callback/DemoProducerCallback.java new file mode 100644 index 00000000..ea2796a3 --- /dev/null +++ b/project/kafka/kafka01/src/main/java/com/example/kafka01/d_callback/DemoProducerCallback.java @@ -0,0 +1,22 @@ +package com.example.kafka01.d_callback; + +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.producer.Callback; +import org.apache.kafka.clients.producer.RecordMetadata; + +/** + * @author chenpq05 + * @since 2024/2/20 10:17 + */ +@Slf4j +public class DemoProducerCallback implements Callback { + + @Override + public void onCompletion(RecordMetadata recordMetadata, Exception e) { + log.info("发送消息回调recordMetadata={}", recordMetadata); + if (e != null) { + log.error("发送消息异常", e); + } + } + +} diff --git a/project/kafka/kafka01/src/main/resources/application.properties b/project/kafka/kafka01/src/main/resources/application.properties new file mode 100644 index 00000000..208bcc4e --- /dev/null +++ b/project/kafka/kafka01/src/main/resources/application.properties @@ -0,0 +1,16 @@ +# 连接kafka集群 +spring.kafka.bootstrap-servers=192.168.1.221:9092 + +#key value的序列化 +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer + + +# 连接kafka 集群 + +# key value 的反序列化 +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer + +# 消费者组id +spring.kafka.consumer.group-id=consumer01 \ No newline at end of file diff --git a/project/kafka/kafka01/src/test/java/com/example/kafka01/Kafka01ApplicationTests.java b/project/kafka/kafka01/src/test/java/com/example/kafka01/Kafka01ApplicationTests.java new file mode 100644 index 00000000..20da2ce1 --- /dev/null +++ b/project/kafka/kafka01/src/test/java/com/example/kafka01/Kafka01ApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.kafka01; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Kafka01ApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/project/linux/ubuntu22.md b/project/linux/ubuntu22.md new file mode 100644 index 00000000..da94d909 --- /dev/null +++ b/project/linux/ubuntu22.md @@ -0,0 +1,88 @@ +## apt是新出的工具,用于取代apt-get和apt-cache,apt 将分散在 apt-get 和 apt-cache 的基础操作统一包含了起来 + + 操作 apt apt- +同步远程仓库中的记录表 sudo apt update sudo apt-get update +将本地所有软件包更新至远程仓库最新版本 sudo apt upgrade sudo apt-get upgrade +在软件仓库中搜索某一软件包 apt search apt-cache search +查看软件包具体信息 apt show apt-cache show +安装软件包 sudo apt install sudo apt-get install +卸载软件包 sudo apt remove sudo apt-get remove + +安装vim +apt -y install vim + +## ubuntu版本 >= 17 读取/etc/netplan/*.yaml中描述的网络配置 +```yaml +# This is the network config written by 'subiquity' +network: + ethernets: + enp0s3: + # 禁用dhcp + dhcp4: false + # 静态IP地址,/24 表示网络号为24为,若使用子网掩码表示,则子网掩码是255.255.255.0 + addresses: + - 10.0.2.200/24 + routes: + - to: default + # 路由(网关) + via: 10.0.2.1 + nameservers: + # dns解析 + addresses: [114.114.114.114,8.8.8.8] + enp0s8: + dhcp4: false + addresses: + - 192.168.56.10/24 + routes: + # 双网卡只能有一个网关配置为default + - to: 192.168.56.0/24 + via: 192.168.56.1 + nameservers: + addresses: [114.114.114.114,8.8.8.8] + version: 2 +``` +执行 netplan apply 命令可以让配置直接生效。virtualbox需要重启才生效 + +## 允许root使用ssh登录 +默认root密码是随机的,即每次开机都有一个新的root密码。输入 sudo passwd 修改密码 +允许root 使用 ssh 登录 +sudo vim /etc/ssh/sshd_config +#PermitRootLogin prohibit-password +改为 +PermitRootLogin yes + +使配置生效 +sudo service ssh restart + +## 修改主机名 +vim /etc/hostname + +## 替换为阿里源 +cd /etc/apt/ +cp sources.list sources.list_bak +vim sources.list +ggdG +输入如下内容 +deb http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse +deb-src http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse +deb http://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse +deb-src http://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse +deb http://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse +deb-src http://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse +deb http://mirrors.aliyun.com/ubuntu/ jammy-proposed main restricted universe multiverse +deb-src http://mirrors.aliyun.com/ubuntu/ jammy-proposed main restricted universe multiverse +deb http://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse +deb-src http://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse + +同步远程仓库中的记录表 +apt update + +将本地所有软件包更新至远程仓库最新版本 +apt upgrade + + +## 安装常用软件,manpages-dev包含gcc,g++,和make +sudo apt -y install manpages-dev vim iputils-ping telnet + + + diff --git a/project/liquibase-test/README.md b/project/liquibase-test/README.md new file mode 100644 index 00000000..6e16cf54 --- /dev/null +++ b/project/liquibase-test/README.md @@ -0,0 +1,145 @@ +## liquibase sql脚本管理工具(类似与git--代码管理工具) + +1. [官方文档](https://docs.liquibase.com/start/home.html) +2. [脚本命令参数执行示例](https://docs.liquibase.com/commands/home.html#database-inspection-commands) +3. [文件定义格式(sql、xml等等)](https://docs.liquibase.com/concepts/changelogs/sql-format.html) +4. [官方文档--springboot使用方式](https://contribute.liquibase.com/extensions-integrations/directory/integration-docs/springboot/) +5. [源码怎么构建](https://contribute.liquibase.com/code/) + +## Changelog 文件的结构 +changelog->changeset()->changeTypes(多个操作类型,例如insert、update、delete、createTable等等) +![结构](https://gitee.com/RenZhenGongZuo/base-components/raw/master/img/liquibase/struct1.png) + +## 最佳实践 +[文档地址](https://docs.liquibase.com/concepts/bestpractices.html) + +1. 定义好版本目录 +```text +com + example + db + changelog + db.changelog-root.xml + db.changelog-1.0.xml + db.changelog-1.1.xml + db.changelog-2.0.xml + DatabasePool.java + AbstractDAO.java +``` +1. 定义团队的changeset ID 格式 我们建议您使用从 1 开始的递增数字序列 +2. 请为每个changeset增加 注释 +3. 规划回滚策略 (为了确保安全且可预测的回滚,请在生产环境中运行它们之前在开发环境中对其进行测试) +```text +1. 通过脚本执行回滚 +2. 通过测试用例执行 (自己打liquibase的扩展测试包,然后引用) + //回滚到某个时间点 + liquibase = createLiquibase(rollbackChangeLog); + liquibase.update(this.contexts); + + liquibase = createLiquibase(rollbackChangeLog); + liquibase.rollback(new Date(0), this.contexts); + //生成回滚的sql + liquibase = createLiquibase(rollbackChangeLog); + liquibase.futureRollbackSQL(new Contexts(this.contexts), new LabelExpression(), writer); + //打tag回滚到tag + liquibase = createLiquibase(completeChangeLog); + liquibase.checkLiquibaseTables(false, null, new Contexts(), new LabelExpression()); + liquibase.tag("empty"); + liquibase = createLiquibase(rollbackChangeLog); + liquibase.update(new Contexts()); + liquibase.rollback("empty", new Contexts()); + //回滚8个changeset + liquibase = createLiquibase(rollbackChangeLog); + liquibase.rollback(8, this.contexts); +``` +4. liquibase 永远不会将相同的变更集应用于同一环境,除非您覆盖其默认值(变更集属性 runOnChange 设置为 "true") + +## 团队管理数据库 +1. 一个团队管理一个数据库或一个团队管理多个数据库 + ![图片1](https://gitee.com/RenZhenGongZuo/base-components/raw/master/img/liquibase/img1.png) + ![图片2](https://gitee.com/RenZhenGongZuo/base-components/raw/master/img/liquibase/img2.png) +2. 多个团队管理一个数据库或多个团队管理多个数据库 + ![创建数据库](https://gitee.com/RenZhenGongZuo/base-components/raw/master/img/liquibase/img3.png) + 1. 您将使用单个 URL 和凭据连接到所有数据库。这要求您使用具有多个数据库权限的单个服务帐户。 + 2. 在 SQL 脚本中,每个对象都需要使用数据库名称、架构名称和对象名称进行完全限定 + 3. Liquibase 跟踪表 ( DATABASECHANGELOG 和 DATABASECHANGELOGLOCK ) 将仅在 URL 中指定的一个数据库中创建。对多个数据库的部署将由 + URL 中指定的数据库中的单个 DATABASECHANGELOG 跟踪表进行跟踪。 + 4. 当多个团队共享公共数据库时,无法使用每个团队自己的应用程序存储库。此用例需要为共享数据库设置专用的 SQL 存储库。 + +```text +-- 面向对象发布1 +com + example + db + changelog + changelog-root.xml + changelog-index.xml + changelog-procedure.xml + changelog-table.xml + changelog-view.xml +-- 面向对象发布2 +com + example + db + changelog + changelog-root.xml + changelog-indexes + my-favorite-index.xml + that-other-index.xml + changelog-tables + employees.xml + customers.xml +-- +com + example + db + changelog + changelog-root.xml + changelog-1.0.xml + changelog-1.1.xml + changelog-2.0.xml +com + example + db + changelog + changelog-root.xml + changelog-1.x + changelog-1.0.xml + changelog-1.1.xml + changelog-2.x + changelog-2.0.xml +``` + +[官网文档地址](https://contribute.liquibase.com/code/get-started/env-setup/) + +
自己操作后的命令记录 + +## diff 的命令允许您将两个相同类型或不同类型的数据库相互比较 + +```text +liquibase diff +--url="jdbc:oracle:thin:@::" +--username= +--password= +--reference-url="jdbc:oracle:thin:@::" +--reference-username= +--reference-password= +``` + +## generate-changelog 命令创建一个更改日志文件,该文件具有一系列变更集,这些变更集描述如何重新创建数据库的当前状态 + +[generate-changelog](https://docs.liquibase.com/commands/inspection/generate-changelog.html) + +```text +第一种方法.通过liquibase.properties 指定数据源信息,然后执行下面命令 +liquibase generate-changelog --changelog-file=example-changelog.xml +第二种方法 +liquibase generate-changelog +--url="jdbc:oracle:thin:@::" +--username= +--password= + +如果您的数据库需要 & in URL,则在命令行上指定 URL 时,可能需要将 URL 括在双引号中 +``` + +
\ No newline at end of file diff --git a/project/liquibase-test/pom.xml b/project/liquibase-test/pom.xml new file mode 100644 index 00000000..6c7aa0e7 --- /dev/null +++ b/project/liquibase-test/pom.xml @@ -0,0 +1,56 @@ + + 4.0.0 + + com.zhou.base.components + base-components + 1.0-SNAPSHOT + + + com.zhou.base + liquibase-test + jar + + liquibase-test + http://maven.apache.org + + + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.liquibase + liquibase-core + + + mysql + mysql-connector-java + 8.0.30 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/project/liquibase-test/src/main/java/com/zhou/base/liquibase/test/LiquibaseApplication.java b/project/liquibase-test/src/main/java/com/zhou/base/liquibase/test/LiquibaseApplication.java new file mode 100644 index 00000000..8561c149 --- /dev/null +++ b/project/liquibase-test/src/main/java/com/zhou/base/liquibase/test/LiquibaseApplication.java @@ -0,0 +1,34 @@ +package com.zhou.base.liquibase.test; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; + +import java.util.Arrays; + +/** + * Hello world! + */ +@SpringBootApplication +public class LiquibaseApplication { + + + public static void main(String[] args) { + System.out.println("Hello Liquibase"); + SpringApplication.run(LiquibaseApplication.class, args); + } + + @Bean + public CommandLineRunner commandLineRunner(ApplicationContext ctx) { + return args -> { + System.out.println("Let's inspect the beans provided by Spring Boot:"); + String[] beanNames = ctx.getBeanDefinitionNames(); + Arrays.sort(beanNames); + for (String beanName : beanNames) { + System.out.println(beanName); + } + }; + } +} diff --git a/project/liquibase-test/src/main/resources/application.properties b/project/liquibase-test/src/main/resources/application.properties new file mode 100644 index 00000000..f9f98d54 --- /dev/null +++ b/project/liquibase-test/src/main/resources/application.properties @@ -0,0 +1,8 @@ +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.url=jdbc:mysql://rm-8vb34z58iimdh7o3z7o.mysql.zhangbei.rds.aliyuncs.com:3306/liquibase-test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=UTC +spring.datasource.username=zxf_1 +spring.datasource.password=123456123 +#spring.liquibase.enabled=true +#spring.liquibase.change-log=classpath:db/changelog/changelog.sql +#spring.liquibase.change-log=classpath:db/changelog-with-folder/myChangeLog.xml +spring.liquibase.change-log=classpath:db/changelogs-multi/mainChangeLog.xml diff --git a/project/liquibase-test/src/main/resources/db/changelog-with-folder/myChangeLog.xml b/project/liquibase-test/src/main/resources/db/changelog-with-folder/myChangeLog.xml new file mode 100644 index 00000000..98cc273b --- /dev/null +++ b/project/liquibase-test/src/main/resources/db/changelog-with-folder/myChangeLog.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/changelog1.xml b/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/changelog1.xml new file mode 100644 index 00000000..c95a6f9e --- /dev/null +++ b/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/changelog1.xml @@ -0,0 +1,34 @@ + + + + + + select 1 + + + + You can add comments to changeSets. + They can even be multiple lines if you would like. + They aren't used to compute the changeSet MD5Sum, so you can update them whenever you want without causing + problems. + + + + + + + + + + + + + + + + + + diff --git a/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/v1.sql b/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/v1.sql new file mode 100644 index 00000000..b53f7e26 --- /dev/null +++ b/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/v1.sql @@ -0,0 +1,18 @@ +-- changeset your.name:zxf21 labels:example-label context:example-context +-- comment: example comment +create table PERSON111121 +( + ID int not null, + FNAME varchar(100) not null +); + +-- changeset your.name:zxf22 labels:example-label context:example-context +-- comment: example comment +create table company122 +( + id int primary key auto_increment not null, + name varchar(50) not null, + address1 varchar(50), + address2 varchar(50), + city varchar(30) +); \ No newline at end of file diff --git a/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/v2.sql b/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/v2.sql new file mode 100644 index 00000000..9fc6ad4c --- /dev/null +++ b/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/v2.sql @@ -0,0 +1,8 @@ +create table company1223 +( + id int primary key auto_increment not null, + name varchar(50) not null, + address1 varchar(50), + address2 varchar(50), + city varchar(30) +); \ No newline at end of file diff --git a/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/v3.sql b/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/v3.sql new file mode 100644 index 00000000..1d8bdd97 --- /dev/null +++ b/project/liquibase-test/src/main/resources/db/changelog-with-folder/sql/v3.sql @@ -0,0 +1,5 @@ +select * +from PERSON111121; +/*A comment*/ +select * +from PERSON111121 \ No newline at end of file diff --git a/project/liquibase-test/src/main/resources/db/changelog/changelog.sql b/project/liquibase-test/src/main/resources/db/changelog/changelog.sql new file mode 100644 index 00000000..0d3bce7a --- /dev/null +++ b/project/liquibase-test/src/main/resources/db/changelog/changelog.sql @@ -0,0 +1,44 @@ +-- liquibase formatted sql + +-- changeset liquibase:1 +CREATE TABLE `test_table` +( + `test_id` int NOT NULL, + `test_column` varchar(255) NULL, + PRIMARY KEY (`test_id`) +); + +-- changeset your.name:1 labels:example-label context:example-context +-- comment: example comment +create table person +( + id int primary key auto_increment not null, + name varchar(50) not null, + address1 varchar(50), + address2 varchar(50), + city varchar(30) +) + +-- changeset your.name:22 labels:example-label context:example-context +-- comment: example comment +create table company +( + id int primary key auto_increment not null, + name varchar(50) not null, + address1 varchar(50), + address2 varchar(50), + city varchar(30) +) +-- rollback DROP TABLE company; + +-- changeset your.name:23 labels:example-label context:example-context +-- comment: example comment +create table company1 +( + id int primary key auto_increment not null, + name varchar(50) not null, + address1 varchar(50), + address2 varchar(50), + city varchar(30) +); +-- rollback DROP TABLE company1; \ No newline at end of file diff --git a/project/liquibase-test/src/main/resources/db/changelogs-multi/databaseA/changelog.xml b/project/liquibase-test/src/main/resources/db/changelogs-multi/databaseA/changelog.xml new file mode 100644 index 00000000..1014f868 --- /dev/null +++ b/project/liquibase-test/src/main/resources/db/changelogs-multi/databaseA/changelog.xml @@ -0,0 +1,34 @@ + + + + + + select 1 + + + + You can add comments to changeSets. + They can even be multiple lines if you would like. + They aren't used to compute the changeSet MD5Sum, so you can update them whenever you want without causing + problems. + + + + + + + + + + + + + + + + + + diff --git a/project/liquibase-test/src/main/resources/db/changelogs-multi/databaseA/v1.sql b/project/liquibase-test/src/main/resources/db/changelogs-multi/databaseA/v1.sql new file mode 100644 index 00000000..e69de29b diff --git a/project/liquibase-test/src/main/resources/db/changelogs-multi/databaseB/changelog.xml b/project/liquibase-test/src/main/resources/db/changelogs-multi/databaseB/changelog.xml new file mode 100644 index 00000000..893a4cfb --- /dev/null +++ b/project/liquibase-test/src/main/resources/db/changelogs-multi/databaseB/changelog.xml @@ -0,0 +1,36 @@ + + + + + + select 1 + + + + + You can add comments to changeSets. + They can even be multiple lines if you would like. + They aren't used to compute the changeSet MD5Sum, so you can update them whenever you want without causing + problems. + + + + + + + + + + + + + + + + + + + diff --git a/project/liquibase-test/src/main/resources/db/changelogs-multi/databaseB/v1.sql b/project/liquibase-test/src/main/resources/db/changelogs-multi/databaseB/v1.sql new file mode 100644 index 00000000..e69de29b diff --git a/project/liquibase-test/src/main/resources/db/changelogs-multi/mainChangelog.xml b/project/liquibase-test/src/main/resources/db/changelogs-multi/mainChangelog.xml new file mode 100644 index 00000000..cd295985 --- /dev/null +++ b/project/liquibase-test/src/main/resources/db/changelogs-multi/mainChangelog.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/project/liquibase-test/src/test/resources/rollbackable.changelog.xml b/project/liquibase-test/src/test/resources/rollbackable.changelog.xml new file mode 100644 index 00000000..3a7bd1e8 --- /dev/null +++ b/project/liquibase-test/src/test/resources/rollbackable.changelog.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Refactorings that can't be automatically rolled back can be custom rolled back with a "rollback" tag. + + + + + + + + + + + + + + + delete from publication; + + + + + + + + + + + + + + + select title, isbn from publication + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/project/modbus/modbus-01/pom.xml b/project/modbus/modbus-01/pom.xml new file mode 100644 index 00000000..af5d37ff --- /dev/null +++ b/project/modbus/modbus-01/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + org.cpq + modbus-01 + 1.0-SNAPSHOT + + + 21 + 21 + UTF-8 + + + + + com.infiniteautomation + modbus4j + 3.1.0 + + + + + + + + false + + + true + + ias-snapshots + Infinite Automation Snapshot Repository + https://maven.mangoautomation.net/repository/ias-snapshot/ + + + + true + + + false + + ias-releases + Infinite Automation Release Repository + https://maven.mangoautomation.net/repository/ias-release/ + + + + \ No newline at end of file diff --git "a/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j-\345\206\231\345\215\225\344\270\252\345\257\204\345\255\230\345\231\250.jpg" "b/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j-\345\206\231\345\215\225\344\270\252\345\257\204\345\255\230\345\231\250.jpg" new file mode 100644 index 00000000..1535a05f Binary files /dev/null and "b/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j-\345\206\231\345\215\225\344\270\252\345\257\204\345\255\230\345\231\250.jpg" differ diff --git "a/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j-\350\257\273\345\217\226Float\346\225\260\346\215\256.jpg" "b/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j-\350\257\273\345\217\226Float\346\225\260\346\215\256.jpg" new file mode 100644 index 00000000..291b0a04 Binary files /dev/null and "b/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j-\350\257\273\345\217\226Float\346\225\260\346\215\256.jpg" differ diff --git "a/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j-\350\257\273\345\217\226int\346\225\260\346\215\256.jpg" "b/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j-\350\257\273\345\217\226int\346\225\260\346\215\256.jpg" new file mode 100644 index 00000000..b0b540dd Binary files /dev/null and "b/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j-\350\257\273\345\217\226int\346\225\260\346\215\256.jpg" differ diff --git a/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j.java b/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j.java new file mode 100644 index 00000000..811bea0c --- /dev/null +++ b/project/modbus/modbus-01/src/main/java/org/cpq/A01modbus4j.java @@ -0,0 +1,102 @@ +package org.cpq; + +import com.serotonin.modbus4j.ModbusFactory; +import com.serotonin.modbus4j.ModbusMaster; +import com.serotonin.modbus4j.code.DataType; +import com.serotonin.modbus4j.exception.ErrorResponseException; +import com.serotonin.modbus4j.exception.ModbusInitException; +import com.serotonin.modbus4j.exception.ModbusTransportException; +import com.serotonin.modbus4j.ip.IpParameters; +import com.serotonin.modbus4j.locator.BaseLocator; +import com.serotonin.modbus4j.msg.ReadHoldingRegistersRequest; +import com.serotonin.modbus4j.msg.ReadHoldingRegistersResponse; +import com.serotonin.modbus4j.msg.WriteRegisterRequest; +import com.serotonin.modbus4j.msg.WriteRegisterResponse; + +public class A01modbus4j { + public static void main(String[] args) { + // 创建 Modbus 工厂 + ModbusFactory factory = new ModbusFactory(); + + // 配置 TCP 参数 + IpParameters params = new IpParameters(); + params.setHost("127.0.0.1"); // 设备 IP + params.setPort(502); // 默认端口 502 + + // 创建 Modbus TCP 主站 + ModbusMaster master = factory.createTcpMaster(params, true); // false 表示非长连接 + + // 示例 1: 读取保持寄存器(功能码 03) + int slaveId = 1; // 从站地址 + int startOffset = 0; // 寄存器起始地址 + int numberOfRegisters = 10; // 读取数量 + + // 初始化连接 + try { + master.init(); + } catch (ModbusInitException e) { + e.printStackTrace(); + System.out.println("master.init()发生了异常"); + return; + } + + try { + holdingRegisterInt(slaveId, startOffset, numberOfRegisters, master); + + // holdingRegisterFloat(slaveId, master); + + // 示例 2: 写入单个寄存器(功能码 06) + // writeRegister(slaveId, 0, 1234, master); + + } catch (Exception e) { + e.printStackTrace(); + } finally { + master.destroy(); // 关闭连接 + } + } + + /** + * 读取整型数据 + * 图片:A01modbus4j-读取int数据.jpg + */ + private static void holdingRegisterInt(int slaveId, int startOffset, int numberOfRegisters, ModbusMaster master) throws ModbusTransportException, ErrorResponseException { + ReadHoldingRegistersRequest request = new ReadHoldingRegistersRequest(slaveId, startOffset, numberOfRegisters); + ReadHoldingRegistersResponse response = (ReadHoldingRegistersResponse) master.send(request); + if (!response.isException()) { + short[] values = response.getShortData(); + System.out.println("寄存器值0:" + values[0] ); + System.out.println("寄存器值1:" + values[1] ); + System.out.println("寄存器值2:" + values[2] ); + System.out.println("寄存器值3:" + values[3] ); + } + + // DataType.TWO_BYTE_INT_SIGNED 是 2字节有符号整数 + BaseLocator loc = BaseLocator.holdingRegister(slaveId, 1, DataType.TWO_BYTE_INT_SIGNED); + Number value = master.getValue(loc); + System.out.println("读取到的值:" + value); + } + + /** + * 读取整型数据 + * 图片:A01modbus4j-读取Float数据.jpg + */ + private static void holdingRegisterFloat(int slaveId, ModbusMaster master) throws ModbusTransportException, ErrorResponseException { + // DataType.FOUR_BYTE_FLOAT 是4字节浮点数 + BaseLocator num7 = BaseLocator.holdingRegister(slaveId, 7, DataType.FOUR_BYTE_FLOAT); + Number v7 = master.getValue(num7); + System.out.println("读取到的值v7:" + v7); + } + + /** + * 写入单个寄存器 + * 图片:A01modbus4j-写单个寄存器.jpg + */ + private static void writeRegister(int slaveId, int writeOffset, int valueToWrite, ModbusMaster master) throws ModbusTransportException { + WriteRegisterRequest writeRequest = new WriteRegisterRequest(slaveId, writeOffset, valueToWrite); + WriteRegisterResponse writeResponse = (WriteRegisterResponse) master.send(writeRequest); + if (!writeResponse.isException()) { + System.out.println("写入成功"); + } + } + +} \ No newline at end of file diff --git a/project/modbus/readme.md b/project/modbus/readme.md new file mode 100644 index 00000000..5d1a58c1 --- /dev/null +++ b/project/modbus/readme.md @@ -0,0 +1,167 @@ +# 存储区 + 输出线圈 0 + 地址:00001 ~ 09999 + 输入线圈 1 + 地址:10001 ~ 19999 + 输入寄存器 3 + 地址:30001 ~ 39999 + 输出寄存器 4 + 地址:40001 ~ 49999 + 存储区范围:5位和6位 标准地址 拓展地址 + 如果是6位的存储区,每个存储区最大地址是65536,则线圈、寄存器的地址分别是 + 000001 ~ 065536 + 100001 ~ 165536 + 300001 ~ 365536 + 400001 ~ 465536 + +# 读写功能码 + 读写 功能码 + + 读输出线圈 01 + 读输入线圈 02 + 读输出寄存器 03 + 读输入寄存器 04 + 写单个输出线圈 05 + 写单个输出寄存器 06 + 写多个输出线圈 15 + 写多个输出寄存器 16 + +# 工具 + mbpoll,模拟Modbus主站(如PLC、SCADA系统),用于测试和调试从设备(如传感器、执行器)。 + mbslave,模拟Modbus从站(如设备、仪表) + +# ModbusRTU协议 + +通用报文格式:从站地址(设备编码)+ 功能码 + 数据 + 校验 + + ModbusRTU + 从站地址:1字节 + 功能码:1字节 + 数据:可变长度 + 校验:2字节(CRC16校验) + +## 01H功能码读取输出线圈 + + 发送报文格式:从站地址(设备编码)+ 功能码 + 开始线圈 + 线圈数量 + CRC + 接收报文格式:从站地址(设备编码)+ 功能码 + 字节计数 + 数据 + CRC + +### 数据解析 + +![](./图片/01-01H功能码读取输出线圈.jpg) + +Tx:000078-01 01 00 0A 00 14 1C 07 + +Tx表示发送,000042-不用理会, + +01表示从站地址 + +01表示功能码,读取输出线圈 + +00 0A表示开始线圈。开始线圈是00010,也就是10,10转16进制是A,开始线圈占2字节,所以是00 0A + +00 14表示线圈数量。读取的线圈数量是20,转为16进制是14,线圈数量占2字节,所以是00 14 + +1C 07表示CRC校验 + +Rx:000079-01 01 03 03 00 00 CC 4E + +Rx表示接受报文,000043-不用理会, + +01表示从站地址 + +01表示功能码,读取输出线圈 + +03表示字节计数,线圈数量是20,一个字节是8,两个字节是16,20得用3个字节表示,所以是03 + +![](./图片/02-01H功能码读取输出线圈-接受报文.jpg) + +03 00 00 表示数据。第一个字节是0000 0011,所以是03,总共20个线圈,但后面的都是0,所以是00 00 + +CC 4E 表示CRC校验 + +## 02H功能码读取输入线圈 + 报文格式与“01H功能码读取输出线圈”相同,只是功能码为02 + 输入线圈只能通过slave改数据,不能通过Modbus poll改数据 + +# ModbusTCP协议 +## 通信格式说明 + MBAP报文头,7字节 + 事务标识符,2字节。报文ID,不参与运算 + 协议标识符,2字节。固定为00 00 + 长度,2字节。 长度之后总共的字节数 + 单元标识符,1字节。相当于ModbusRTU的从站地址 + 功能码,1字节 + 数据部分,可变长度 + +![](./图片/03-ModbusTCP与ModbusRTU对比.jpg) + +### ModbusTCP与RTU之间的关系 + 校验: + ModbusTCP一般是基于TCP/UDP,在传输层已经有了校验,不需要在应用层再做校验 + 站地址: + ModbusTCP弱化了站地址的概念,因为在以太网中,可通过IP地址确定一个设备。 + 但为了保守起见,还是保留了站地址,可使用单元标识符表示站地址。 + 在实际应用中,可以使用单元标识符表示站地址,也可以不使用单元标识符表示站地址。 + 如果忽略单元标识符,则单元标识符默认为1 + +## 02功能码读取输入线圈 + 发送报文格式:事务标识符 + 协议标识符 + 长度 + 单元标识符 + 功能码 + 线圈地址 + 长度 + 返回报文格式:事务标识符 + 协议标识符 + 长度 + 单元标识符 + 功能码 + 字节计数 + 数据 + +### 例子:读取1号站点从10开始的20个线圈的值 +![](./图片/04-读取输入线圈.jpg) + +Tx:000102-02 D8 00 00 00 06 01 02 00 0A 00 14 + + Tx表示发送,000102-不用理会 + 02 D8:事务标识符,自增 + 00 00:协议标识符 + 00 06:长度,06后面的数据是6字节,所以是06 + 01:单元标识符,忽略单元标识符,则单元标识符默认为1 + 02:功能码,02功能码读取输入线圈 + 00 0A:线圈地址,从00010开始读取,所以是00 0A + 00 14:读取的线圈数量,20个,转为16进制是14,所以是00 14 + +Rx:000103-02 D8 00 00 00 06 01 02 03 03 00 00 + + Rx表示接收数据,000103-不用理会 + 02 D8:事务标识符,自增 + 00 00:协议标识符 + 00 06:长度,06后面的数据是6字节,所以是06 + 01:单元标识符,忽略单元标识符,则单元标识符默认为1 + 02:功能码,02功能码读取输入线圈 + 03:字节计数,读20个线圈,一个字节是8,两个字节是16,20得用3个字节表示,所以是03 + 03 00 00:数据,输入线圈的值。第一个字节是0000 0011,所以是03,总共20个线圈,但后面的都是0,所以是00 00 + +## 04功能码读取输入寄存器 + 发送报文格式:事务标识符 + 协议标识符 + 长度 + 单元标识符 + 功能码 + 寄存器地址 + 长度 + 返回报文格式:事务标识符 + 协议标识符 + 长度 + 单元标识符 + 功能码 + 字节计数 + 数据 + +### 例子:读取1号站点从4开始的2个寄存器的值 +![](./图片/05-04功能码读取输入寄存器.jpg) + +Tx:001020-03 48 00 00 00 06 01 04 00 04 00 02 + + Tx表示发送,001020-不用理会 + 03 48:事务标识符,自增 + 00 00:协议标识符 + 00 06:长度,06后面的数据是6字节,所以是06 + 01:单元标识符,忽略单元标识符,则单元标识符默认为1 + 04:功能码,04功能码读取输入寄存器 + 00 04:寄存器地址,从起始地址4开始读取 + 00 02:长度,读取数量是2寄存器 + +Rx:001021-03 48 00 00 00 07 01 04 04 00 7B 01 59 + + Rx表示接收数据,001021-不用理会 + 03 48:事务标识符,自增 + 00 00:协议标识符 + 00 07:长度,07后面的数据是6字节,所以是06 + 01:单元标识符,忽略单元标识符,则单元标识符默认为1 + 04:功能码,04功能码读取输入寄存器 + 04:字节计数,读取两个寄存器,一个字节是8,两个字节是16,所以是04 + 00 7B 01 59:十六进制007B转十进制是123,0159转十进制是345 + + + + diff --git "a/project/modbus/\345\233\276\347\211\207/01-01H\345\212\237\350\203\275\347\240\201\350\257\273\345\217\226\350\276\223\345\207\272\347\272\277\345\234\210.jpg" "b/project/modbus/\345\233\276\347\211\207/01-01H\345\212\237\350\203\275\347\240\201\350\257\273\345\217\226\350\276\223\345\207\272\347\272\277\345\234\210.jpg" new file mode 100644 index 00000000..55e393bc Binary files /dev/null and "b/project/modbus/\345\233\276\347\211\207/01-01H\345\212\237\350\203\275\347\240\201\350\257\273\345\217\226\350\276\223\345\207\272\347\272\277\345\234\210.jpg" differ diff --git "a/project/modbus/\345\233\276\347\211\207/02-01H\345\212\237\350\203\275\347\240\201\350\257\273\345\217\226\350\276\223\345\207\272\347\272\277\345\234\210-\346\216\245\345\217\227\346\212\245\346\226\207.jpg" "b/project/modbus/\345\233\276\347\211\207/02-01H\345\212\237\350\203\275\347\240\201\350\257\273\345\217\226\350\276\223\345\207\272\347\272\277\345\234\210-\346\216\245\345\217\227\346\212\245\346\226\207.jpg" new file mode 100644 index 00000000..b9fce06d Binary files /dev/null and "b/project/modbus/\345\233\276\347\211\207/02-01H\345\212\237\350\203\275\347\240\201\350\257\273\345\217\226\350\276\223\345\207\272\347\272\277\345\234\210-\346\216\245\345\217\227\346\212\245\346\226\207.jpg" differ diff --git "a/project/modbus/\345\233\276\347\211\207/03-ModbusTCP\344\270\216ModbusRTU\345\257\271\346\257\224.jpg" "b/project/modbus/\345\233\276\347\211\207/03-ModbusTCP\344\270\216ModbusRTU\345\257\271\346\257\224.jpg" new file mode 100644 index 00000000..ee5ca2cf Binary files /dev/null and "b/project/modbus/\345\233\276\347\211\207/03-ModbusTCP\344\270\216ModbusRTU\345\257\271\346\257\224.jpg" differ diff --git "a/project/modbus/\345\233\276\347\211\207/04-\350\257\273\345\217\226\350\276\223\345\205\245\347\272\277\345\234\210.jpg" "b/project/modbus/\345\233\276\347\211\207/04-\350\257\273\345\217\226\350\276\223\345\205\245\347\272\277\345\234\210.jpg" new file mode 100644 index 00000000..4497e1e6 Binary files /dev/null and "b/project/modbus/\345\233\276\347\211\207/04-\350\257\273\345\217\226\350\276\223\345\205\245\347\272\277\345\234\210.jpg" differ diff --git "a/project/modbus/\345\233\276\347\211\207/05-04\345\212\237\350\203\275\347\240\201\350\257\273\345\217\226\350\276\223\345\205\245\345\257\204\345\255\230\345\231\250.jpg" "b/project/modbus/\345\233\276\347\211\207/05-04\345\212\237\350\203\275\347\240\201\350\257\273\345\217\226\350\276\223\345\205\245\345\257\204\345\255\230\345\231\250.jpg" new file mode 100644 index 00000000..186d60fa Binary files /dev/null and "b/project/modbus/\345\233\276\347\211\207/05-04\345\212\237\350\203\275\347\240\201\350\257\273\345\217\226\350\276\223\345\205\245\345\257\204\345\255\230\345\231\250.jpg" differ diff --git a/project/mqtt/README.md b/project/mqtt/README.md new file mode 100644 index 00000000..10c66caf --- /dev/null +++ b/project/mqtt/README.md @@ -0,0 +1,61 @@ +# emqx + +### docker 安装 + +docker pull emqx/emqx:v4.0.5 + +docker run -tid --name emqx -p 1883:1883 -p 8083:8083 -p 8081:8081 -p 8883:8883 -p 8084:8084 -p 18083:18083 emqx/emqx:v4.0.5 + +控制台访问地址:http://192.168.1.231:18083/ + +默认用户名/密码:admin/public + +### 认证 +EMQX 默认开启匿名认证,任何客户端都可以连接 + +/opt/emqx/etc + +allow_anonymous = true + +emqx restart 重启 + +启用 emqx_auth_username 插件,使用api接口新增密码 + +curl --location --request POST 'http://192.168.1.231:18083/api/v4/auth_username' \ +--header 'Authorization: Basic YWRtaW46cHVibGlj' \ +--header 'Content-Type: application/json' \ +--data-raw '{ +"username": "user", +"password": "123456" +}' + +Authorization 是 Basic空格 加上 admin/public 的base64编码 + +默认配置中 ACL 是开放授权的,acl_nomatch = allow,需要改为禁止 +配置文件位置:etc/emqx.conf +acl_nomatch = deny + +### webhook +修改 /opt/emqx/etc/plugins/emqx_web_hook.conf + +web.hook.api.url = http://192.168.1.3:8991/mqtt/webhook + +emqx restart 重启 + +在EMQX后台启动 emqx_web_hook 插件 + + + + + + + + + + + + + + + + diff --git a/project/mqtt/mqtt-client-paho/.gitignore b/project/mqtt/mqtt-client-paho/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/project/mqtt/mqtt-client-paho/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/project/mqtt/mqtt-client-paho/pom.xml b/project/mqtt/mqtt-client-paho/pom.xml new file mode 100644 index 00000000..b3b8e0eb --- /dev/null +++ b/project/mqtt/mqtt-client-paho/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.3.1.RELEASE + + + com.example + mqtt-client-paho + 0.0.1-SNAPSHOT + mqtt-client-paho + mqtt-client-paho + + 1.8 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.2 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/MqttClientPahoApplication.java b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/MqttClientPahoApplication.java new file mode 100644 index 00000000..ed9379ef --- /dev/null +++ b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/MqttClientPahoApplication.java @@ -0,0 +1,13 @@ +package com.example.mqttclientpaho; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MqttClientPahoApplication { + + public static void main(String[] args) { + SpringApplication.run(MqttClientPahoApplication.class, args); + } + +} diff --git a/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/TestMqtt.java b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/TestMqtt.java new file mode 100644 index 00000000..7b5b509a --- /dev/null +++ b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/TestMqtt.java @@ -0,0 +1,45 @@ +package com.example.mqttclientpaho; + +import com.example.mqttclientpaho.client.EmqClient; +import com.example.mqttclientpaho.enums.QosEnum; +import com.example.mqttclientpaho.properties.MqttProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class TestMqtt implements CommandLineRunner { + @Autowired + private EmqClient emqClient; + @Autowired + private MqttProperties mqttProperties; + + @Override + public void run(String... args) throws Exception { + // 连接mqtt broker + emqClient.connect(mqttProperties.getUsername(), mqttProperties.getPassword()); + // 订阅主题 + emqClient.subscribe("testtopic/#", QosEnum.QoS2); + + // 循环推送消息测试 + new Thread(() -> { + String topic = "testtopic/1234"; + while (true) { + String payload = "循环推送消息" + LocalDateTime.now().toString(); + emqClient.publish(topic, payload, QosEnum.QoS2, false); + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("", e); + } + } + }).start(); + } + +} diff --git a/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/callback/MessageCallback.java b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/callback/MessageCallback.java new file mode 100644 index 00000000..6de98d15 --- /dev/null +++ b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/callback/MessageCallback.java @@ -0,0 +1,53 @@ +package com.example.mqttclientpaho.callback; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +/** + * mqtt回调 + */ +@Slf4j +@Component +public class MessageCallback implements MqttCallback { + + @Override + public void connectionLost(Throwable throwable) { + // 丢失对服务端的连接后触发该方法回调,此处可以做一些特殊处理,比如重连 + log.info("丢失了对broker的连接"); + } + + /** + * 收到订阅的消息 + * 该方法由mqtt客户端同步调用,在此方法未正确返回之前,不会发送ack确认消息到broker。 + * 一旦该方法向外抛出了异常客户端将异常关闭,当再次连接时;所有QoS1,QoS2且客户端未进行ack确认的消息都将由 + * broker服务器再次发送到客户端 + */ + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + try { + log.info("收到订阅消息,topic={},messageId={},qos={}, payload={}", + topic, message.getId(), message.getQos(), new String(message.getPayload(), StandardCharsets.UTF_8)); + } catch (Exception e) { + log.error("处理订阅消息异常", e); + } + } + + /** + * 消息发布完成且收到ack确认后的回调 + * QoS0:消息被网络发出后触发一次 + * QoS1:当收到broker的PUBACK消息后触发 + * QoS2:当收到broer的PUBCOMP消息后触发 + */ + @Override + public void deliveryComplete(IMqttDeliveryToken token) { + int messageId = token.getMessageId(); + String[] topics = token.getTopics(); + log.info("消息发送完成,messageId={},topics={}",messageId,topics); + } + +} diff --git a/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/client/EmqClient.java b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/client/EmqClient.java new file mode 100644 index 00000000..ff0ca4ef --- /dev/null +++ b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/client/EmqClient.java @@ -0,0 +1,128 @@ +package com.example.mqttclientpaho.client; + +import com.example.mqttclientpaho.enums.QosEnum; +import com.example.mqttclientpaho.properties.MqttProperties; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +/** + * mqtt客户端封装 + */ +@Slf4j +@Component +public class EmqClient { + + private MqttClient mqttClient; + + @Autowired + private MqttProperties mqttProperties; + + @Autowired + private MqttCallback mqttCallback; + + @PostConstruct + public void init() { + // MqttClientPersistence是接口,实现类有 MqttDefaultFilePersistence、MemoryPersistence + MemoryPersistence memoryPersistence = new MemoryPersistence(); + try { + mqttClient = new MqttClient(mqttProperties.getBrokerUrl(), mqttProperties.getClientId(), memoryPersistence); + } catch (MqttException e) { + log.error("初始化mqttClient异常", e); + } + } + + /** + * 连接broker + */ + public void connect(String username, String password) { + // 创建 Mqtt连接选项 + MqttConnectOptions connectOptions = new MqttConnectOptions(); + // 自动重连 + connectOptions.setAutomaticReconnect(true); + /** + * 设置为true后意味着:客户端断开连接后emq不保留会话保留会话,否则会产生订阅共享队列的存活 + 客户端收不到消息的情况 + * 因为断开的连接还被保留的话,emq会将队列中的消息负载到断开但还保留的客户端,导致存活的客户 + 端收不到消息 + * 解决该问题有两种方案:1.连接断开后不要保持;2.保证每个客户端有固定的clientId + */ + connectOptions.setCleanSession(true); + connectOptions.setUserName(username); + connectOptions.setPassword(password.toCharArray()); + + //设置mqtt消息回调 + mqttClient.setCallback(mqttCallback); + + try { + mqttClient.connect(connectOptions); + } catch (MqttException e) { + log.error("连接mqtt broker失败", e); + } + } + + /** + * 发布消息 + */ + public void publish(String topic, String payload, QosEnum qos, boolean retain) { + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setQos(qos.value()); + mqttMessage.setRetained(retain); + mqttMessage.setPayload(payload.getBytes()); + if (mqttClient.isConnected()) { + try { + mqttClient.publish(topic, mqttMessage); + } catch (MqttException e) { + log.error("发布消息失败", e); + } + } + } + + /** + * 订阅主题 + */ + public void subscribe(String topicFilter, QosEnum qos) { + try { + mqttClient.subscribe(topicFilter, qos.value()); + } catch (MqttException e) { + log.error("订阅失败", e); + } + } + + /** + * 取消订阅 + */ + public void unSubscribe(String topicFilter) { + try { + mqttClient.unsubscribe(topicFilter); + } catch (MqttException e) { + log.error("取消订阅异常", e); + } + } + + /** + * 重连 + */ + public void reConnect() { + try { + mqttClient.reconnect(); + } catch (MqttException e) { + log.error("重连异常", e); + } + } + + @PreDestroy + public void disConnect() { + try { + mqttClient.disconnect(); + } catch (MqttException e) { + log.error("断开连接异常", e); + } + } + +} diff --git a/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/controller/WebHookController.java b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/controller/WebHookController.java new file mode 100644 index 00000000..ebacd8a5 --- /dev/null +++ b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/controller/WebHookController.java @@ -0,0 +1,39 @@ +package com.example.mqttclientpaho.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/mqtt") +public class WebHookController { + private static final Logger log = LoggerFactory.getLogger(WebHookController.class); + + private Map clientStatus = new HashMap<>(); + + @PostMapping("/webhook") + public void hook(@RequestBody Map params){ + log.info("emqx 触发 webhook,请求体数据={}",params); + + String action = (String) params.get("action"); + String clientId = (String) params.get("clientid"); + if(action.equals("client_connected")){ + log.info("客户端{}接入本系统",clientId); + clientStatus.put(clientId,true); + } + + if(action.equals("client_disconnected")){ + log.info("客户端{}下线",clientId); + clientStatus.put(clientId,false); + } + + } + + @GetMapping("/allStatus") + public Map getStatus(){ + return this.clientStatus; + } +} diff --git a/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/enums/QosEnum.java b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/enums/QosEnum.java new file mode 100644 index 00000000..79ff0afd --- /dev/null +++ b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/enums/QosEnum.java @@ -0,0 +1,19 @@ +package com.example.mqttclientpaho.enums; + +/** + * Qos枚举值 + */ +public enum QosEnum { + + QoS0(0),QoS1(1),QoS2(2); + + private final int value; + + QosEnum(int value) { + this.value = value; + } + + public int value(){ + return this.value; + } +} diff --git a/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/properties/MqttProperties.java b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/properties/MqttProperties.java new file mode 100644 index 00000000..8b3c12c5 --- /dev/null +++ b/project/mqtt/mqtt-client-paho/src/main/java/com/example/mqttclientpaho/properties/MqttProperties.java @@ -0,0 +1,60 @@ +package com.example.mqttclientpaho.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "mqtt") +public class MqttProperties { + + private String brokerUrl; + + private String clientId; + + private String username; + + private String password; + + + public String getBrokerUrl() { + return brokerUrl; + } + + public void setBrokerUrl(String brokerUrl) { + this.brokerUrl = brokerUrl; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + return "MqttProperties{" + + "brokerUrl='" + brokerUrl + '\'' + + ", clientId='" + clientId + '\'' + + ", username='" + username + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/project/mqtt/mqtt-client-paho/src/main/resources/application.yml b/project/mqtt/mqtt-client-paho/src/main/resources/application.yml new file mode 100644 index 00000000..ff65e1eb --- /dev/null +++ b/project/mqtt/mqtt-client-paho/src/main/resources/application.yml @@ -0,0 +1,12 @@ +server: + port: 8991 +spring: + application: + name: mqtt-client-paho + + +mqtt: + broker-url: tcp://192.168.1.231:1883 + client-id: emq-client-01 + username: user + password: 123456 \ No newline at end of file diff --git a/project/mqtt/mqtt-client-paho/src/test/java/com/example/mqttclientpaho/MqttClientPahoApplicationTests.java b/project/mqtt/mqtt-client-paho/src/test/java/com/example/mqttclientpaho/MqttClientPahoApplicationTests.java new file mode 100644 index 00000000..23ee7ccd --- /dev/null +++ b/project/mqtt/mqtt-client-paho/src/test/java/com/example/mqttclientpaho/MqttClientPahoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.mqttclientpaho; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class MqttClientPahoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git "a/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\2541\347\253\240 MQTT\345\215\217\350\256\256\344\270\216EMQ.pdf" "b/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\2541\347\253\240 MQTT\345\215\217\350\256\256\344\270\216EMQ.pdf" new file mode 100644 index 00000000..3a6f7503 Binary files /dev/null and "b/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\2541\347\253\240 MQTT\345\215\217\350\256\256\344\270\216EMQ.pdf" differ diff --git "a/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\2542\347\253\240 EMQ\345\237\272\347\241\200\345\212\237\350\203\275.pdf" "b/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\2542\347\253\240 EMQ\345\237\272\347\241\200\345\212\237\350\203\275.pdf" new file mode 100644 index 00000000..95df1222 Binary files /dev/null and "b/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\2542\347\253\240 EMQ\345\237\272\347\241\200\345\212\237\350\203\275.pdf" differ diff --git "a/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\254\344\270\211\347\253\240 \351\253\230\347\272\247\345\212\237\350\203\275\344\275\277\347\224\250(\344\270\200).pdf" "b/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\254\344\270\211\347\253\240 \351\253\230\347\272\247\345\212\237\350\203\275\344\275\277\347\224\250(\344\270\200).pdf" new file mode 100644 index 00000000..4e945ec2 Binary files /dev/null and "b/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\254\344\270\211\347\253\240 \351\253\230\347\272\247\345\212\237\350\203\275\344\275\277\347\224\250(\344\270\200).pdf" differ diff --git "a/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\254\344\272\224\347\253\240 EMQ\347\232\204\351\253\230\347\272\247\345\212\237\350\203\275\344\275\277\347\224\250(\344\270\211).pdf" "b/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\254\344\272\224\347\253\240 EMQ\347\232\204\351\253\230\347\272\247\345\212\237\350\203\275\344\275\277\347\224\250(\344\270\211).pdf" new file mode 100644 index 00000000..2c931354 Binary files /dev/null and "b/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\254\344\272\224\347\253\240 EMQ\347\232\204\351\253\230\347\272\247\345\212\237\350\203\275\344\275\277\347\224\250(\344\270\211).pdf" differ diff --git "a/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\254\345\233\233\347\253\240 EMQ\347\232\204\351\253\230\347\272\247\345\212\237\350\203\275\344\275\277\347\224\250(\344\272\214).pdf" "b/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\254\345\233\233\347\253\240 EMQ\347\232\204\351\253\230\347\272\247\345\212\237\350\203\275\344\275\277\347\224\250(\344\272\214).pdf" new file mode 100644 index 00000000..05f08ef9 Binary files /dev/null and "b/project/mqtt/mqtt-client-paho/\346\226\207\346\241\243/\347\254\254\345\233\233\347\253\240 EMQ\347\232\204\351\253\230\347\272\247\345\212\237\350\203\275\344\275\277\347\224\250(\344\272\214).pdf" differ diff --git a/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/MybatisPulsLearnApplication.java b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/MybatisPulsLearnApplication.java index 2acd157b..ecd52ac9 100644 --- a/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/MybatisPulsLearnApplication.java +++ b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/MybatisPulsLearnApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @SpringBootApplication //@MapperScan("com.cpq.mybatispulslearn.**.mapper") public class MybatisPulsLearnApplication { @@ -11,7 +13,5 @@ public static void main(String[] args) { SpringApplication.run(MybatisPulsLearnApplication.class, args); } - - } diff --git a/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/config/SpringDynamicCronTask.java b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/config/SpringDynamicCronTask.java new file mode 100644 index 00000000..20f955b0 --- /dev/null +++ b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/config/SpringDynamicCronTask.java @@ -0,0 +1,45 @@ +package com.cpq.mybatispulslearn.config; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.cpq.mybatispulslearn.linyi.third_to_cottage.entity.ThirdToCottage; +import com.cpq.mybatispulslearn.linyi.third_to_cottage.mapper.ThirdToCottageMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.TriggerContext; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Component; + +import java.util.Date; + + +@Slf4j +@Component +public class SpringDynamicCronTask implements SchedulingConfigurer { + + @Autowired + private ThirdToCottageMapper thirdToCottageMapper; + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.addTriggerTask(new Runnable() { + @Override + public void run() { + // 任务逻辑 + log.debug("##############..."); + } + }, new Trigger() { + @Override + public Date nextExecutionTime(TriggerContext triggerContext) { + ThirdToCottage thirdToCottage = thirdToCottageMapper.selectOne(Wrappers.lambdaQuery()); + String cron = thirdToCottage.getThirdVillageCode(); + // 任务触发,可修改任务的执行周期 + CronTrigger trigger = new CronTrigger(cron); + Date nextExec = trigger.nextExecutionTime(triggerContext); + return nextExec; + } + }); + } +} diff --git a/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/config/TaskConfig.java b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/config/TaskConfig.java new file mode 100644 index 00000000..df1be960 --- /dev/null +++ b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/config/TaskConfig.java @@ -0,0 +1,21 @@ +package com.cpq.mybatispulslearn.config; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.cpq.mybatispulslearn.linyi.third_to_cottage.entity.ThirdToCottage; +import com.cpq.mybatispulslearn.linyi.third_to_cottage.mapper.ThirdToCottageMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class TaskConfig { + + @Autowired + private ThirdToCottageMapper thirdToCottageMapper; + + public String getCron() { + ThirdToCottage thirdToCottage = thirdToCottageMapper.selectOne(Wrappers.lambdaQuery()); + return thirdToCottage.getThirdVillageCode(); + } +} diff --git a/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/config/TestTask.java b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/config/TestTask.java new file mode 100644 index 00000000..6f9e5ba0 --- /dev/null +++ b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/config/TestTask.java @@ -0,0 +1,25 @@ +// package com.cpq.mybatispulslearn.config; +// +// import lombok.extern.slf4j.Slf4j; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.scheduling.annotation.Scheduled; +// import org.springframework.stereotype.Component; +// +// import java.time.LocalDateTime; +// +// @Slf4j +// @Component +// public class TestTask { +// +// @Autowired +// private TaskConfig taskConfig; +// +// @Scheduled(cron = "#{@taskConfig.getCron() ?: '0 0/12 * * * ?'}") +// public void aaa() { +// LocalDateTime now = LocalDateTime.now(); +// log.info("{}################", now); +// } +// +// +// +// } diff --git a/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/controller/ThirdToCottageController.java b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/controller/ThirdToCottageController.java index 9ccaae99..02e4bd2e 100644 --- a/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/controller/ThirdToCottageController.java +++ b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/controller/ThirdToCottageController.java @@ -1,10 +1,16 @@ package com.cpq.mybatispulslearn.linyi.third_to_cottage.controller; - +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.cpq.mybatispulslearn.linyi.third_to_cottage.entity.ThirdToCottage; +import com.cpq.mybatispulslearn.linyi.third_to_cottage.mapper.ThirdToCottageMapper; +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.RestController; +import java.util.List; + /** *

* 第三方系统、田丁后台的社区、房屋映射表 前端控制器 @@ -17,4 +23,15 @@ @RequestMapping("/third_to_cottage/thirdToCottage") public class ThirdToCottageController { + @Autowired + private ThirdToCottageMapper thirdToCottageMapper; + + @GetMapping("/test") + public String test() { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + List thirdToCottages = thirdToCottageMapper.selectList(lqw); + System.out.println(thirdToCottages); + return ""; + } + } diff --git a/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/entity/ThirdToCottage.java b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/entity/ThirdToCottage.java index fa4f8d2c..e834e176 100644 --- a/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/entity/ThirdToCottage.java +++ b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/entity/ThirdToCottage.java @@ -1,11 +1,11 @@ package com.cpq.mybatispulslearn.linyi.third_to_cottage.entity; -import com.baomidou.mybatisplus.annotation.TableField; -import java.io.Serializable; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; +import java.io.Serializable; + /** *

* 第三方系统、田丁后台的社区、房屋映射表 @@ -21,24 +21,6 @@ public class ThirdToCottage implements Serializable { private static final long serialVersionUID = 1L; - /** - * 社区id - */ - @TableField("villageID") - private Integer villageID; - - /** - * 社区楼栋id - */ - @TableField("buildingID") - private Integer buildingID; - - /** - * 社区房屋id - */ - @TableField("cottageID") - private Integer cottageID; - /** * 第三方社区code */ @@ -49,15 +31,4 @@ public class ThirdToCottage implements Serializable { */ private String thirdBuildingCode; - /** - * 第三方社区房屋code - */ - private String thirdCottageCode; - - /** - * 第三方系统:1-零壹系统 - */ - private Integer source; - - } diff --git a/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/mapper/ThirdToCottageMapper.xml b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/mapper/ThirdToCottageMapper.xml index e4d57084..a29757ef 100644 --- a/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/mapper/ThirdToCottageMapper.xml +++ b/project/mybatis-plus-learn/src/main/java/com/cpq/mybatispulslearn/linyi/third_to_cottage/mapper/ThirdToCottageMapper.xml @@ -4,18 +4,13 @@ - - - - - - villageID, buildingID, cottageID, third_village_code, third_building_code, third_cottage_code, source + third_village_code, third_building_code diff --git a/project/mybatis-plus-learn/src/main/resources/application.properties b/project/mybatis-plus-learn/src/main/resources/application.properties index 9fb7aae7..0c62a27e 100644 --- a/project/mybatis-plus-learn/src/main/resources/application.properties +++ b/project/mybatis-plus-learn/src/main/resources/application.properties @@ -7,7 +7,7 @@ spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name = com.mysql.jdbc.Driver spring.datasource.url= jdbc:mysql://localhost:3306/cpq?useUnicode=true&useSSL=false&characterEncoding=utf8 spring.datasource.username=root -spring.datasource.password=poly2017 +spring.datasource.password=cpq..123 spring.datasource.initialSize=10 spring.datasource.maxActive=50 spring.datasource.minIdle=0 @@ -21,10 +21,6 @@ spring.datasource.poolPreparedStatements=false #spring.redis.port=6379 #spring.redis.database=0 -120.24.84.12 - - - #xml\u4F4D\u7F6E #mybatis-plus.mapper-locations=classpath:com/cpq/mybatispulslearn/**/mapper/*.xml #mybatis.mapper-locations=classpath:com/cpq/mybatispulslearn/**/mapper/*.xml diff --git a/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/test/TestAbs.java b/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/test/TestAbs.java new file mode 100644 index 00000000..9c46669f --- /dev/null +++ b/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/test/TestAbs.java @@ -0,0 +1,20 @@ +package org.cpq.nettyinaction.a12_first_app.test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public abstract class TestAbs implements TestInterface{ + + public void testObject(Object obj) { + Type[] genericInterfaces = getClass().getGenericInterfaces(); + ParameterizedType parameterizedType = (ParameterizedType)genericInterfaces[0]; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + Type actualTypeArgument = actualTypeArguments[0]; + ObjectMapper objectMapper = new ObjectMapper(); + // objectMapper.readValue("fsdfas", actualTypeArgument.getClass()) + } + + public abstract void testType(T type); +} diff --git a/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/test/TestInterface.java b/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/test/TestInterface.java new file mode 100644 index 00000000..22171493 --- /dev/null +++ b/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/test/TestInterface.java @@ -0,0 +1,8 @@ +package org.cpq.nettyinaction.a12_first_app.test; + +public interface TestInterface { + + void testObject(Object obj); + + void testType(T type); +} diff --git a/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/test/User.java b/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/test/User.java new file mode 100644 index 00000000..5feb62a1 --- /dev/null +++ b/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/test/User.java @@ -0,0 +1,30 @@ +package org.cpq.nettyinaction.a12_first_app.test; + +import java.io.Serializable; + +public class User implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer age; + + private String name; + + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/type/TypeParent.java b/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/type/TypeParent.java new file mode 100644 index 00000000..b40a6c2b --- /dev/null +++ b/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/type/TypeParent.java @@ -0,0 +1,29 @@ +package org.cpq.nettyinaction.a12_first_app.type; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public class TypeParent { + + private Class mTClass; + private Class mRClass; + + @SuppressWarnings("unchecked") + public TypeParent() { + Type t = getClass().getGenericSuperclass(); + System.out.println("getClass() == " + getClass()); + System.out.println("getClass().getSuperclass() == " + getClass().getSuperclass()); + System.out.println("getClass().getGenericSuperclass() == " + t); + Type firType = ((ParameterizedType) t).getActualTypeArguments()[0]; + System.out.println("((ParameterizedType) t).getActualTypeArguments()[0] == " + firType); + Type secType = ((ParameterizedType) t).getActualTypeArguments()[1]; + System.out.println("((ParameterizedType) t).getActualTypeArguments()[1] == " + secType); + try { + mTClass = (Class) firType; + mRClass = (Class) secType; + } catch (ClassCastException e) { + e.printStackTrace(); + } + System.out.println("getActualTypeArguments() length == " + ((ParameterizedType) t).getActualTypeArguments().length); + } +} diff --git a/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/type/TypeSub.java b/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/type/TypeSub.java new file mode 100644 index 00000000..8f15e7f8 --- /dev/null +++ b/project/netty/netty-in-action/src/main/java/org/cpq/nettyinaction/a12_first_app/type/TypeSub.java @@ -0,0 +1,23 @@ +package org.cpq.nettyinaction.a12_first_app.type; + + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public class TypeSub extends TypeParent { + public static void main(String[] args) throws Exception{ + // new TypeSub(); + + System.out.println("##############"); + /** + * 如果是继承基类而来的泛型,就用 getGenericSuperclass() , 转型为 ParameterizedType 来获得实际类型 + * 如果是实现接口而来的泛型,就用 getGenericInterfaces() , 针对其中的元素转型为 ParameterizedType 来获得实际类型 + */ + ParameterizedType parameterizedType = (ParameterizedType)TypeSub.class.getGenericSuperclass(); + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + System.out.println("00000000000 " + actualTypeArguments[0]); + System.out.println("11111 " + actualTypeArguments[1]); + + } +} + diff --git "a/project/rabbitmq/rabbitmq-springboot-consumer/src/main/resources/application-\345\216\237\345\247\213\346\226\207\344\273\266.properties" "b/project/rabbitmq/rabbitmq-springboot-consumer/src/main/resources/application-\345\216\237\345\247\213\346\226\207\344\273\266.properties" index 41387578..b976c4d1 100644 --- "a/project/rabbitmq/rabbitmq-springboot-consumer/src/main/resources/application-\345\216\237\345\247\213\346\226\207\344\273\266.properties" +++ "b/project/rabbitmq/rabbitmq-springboot-consumer/src/main/resources/application-\345\216\237\345\247\213\346\226\207\344\273\266.properties" @@ -3,17 +3,17 @@ spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.virtual-host=/ spring.rabbitmq.connection-timeout=15000 -#\u624B\u5DE5\u7B7E\u6536 -#none\u4E0D\u53D1\u9001ack\u3002manual\u624B\u5DE5\u7B7E\u6536\u3002auto\u81EA\u52A8ack +#手工签收 +#none不发送ack。manual手工签收。auto自动ack spring.rabbitmq.listener.simple.acknowledge-mode=manual -#\u6700\u5C0F\u6D88\u8D39\u8005\u6570\u91CF +#最小消费者数量 spring.rabbitmq.listener.simple.concurrency=5 -#\u6700\u5927\u6D88\u8D39\u8005\u6570\u91CF +#最大消费者数量 spring.rabbitmq.listener.simple.max-concurrency=10 spring.rabbitmq.listener.order.queue.name=queue-2 spring.rabbitmq.listener.order.queue.durable=true spring.rabbitmq.listener.order.exchange.name=exchange-2 spring.rabbitmq.listener.order.exchange.durable=true spring.rabbitmq.listener.order.exchange.type=topic -spring.rabbitmq.listener.order.exchange.ignoreDeclarationExceptions=true +spring.rabbitmq.listener.order.exchange.ignoredeclarationexceptions=true spring.rabbitmq.listener.order.key=springboot.* \ No newline at end of file diff --git a/project/rabbitmq/rabbitmq-springboot-consumer/src/main/resources/application.properties b/project/rabbitmq/rabbitmq-springboot-consumer/src/main/resources/application.properties index 6c16aedf..b6b7fb46 100644 --- a/project/rabbitmq/rabbitmq-springboot-consumer/src/main/resources/application.properties +++ b/project/rabbitmq/rabbitmq-springboot-consumer/src/main/resources/application.properties @@ -7,43 +7,43 @@ spring.rabbitmq.connection-timeout=1500000000 spring.rabbitmq.publisher-confirms=true spring.rabbitmq.publisher-returns=true spring.rabbitmq.template.mandatory=true -#\u624B\u5DE5\u7B7E\u6536 -#none\u4E0D\u53D1\u9001ack\u3002manual\u624B\u5DE5\u7B7E\u6536\u3002auto\u81EA\u52A8ack +#手工签收 +#none不发送ack。manual手工签收。auto自动ack spring.rabbitmq.listener.simple.acknowledge-mode=manual -#\u6700\u5C0F\u6D88\u8D39\u8005\u6570\u91CF +#最小消费者数量 spring.rabbitmq.listener.simple.concurrency=5 -#\u6700\u5927\u6D88\u8D39\u8005\u6570\u91CF +#最大消费者数量 spring.rabbitmq.listener.simple.max-concurrency=10 #spring.rabbitmq.listener.simple.default-requeue-rejected=false -##\u662F\u5426\u5F00\u542F\u6D88\u8D39\u8005\u91CD\u8BD5\uFF0C\u6D88\u8D39\u7AEF\u4EE3\u7801\u629B\u51FA\u5F02\u5E38\uFF0C\u5219\u91CD\u8BD5 +##是否开启消费者重试,消费端代码抛出异常,则重试 #spring.rabbitmq.listener.simple.retry.enabled=true -##\u6700\u5927\u91CD\u8BD5\u6B21\u6570 +##最大重试次数 #spring.rabbitmq.listener.simple.retry.max-attempts=3 -##\u91CD\u8BD5\u95F4\u9694\u65F6\u95F4\uFF08\u5355\u4F4D\u6BEB\u79D2\uFF09 +##重试间隔时间(单位毫秒) #spring.rabbitmq.listener.simple.retry.initial-interval=30000 -##\u91CD\u8BD5\u6B21\u6570\u8D85\u8FC7\u4E0A\u9762\u7684\u8BBE\u7F6E\u4E4B\u540E\u662F\u5426\u4E22\u5F03\uFF08false\u4E0D\u4E22\u5F03\u65F6\u9700\u8981\u5199\u76F8\u5E94\u4EE3\u7801\u5C06\u8BE5\u6D88\u606F\u52A0\u5165\u6B7B\u4FE1\u961F\u5217\uFF09 +##重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列) #spring.rabbitmq.listener.simple.default-requeue-rejected=false -# @RabbitListener\u7684\u914D\u7F6E\u5199\u5230\u914D\u7F6E\u6587\u4EF6\u4E2D -# queueName\u4E0E\u6D88\u606F\u7684\u8F6C\u53D1\u63A5\u53D7\u89C4\u5219\u65E0\u5173 +# @rabbitlistener的配置写到配置文件中 +# queuename与消息的转发接受规则无关 spring.rabbitmq.listener.order.queue.name=test-qudddeue-2 spring.rabbitmq.listener.order.queue.durable=true -# \u9ED8\u8BA4\u7684exchange\uFF0C\u53D1\u9001mqtt\u4F7F\u7528\u9ED8\u8BA4\u7684exchange +# 默认的exchange,发送mqtt使用默认的exchange #spring.rabbitmq.listener.order.exchange.name=amq.topic spring.rabbitmq.listener.order.exchange.name=test-exchange-2 -# \u4E0D\u6301\u4E45\u5316 +# 不持久化 spring.rabbitmq.listener.order.exchange.durable=true -# routingKey\u5B8C\u5168\u5339\u914D\u6A21\u5F0F -# exchange\u5DF2\u7ECF\u5EFA\u7ACB\uFF0C\u5219exchange\u7684type\u4E0D\u80FD\u518D\u66F4\u6539\u3002\u66F4\u6539\u540E\uFF0C\u63A7\u5236\u53F0\u4F1A\u6253\u5370\u4E00\u4E2A\u9519\u8BEF\u4FE1\u606F -# direct\u6A21\u5F0F\uFF0C\u591A\u4E2A\u526F\u672C\uFF0C\u540C\u4E00\u4E2A\u6D88\u606F\uFF0C\u53EA\u6709\u4E00\u4E2A\u526F\u672C\u80FD\u83B7\u53D6\u5230\u4FE1\u606F\uFF0C\u6D88\u606F\u4E0D\u662F\u5E7F\u64AD\u6A21\u5F0F -# mqtt\u7684\u4EA4\u6362\u673A\u662Ftopic\u7C7B\u578B +# routingkey完全匹配模式 +# exchange已经建立,则exchange的type不能再更改。更改后,控制台会打印一个错误信息 +# direct模式,多个副本,同一个消息,只有一个副本能获取到信息,消息不是广播模式 +# mqtt的交换机是topic类型 spring.rabbitmq.listener.order.exchange.type=topic spring.rabbitmq.listener.order.key=routing_key_2 -# mqtt\u4F7F\u7528 +# mqtt使用 #spring.rabbitmq.listener.order.key=routing_key_2 -# topic\u901A\u914D\u7B26\u6A21\u5F0F +# topic通配符模式 #spring.rabbitmq.listener.order.exchange.type=topic #spring.rabbitmq.listener.order.key=test-exchange-2.* -spring.rabbitmq.listener.order.exchange.ignoreDeclarationExceptions=true +spring.rabbitmq.listener.order.exchange.ignoredeclarationexceptions=true custom.rabbit.durable.true=true custom.rabbit.type.fanout=fanout custom.rabbit.type.direct=direct diff --git a/project/rabbitmq/rabbitmq-springcloudstream-consumer/src/main/resources/application.properties b/project/rabbitmq/rabbitmq-springcloudstream-consumer/src/main/resources/application.properties index eafce7ee..76a1fe8c 100644 --- a/project/rabbitmq/rabbitmq-springcloudstream-consumer/src/main/resources/application.properties +++ b/project/rabbitmq/rabbitmq-springcloudstream-consumer/src/main/resources/application.properties @@ -2,9 +2,9 @@ server.port=8002 server.context-path=/consumer spring.application.name=consumer -#\u6307\u5B9A\u4EA4\u6362\u673A +#指定交换机 spring.cloud.stream.bindings.input_channel.destination=exchange-3 -#\u517C\u5BB9kafka\uFF0C\u7528group\u4EE3\u66FFqueue +#兼容kafka,用group代替queue spring.cloud.stream.bindings.input_channel.group=queue-3 spring.cloud.stream.bindings.input_channel.binder=rabbit_cluster spring.cloud.stream.bindings.input_channel.consumer.concurrency=1 @@ -13,7 +13,7 @@ spring.cloud.stream.rabbit.bindings.input_channel.consumer.acknowledge-mode=MANU spring.cloud.stream.rabbit.bindings.input_channel.consumer.recovery-interval=3000 spring.cloud.stream.rabbit.bindings.input_channel.consumer.durable-subscription=true spring.cloud.stream.rabbit.bindings.input_channel.consumer.max-concurrency=5 -#rabbit_cluster\u5C31\u662F\u4E0A\u9762\u5B9A\u4E49binder +#rabbit_cluster就是上面定义binder spring.cloud.stream.binders.rabbit_cluster.type=rabbit spring.cloud.stream.binders.rabbit_cluster.environment.spring.rabbitmq.addresses=localhost:5672 spring.cloud.stream.binders.rabbit_cluster.environment.spring.rabbitmq.username=guest diff --git a/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/config/RabbitMqConfig.java b/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/config/RabbitMqConfig.java index f0dfb4ed..5b8dbbb5 100644 --- a/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/config/RabbitMqConfig.java +++ b/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/config/RabbitMqConfig.java @@ -25,7 +25,7 @@ public Object handleError(Message amqpMessage, org.springframework.messaging.Mes String playloadStr = objectMapper.writeValueAsString(payload); System.out.println("customListenerErrorHandler,playloadStr: " + playloadStr); - log.error("", exception); + exception.printStackTrace(); /** * 需要抛出异常,在ack=auto的场景下,不抛出异常,不会重试 */ diff --git a/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverAck.java b/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverAck.java index 75c86452..c829d220 100644 --- a/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverAck.java +++ b/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverAck.java @@ -1,158 +1,47 @@ -package com.coq.rabbitmq.sc01.conusmer; - -import com.coq.rabbitmq.sc01.bean.Order01; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.rabbitmq.client.Channel; -import lombok.extern.slf4j.Slf4j; -import org.springframework.amqp.core.ExchangeTypes; -import org.springframework.amqp.rabbit.annotation.*; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.support.AmqpHeaders; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -public class ReceiverAck { - - @Autowired - RabbitTemplate rabbitTemplate; - - /* - 把RabbitAdmin设置成admin.setIgnoreDeclarationExceptions(true);,这样的好处是即使配置出现了错误也不至于整个应用程序都启动失败的情况。默认情况下,当出现异常时, RabbitAdmin 会立即停止所有声明的处理过程,这就有可能会导致一些问题- 如监听器容器会初始化失败,因另外的队列没有声明,从而web应用启动失败 - - # 重试 - #是否开启消费者重试,消费端代码抛出异常,则重试 - spring.rabbitmq.listener.simple.retry.enabled=true - #最大重试次数 - spring.rabbitmq.listener.simple.retry.max-attempts=6 - #重试间隔时间(单位毫秒) - spring.rabbitmq.listener.simple.retry.initial-interval=20000 - #重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列) - #如果没加死信队列,即使设置为false,重试到达最大次数后也会被丢弃 - spring.rabbitmq.listener.simple.default-requeue-rejected=false - - # 死信队列 - 队列已经创建,但是没加死信队列;后期加上死信队列,会报错。 - 解决办法一:删除队列重建 - 解决办法二: - 1、打开rabbitmq控制台 -> admin -> policies -> Add / update a policy - Name: 这个Policy的名称 - Pattern: Policy根据正则表达式去匹配Queues/Exchanges名称 - Apply to: 这个Policy对Queue还是对Exchange生效,或者两者都适用 - Priority: 优先级。 - Definition: 添加的args,KV键值对。 - 在控制台设置死信队列,key取消前缀x- 。以下是示例 - Name Pattern Apply to Definition Priority - dead-policy queue-ack queues dead-letter-exchange: dead_exchange - dead-letter-routing-key: dead_key - 消费者抛异常的现象: - 1、执行消费端代码,执行抛出异常代码,但是没有异常堆栈 - 2、进入errorHandler,执行errorHandler代码,但是不抛出异常,也没有异常堆栈 - 3、重复执行 1、2 直到到达最大重试次数 - 4、抛出异常,打印异常堆栈 - 5、进入死信队列消费端代码 - 创建queue时绑定死信列队需要以前缀x-开头,不以x-开头,就要让policy的Pattern能匹配到queue - 在rabbitmq控制台给queue绑定死信队列,然后添加@Argument注解,key需要取消x- - */ - @RabbitListener( - bindings = @QueueBinding( - value = @Queue(value = "queue-ack", - durable = "true", - autoDelete = "false" - // arguments = { - // @Argument(name = "x-dead-letter-exchange", value = DEAD_EXCHANGE), - // @Argument(name = "x-dead-letter-routing-key", value = DEAD_KEY) - // } - ), - exchange = @Exchange(value = "exchange-ack", - durable = "true", - type = ExchangeTypes.DIRECT, - autoDelete = "false", - ignoreDeclarationExceptions = "true"), - key = {"key-ack"}), - errorHandler = "customListenerErrorHandler" + package com.coq.rabbitmq.sc01.conusmer; + + import com.coq.rabbitmq.sc01.bean.Order01; + import com.fasterxml.jackson.databind.ObjectMapper; + import com.rabbitmq.client.Channel; + import java.math.BigDecimal; + import java.util.Date; + import lombok.extern.slf4j.Slf4j; + import org.springframework.amqp.core.ExchangeTypes; + import org.springframework.amqp.rabbit.annotation.*; + import org.springframework.amqp.rabbit.core.RabbitTemplate; + import org.springframework.amqp.support.AmqpHeaders; + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.messaging.Message; + import org.springframework.messaging.MessageHeaders; + import org.springframework.stereotype.Component; + + @Component + public class ReceiverAck { + + @Autowired + RabbitTemplate rabbitTemplate; + + /** + * 监听福讯支付通知 + */ + @RabbitListener(bindings = @QueueBinding( + exchange = @Exchange(value = "02q44LTW8O9Xb8uK_thing",type = "topic"), + key = "2R3qQJs42It.event.#", + value = @Queue(value = "02q44LTW8O9Xb8uK_ibms_b11") ) - @RabbitHandler - public void onOrderMessage(Message message, Channel channel) throws Exception{ - - /** - * Message总共有两种 - * org.springframework.amqp.core.Message 包含较多信息,但是headers默认空的,不好用 - * org.springframework.messaging.Message 包含 Payload、Headers - * @Payload 如果使用javaBean,生产者、消费者 发送的javaBean必须是同一个类 - */ - // byte[] body = message.getBody(); - // ObjectMapper objectMapper = new ObjectMapper(); - // Order01 order01 = objectMapper.readValue(body, Order01.class); - // System.out.println("消费端:"+order01.toString()); - // - // MessageProperties messageProperties = message.getMessageProperties(); - // Long deliveryTag = messageProperties.getDeliveryTag(); - // System.out.println("消费端deliveryTag:"+deliveryTag); - - /** - * org.springframework.messaging.Message 不能修改header - */ - Object payload = message.getPayload(); - ObjectMapper objectMapper = new ObjectMapper(); - String payloadStr = payload instanceof String ? (String) payload : objectMapper.writeValueAsString(payload); - Order01 order01 = objectMapper.readValue(payloadStr, Order01.class); - System.out.println("消费端:"+order01.toString()); - - MessageHeaders headers = message.getHeaders(); - Long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG); - System.out.println("消费端deliveryTag:"+deliveryTag); - - // 可以使用MESSAGE_ID作为消息标识,重回队列后MESSAGE_ID不变 - String messageId = String.valueOf(headers.get(AmqpHeaders.MESSAGE_ID)); - System.out.println("消费端messageId:"+messageId); - - - /* - channel.basicAck(deliveryTag, multiple); - consumer处理成功后,通知broker删除队列中的消息,如果设置multiple=true,表示支持批量确认机制以减少网络流量。 - 例如:有值为5,6,7,8 deliveryTag的投递 - 如果此时channel.basicAck(8, true);则表示前面未确认的5,6,7投递也一起确认处理完毕。 - 如果此时channel.basicAck(8, false);则仅表示deliveryTag=8的消息已经成功处理。 - */ - // channel.basicAck(deliveryTag, false); - - /* - consumer处理失败后,例如:有值为5,6,7,8 deliveryTag的投递。 - 如果channel.basicNack(8, true, true);表示deliveryTag=8之前未确认的消息都处理失败且将这些消息重新放回队列中。 - 如果channel.basicNack(8, true, false);表示deliveryTag=8之前未确认的消息都处理失败且将这些消息直接丢弃。 - 如果channel.basicNack(8, false, true);表示deliveryTag=8的消息处理失败且将该消息重新放回队列。 - 如果channel.basicNack(8, false, false);表示deliveryTag=8的消息处理失败且将该消息直接丢弃。 - */ - // channel.basicNack(deliveryTag, false, true); - /* - spring.rabbitmq.listener.simple.acknowledge-mode=manual - 手动ack确认,抛出异常 - 1、不重试 - 2、消息不会从队列移除 - 3、消费端重启,再次消费消息 - - spring.rabbitmq.listener.simple.acknowledge-mode=auto // auto是默认值 - 自动ack确认,,抛出异常 - 1、一直重试 - - */ - - // if (order01.getId() > 100){ - // // 签收 - // channel.basicAck(deliveryTag, false); - // }else { - // // acknowledge-mode=manual - // // 重回队列,会一直重试,retry.max-attempts无效 - // channel.basicNack(deliveryTag, false, true); - - // acknowledge-mode=auto 抛异常导致的重试,重试期间阻塞消费端消费其他消息 - throw new RuntimeException("抛异常"); - // } - + ) + public void basicDataListener(Message message, Channel channel) throws Exception { + MessageHeaders headers = message.getHeaders(); + Object deliveryTagObject = headers.get(AmqpHeaders.DELIVERY_TAG); + String payload = message.getPayload(); + System.out.println("收到福讯账单mq消息"); + System.out.println(deliveryTagObject); + System.out.println(payload); + + + //手动ack + // 签收 + //channel.basicAck(deliveryTag, false); } -} + } diff --git a/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverDead.java b/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverDead.java index d719b6d7..7b895559 100644 --- a/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverDead.java +++ b/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverDead.java @@ -1,65 +1,65 @@ -package com.coq.rabbitmq.sc01.conusmer; - -import com.rabbitmq.client.Channel; -import org.springframework.amqp.core.ExchangeTypes; -import org.springframework.amqp.rabbit.annotation.*; -import org.springframework.amqp.support.AmqpHeaders; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.stereotype.Component; - -import static com.coq.rabbitmq.sc01.config.RabbitmqConstant.*; - -@Component -public class ReceiverDead { - - /** - * 死信队列消费者,由于消息体内容千变万化,不能使用@Payload Object接受,@Payload只接受javabean - */ - @RabbitListener( - bindings = @QueueBinding( - value = @Queue(value = DEAD_QUEUE, - durable = "true", - autoDelete = "false"), - exchange = @Exchange(value = DEAD_EXCHANGE, - durable = "true", - type = ExchangeTypes.DIRECT, - ignoreDeclarationExceptions = "true"), - key = {DEAD_KEY}) - ) - @RabbitHandler - public void onDeadQueueMessage(Message message, Channel channel) throws Exception{ - try { - Object dataObj = message.getPayload(); - System.out.println("死信队列,dataObj:"+dataObj.toString()); - - MessageHeaders headers = message.getHeaders(); - Object deliveryTag = headers.get(AmqpHeaders.DELIVERY_TAG); - System.out.println("死信队列,deliveryTag: " + deliveryTag.toString()); - channel.basicAck((Long)deliveryTag, false); - - // Object xDeath = headers.get("x-death"); - // if (xDeath instanceof ArrayList){ - // ArrayList arrayList = (ArrayList)xDeath; - // System.out.println(arrayList.toString()); - // for (Object elem:arrayList){ - // if (elem instanceof Map){ - // String fromExchange = ((Map) elem).get("exchange").toString(); - // String fromRoutingKeys = ((Map) elem).get("routing-keys").toString(); - // String fromQueue = ((Map) elem).get("queue").toString(); - // Object intoDeadExchangeTime = ((Map) elem).get("time"); - // } - // } - // arrayList.add(new HashMap(){{put("aaa", 11);}}); - // } - // headers.put("x-death", xDeath); - - //channel.basicAck((Long)headers.get(AmqpHeaders.DELIVERY_TAG), false); - // channel.basicNack((Long)headers.get(AmqpHeaders.DELIVERY_TAG), false, true); - } catch (Exception e){ - // 不能出错 - System.out.println("死信队列异常"); - e.printStackTrace(); - } - } -} +// package com.coq.rabbitmq.sc01.conusmer; +// +// import com.rabbitmq.client.Channel; +// import org.springframework.amqp.core.ExchangeTypes; +// import org.springframework.amqp.rabbit.annotation.*; +// import org.springframework.amqp.support.AmqpHeaders; +// import org.springframework.messaging.Message; +// import org.springframework.messaging.MessageHeaders; +// import org.springframework.stereotype.Component; +// +// import static com.coq.rabbitmq.sc01.config.RabbitmqConstant.*; +// +// @Component +// public class ReceiverDead { +// +// /** +// * 死信队列消费者,由于消息体内容千变万化,不能使用@Payload Object接受,@Payload只接受javabean +// */ +// @RabbitListener( +// bindings = @QueueBinding( +// value = @Queue(value = DEAD_QUEUE, +// durable = "true", +// autoDelete = "false"), +// exchange = @Exchange(value = DEAD_EXCHANGE, +// durable = "true", +// type = ExchangeTypes.DIRECT, +// ignoreDeclarationExceptions = "true"), +// key = {DEAD_KEY}) +// ) +// @RabbitHandler +// public void onDeadQueueMessage(Message message, Channel channel) throws Exception{ +// try { +// Object dataObj = message.getPayload(); +// System.out.println("死信队列,dataObj:"+dataObj.toString()); +// +// MessageHeaders headers = message.getHeaders(); +// Object deliveryTag = headers.get(AmqpHeaders.DELIVERY_TAG); +// System.out.println("死信队列,deliveryTag: " + deliveryTag.toString()); +// channel.basicAck((Long)deliveryTag, false); +// +// // Object xDeath = headers.get("x-death"); +// // if (xDeath instanceof ArrayList){ +// // ArrayList arrayList = (ArrayList)xDeath; +// // System.out.println(arrayList.toString()); +// // for (Object elem:arrayList){ +// // if (elem instanceof Map){ +// // String fromExchange = ((Map) elem).get("exchange").toString(); +// // String fromRoutingKeys = ((Map) elem).get("routing-keys").toString(); +// // String fromQueue = ((Map) elem).get("queue").toString(); +// // Object intoDeadExchangeTime = ((Map) elem).get("time"); +// // } +// // } +// // arrayList.add(new HashMap(){{put("aaa", 11);}}); +// // } +// // headers.put("x-death", xDeath); +// +// //channel.basicAck((Long)headers.get(AmqpHeaders.DELIVERY_TAG), false); +// // channel.basicNack((Long)headers.get(AmqpHeaders.DELIVERY_TAG), false, true); +// } catch (Exception e){ +// // 不能出错 +// System.out.println("死信队列异常"); +// e.printStackTrace(); +// } +// } +// } diff --git a/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverReturnQueueTimes.java b/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverReturnQueueTimes.java index c3125059..e1cb16d0 100644 --- a/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverReturnQueueTimes.java +++ b/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverReturnQueueTimes.java @@ -1,93 +1,93 @@ -package com.coq.rabbitmq.sc01.conusmer; - -import com.coq.rabbitmq.sc01.bean.Order01; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.rabbitmq.client.Channel; -import lombok.extern.slf4j.Slf4j; -import org.springframework.amqp.core.ExchangeTypes; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.rabbit.annotation.*; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.Map; - -import static com.coq.rabbitmq.sc01.config.RabbitmqConstant.DEAD_EXCHANGE; -import static com.coq.rabbitmq.sc01.config.RabbitmqConstant.DEAD_KEY; - -@Slf4j -@Component -public class ReceiverReturnQueueTimes { - - @Autowired - RabbitTemplate rabbitTemplate; - - @RabbitListener( - bindings = @QueueBinding( - value = @Queue(value = "queue-return-queue-times", - durable = "true", - autoDelete = "false", - arguments = { - @Argument(name = "x-dead-letter-exchange", value = DEAD_EXCHANGE), - @Argument(name = "x-dead-letter-routing-key", value = DEAD_KEY) - } - ), - exchange = @Exchange(value = "exchange-return-queue-times", - durable = "true", - type = ExchangeTypes.DIRECT, - autoDelete = "false", - ignoreDeclarationExceptions = "true"), - key = {"key-return-queue-times"}), - errorHandler = "customListenerErrorHandler" - ) - @RabbitHandler - public void onOrderMessage(Message message, Channel channel) throws Exception{ - - byte[] body = message.getBody(); - ObjectMapper objectMapper = new ObjectMapper(); - Order01 order01 = objectMapper.readValue(body, Order01.class); - System.out.println("消费端:"+order01.toString()); - - MessageProperties messageProperties = message.getMessageProperties(); - - Long deliveryTag = messageProperties.getDeliveryTag(); - System.out.println("消费端deliveryTag:"+deliveryTag); - - String messageId = messageProperties.getMessageId(); - System.out.println("消费端messageId:"+messageId); - - // 发送端需在MessagePostProcessor添加return-queues-times - Map headers = messageProperties.getHeaders(); - Integer returnQueuesTimes = Integer.parseInt(headers.get("return-queues-times").toString()); - System.out.println("消费端returnQueuesTimes:"+returnQueuesTimes); - - - if (order01.getId() > 100){ - // 签收 - channel.basicAck(deliveryTag, false); - }else { - if (returnQueuesTimes < 10){ - channel.basicNack(deliveryTag, false, true); - - /** - * spring.rabbitmq.listener.simple.acknowledge-mode=manual - * - * org.springframework.messaging.Message 修改header会报错 - * org.springframework.amqp.core.Message 设置headers,重回队列后无效 - * - * header 不能承载 重试次数 ? - */ - messageProperties.setHeader("return-queues-times", ++returnQueuesTimes); - } else { - rabbitTemplate.convertAndSend(DEAD_EXCHANGE, DEAD_KEY, objectMapper.writeValueAsString(order01)); - } - - // acknowledge-mode=auto 抛异常导致的重试,重试期间阻塞消费端消费其他消息 - // throw new RuntimeException("抛异常"); - } - - } - -} +// package com.coq.rabbitmq.sc01.conusmer; +// +// import com.coq.rabbitmq.sc01.bean.Order01; +// import com.fasterxml.jackson.databind.ObjectMapper; +// import com.rabbitmq.client.Channel; +// import lombok.extern.slf4j.Slf4j; +// import org.springframework.amqp.core.ExchangeTypes; +// import org.springframework.amqp.core.Message; +// import org.springframework.amqp.core.MessageProperties; +// import org.springframework.amqp.rabbit.annotation.*; +// import org.springframework.amqp.rabbit.core.RabbitTemplate; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.stereotype.Component; +// +// import java.util.Map; +// +// import static com.coq.rabbitmq.sc01.config.RabbitmqConstant.DEAD_EXCHANGE; +// import static com.coq.rabbitmq.sc01.config.RabbitmqConstant.DEAD_KEY; +// +// @Slf4j +// @Component +// public class ReceiverReturnQueueTimes { +// +// @Autowired +// RabbitTemplate rabbitTemplate; +// +// @RabbitListener( +// bindings = @QueueBinding( +// value = @Queue(value = "queue-return-queue-times", +// durable = "true", +// autoDelete = "false", +// arguments = { +// @Argument(name = "x-dead-letter-exchange", value = DEAD_EXCHANGE), +// @Argument(name = "x-dead-letter-routing-key", value = DEAD_KEY) +// } +// ), +// exchange = @Exchange(value = "exchange-return-queue-times", +// durable = "true", +// type = ExchangeTypes.DIRECT, +// autoDelete = "false", +// ignoreDeclarationExceptions = "true"), +// key = {"key-return-queue-times"}), +// errorHandler = "customListenerErrorHandler" +// ) +// @RabbitHandler +// public void onOrderMessage(Message message, Channel channel) throws Exception{ +// +// byte[] body = message.getBody(); +// ObjectMapper objectMapper = new ObjectMapper(); +// Order01 order01 = objectMapper.readValue(body, Order01.class); +// System.out.println("消费端:"+order01.toString()); +// +// MessageProperties messageProperties = message.getMessageProperties(); +// +// Long deliveryTag = messageProperties.getDeliveryTag(); +// System.out.println("消费端deliveryTag:"+deliveryTag); +// +// String messageId = messageProperties.getMessageId(); +// System.out.println("消费端messageId:"+messageId); +// +// // 发送端需在MessagePostProcessor添加return-queues-times +// Map headers = messageProperties.getHeaders(); +// Integer returnQueuesTimes = Integer.parseInt(headers.get("return-queues-times").toString()); +// System.out.println("消费端returnQueuesTimes:"+returnQueuesTimes); +// +// +// if (order01.getId() > 100){ +// // 签收 +// channel.basicAck(deliveryTag, false); +// }else { +// if (returnQueuesTimes < 10){ +// channel.basicNack(deliveryTag, false, true); +// +// /** +// * spring.rabbitmq.listener.simple.acknowledge-mode=manual +// * +// * org.springframework.messaging.Message 修改header会报错 +// * org.springframework.amqp.core.Message 设置headers,重回队列后无效 +// * +// * header 不能承载 重试次数 ? +// */ +// messageProperties.setHeader("return-queues-times", ++returnQueuesTimes); +// } else { +// rabbitTemplate.convertAndSend(DEAD_EXCHANGE, DEAD_KEY, objectMapper.writeValueAsString(order01)); +// } +// +// // acknowledge-mode=auto 抛异常导致的重试,重试期间阻塞消费端消费其他消息 +// // throw new RuntimeException("抛异常"); +// } +// +// } +// +// } diff --git a/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverSpecific.java b/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverSpecific.java index 14f654e6..298206e8 100644 --- a/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverSpecific.java +++ b/project/rabbitmq/springboot-consumer01/src/main/java/com/coq/rabbitmq/sc01/conusmer/ReceiverSpecific.java @@ -1,54 +1,54 @@ -package com.coq.rabbitmq.sc01.conusmer; - -import com.rabbitmq.client.Channel; -import org.springframework.amqp.rabbit.annotation.*; -import org.springframework.amqp.support.AmqpHeaders; -import org.springframework.messaging.handler.annotation.Headers; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.stereotype.Component; - -import java.util.Map; - -@Component -public class ReceiverSpecific { - - /* - 使用控制台像特定的 consumer 发送消息,rabbitmq不提供此功能,但是可以一定的技巧实现 - 1、消费端 @RabbitListener 的 @Queue 注解添加两个属性 - durable = "false", // 不持久化 - autoDelete = "true" // 队列不再使用时自动删除,关闭程序时会自动删除 - 2、确保此队列的消费者只有一个 - -控制台发送消息: - 1、properties 添加 content_type=text/plain - 2、Payload填写:{"id": 12345, "name": "中文名"} - 3、点击发送 - */ - @RabbitListener(bindings = @QueueBinding( - value = @Queue(value = "specific-listener-queue", - // exclusive = "true", - durable = "false", - autoDelete = "true" - // durable = "true" - ), - exchange = @Exchange(value = "specific-listener-exchange", - // durable = "true", - durable = "false", - type = "topic", - ignoreDeclarationExceptions = "true"), - key = {"specific-listener-key"} - )) - @RabbitHandler - public void onOrderMessage(@Payload String str, - Channel channel, - @Headers Map headers) throws Exception{ - - System.out.println("消费端:"+str.toString()); - Long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG); - System.out.println("deliveryTag "+deliveryTag); - - // 签收信息 - channel.basicAck(deliveryTag, false); - } - -} +// package com.coq.rabbitmq.sc01.conusmer; +// +// import com.rabbitmq.client.Channel; +// import org.springframework.amqp.rabbit.annotation.*; +// import org.springframework.amqp.support.AmqpHeaders; +// import org.springframework.messaging.handler.annotation.Headers; +// import org.springframework.messaging.handler.annotation.Payload; +// import org.springframework.stereotype.Component; +// +// import java.util.Map; +// +// @Component +// public class ReceiverSpecific { +// +// /* +// 使用控制台像特定的 consumer 发送消息,rabbitmq不提供此功能,但是可以一定的技巧实现 +// 1、消费端 @RabbitListener 的 @Queue 注解添加两个属性 +// durable = "false", // 不持久化 +// autoDelete = "true" // 队列不再使用时自动删除,关闭程序时会自动删除 +// 2、确保此队列的消费者只有一个 +// +// 控制台发送消息: +// 1、properties 添加 content_type=text/plain +// 2、Payload填写:{"id": 12345, "name": "中文名"} +// 3、点击发送 +// */ +// @RabbitListener(bindings = @QueueBinding( +// value = @Queue(value = "specific-listener-queue", +// // exclusive = "true", +// durable = "false", +// autoDelete = "true" +// // durable = "true" +// ), +// exchange = @Exchange(value = "specific-listener-exchange", +// // durable = "true", +// durable = "false", +// type = "topic", +// ignoreDeclarationExceptions = "true"), +// key = {"specific-listener-key"} +// )) +// @RabbitHandler +// public void onOrderMessage(@Payload String str, +// Channel channel, +// @Headers Map headers) throws Exception{ +// +// System.out.println("消费端:"+str.toString()); +// Long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG); +// System.out.println("deliveryTag "+deliveryTag); +// +// // 签收信息 +// channel.basicAck(deliveryTag, false); +// } +// +// } diff --git a/project/rabbitmq/springboot-consumer01/src/main/resources/application.properties b/project/rabbitmq/springboot-consumer01/src/main/resources/application.properties index 53d7a75c..d831c740 100644 --- a/project/rabbitmq/springboot-consumer01/src/main/resources/application.properties +++ b/project/rabbitmq/springboot-consumer01/src/main/resources/application.properties @@ -1,26 +1,26 @@ server.port=9091 -spring.rabbitmq.addresses=127.0.0.1:5672 -spring.rabbitmq.username=guest -spring.rabbitmq.password=guest -spring.rabbitmq.virtual-host=/ -spring.rabbitmq.connection-timeout=1500000000 +spring.rabbitmq.addresses=rabbitmq-iot.onewo.com:5672 +spring.rabbitmq.username=cq_b11 +spring.rabbitmq.password=GIL16JM7yZ8F +spring.rabbitmq.virtual-host=/ihw +#spring.rabbitmq.connection-timeout=1500000000 #最小消费者数量 -spring.rabbitmq.listener.simple.concurrency=1 +#spring.rabbitmq.listener.simple.concurrency=1 #最大消费者数量,如果消息一直重试,消费者会增加 -spring.rabbitmq.listener.simple.max-concurrency=2 +#spring.rabbitmq.listener.simple.max-concurrency=2 # ACK #none不发送ack。manual手工签收。auto自动ack(默认值) #spring.rabbitmq.listener.simple.acknowledge-mode=manual # 重试 -#是否开启消费者重试,消费端代码抛出异常,则重试 -spring.rabbitmq.listener.simple.retry.enabled=true -#最大重试次数 -spring.rabbitmq.listener.simple.retry.max-attempts=4 -#重试间隔时间(单位毫秒) -spring.rabbitmq.listener.simple.retry.initial-interval=20000 -#重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列) -#如果没加死信队列,即使设置为false,重试到达最大次数后也会被丢弃 -spring.rabbitmq.listener.simple.default-requeue-rejected=false \ No newline at end of file +##是否开启消费者重试,消费端代码抛出异常,则重试 +#spring.rabbitmq.listener.simple.retry.enabled=true +##最大重试次数 +#spring.rabbitmq.listener.simple.retry.max-attempts=4 +##重试间隔时间(单位毫秒) +#spring.rabbitmq.listener.simple.retry.initial-interval=20000 +##重试次数超过上面的设置之后是否丢弃(false不丢弃时需要写相应代码将该消息加入死信队列) +##如果没加死信队列,即使设置为false,重试到达最大次数后也会被丢弃 +#spring.rabbitmq.listener.simple.default-requeue-rejected=false \ No newline at end of file diff --git a/project/rabbitmq/springboot-consumer01/src/test/java/com/coq/rabbitmq/sc01/ApplicationTests.java b/project/rabbitmq/springboot-consumer01/src/test/java/com/coq/rabbitmq/sc01/ApplicationTests.java index fbd14bb7..3f3c9304 100644 --- a/project/rabbitmq/springboot-consumer01/src/test/java/com/coq/rabbitmq/sc01/ApplicationTests.java +++ b/project/rabbitmq/springboot-consumer01/src/test/java/com/coq/rabbitmq/sc01/ApplicationTests.java @@ -1,27 +1,27 @@ -package com.coq.rabbitmq.sc01; - -import com.coq.rabbitmq.sc01.conusmer.ReceiverSpecific; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -import java.util.HashMap; - -@RunWith(SpringRunner.class) -@SpringBootTest -public class ApplicationTests { - - @Autowired - ReceiverSpecific receiverSpecific; - - @Test - public void contextLoads() throws Exception { - - receiverSpecific.onOrderMessage("{\"id\": 12345, \"name\": \"中文名\"}", null, new HashMap<>()); - - - } - -} +// package com.coq.rabbitmq.sc01; +// +// import com.coq.rabbitmq.sc01.conusmer.ReceiverSpecific; +// import org.junit.Test; +// import org.junit.runner.RunWith; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.context.SpringBootTest; +// import org.springframework.test.context.junit4.SpringRunner; +// +// import java.util.HashMap; +// +// @RunWith(SpringRunner.class) +// @SpringBootTest +// public class ApplicationTests { +// +// @Autowired +// ReceiverSpecific receiverSpecific; +// +// @Test +// public void contextLoads() throws Exception { +// +// receiverSpecific.onOrderMessage("{\"id\": 12345, \"name\": \"中文名\"}", null, new HashMap<>()); +// +// +// } +// +// } diff --git a/project/rabbitmq/springboot-producer01/pom.xml b/project/rabbitmq/springboot-producer01/pom.xml index 9a3f2e06..43b989e3 100644 --- a/project/rabbitmq/springboot-producer01/pom.xml +++ b/project/rabbitmq/springboot-producer01/pom.xml @@ -40,6 +40,20 @@ spring-boot-starter-amqp + + com.alibaba + fastjson + 1.2.78 + + + + + commons-io + commons-io + 2.11.0 + + + org.projectlombok lombok diff --git a/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/config/MyRabbitmqConfig.java b/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/config/MyRabbitmqConfig.java index 7fb50aed..051094f4 100644 --- a/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/config/MyRabbitmqConfig.java +++ b/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/config/MyRabbitmqConfig.java @@ -4,6 +4,7 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.support.CorrelationData; import org.springframework.context.annotation.Bean; @@ -17,6 +18,14 @@ public class MyRabbitmqConfig { // return new RabbitTemplate(); // } + @Bean + public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) { + RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory); + // 必须设置为true,不然在spring启动时不会注入到ioc + rabbitAdmin.setAutoStartup(true); + return rabbitAdmin; + } + /** * 设置回调 * @param connectionFactory @@ -34,7 +43,9 @@ RabbitTemplate callbackRabbitTemplate(ConnectionFactory connectionFactory){ final RabbitTemplate.ConfirmCallback confirmCallback= new RabbitTemplate.ConfirmCallback(){ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause){ - System.out.println("ConfirmCallback##correlationData "+correlationData.toString()); + if (correlationData != null) { + System.out.println("ConfirmCallback##correlationData "+correlationData.toString()); + } System.out.println("ConfirmCallback##ack " +ack); System.out.println("ConfirmCallback##cause " +cause); if (!ack){ @@ -78,4 +89,5 @@ public Message postProcessMessage(Message message) throws AmqpException { } }; } + } diff --git a/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/notuserspringcomsumer/Consumer01.java b/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/notuserspringcomsumer/Consumer01.java new file mode 100644 index 00000000..d761cfc5 --- /dev/null +++ b/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/notuserspringcomsumer/Consumer01.java @@ -0,0 +1,27 @@ +package com.coq.rabbitmq.sp01.notuserspringcomsumer; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; + +import java.io.IOException; + +public class Consumer01 extends DefaultConsumer { + + + private Channel channel; + + public Consumer01(Channel channel) { + super(channel); + this.channel = channel; + } + + @Override + public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { + System.err.println("-----------consume message-11111---------body: " + new String(body)); + channel.basicAck(envelope.getDeliveryTag(), false); + } + + +} diff --git a/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/notuserspringcomsumer/Consumer02.java b/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/notuserspringcomsumer/Consumer02.java new file mode 100644 index 00000000..ad1ceddc --- /dev/null +++ b/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/notuserspringcomsumer/Consumer02.java @@ -0,0 +1,27 @@ +package com.coq.rabbitmq.sp01.notuserspringcomsumer; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; + +import java.io.IOException; + +public class Consumer02 extends DefaultConsumer { + + + private Channel channel; + + public Consumer02(Channel channel) { + super(channel); + this.channel = channel; + } + + @Override + public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { + System.err.println("-----------consume message-222222222222---------body: " + new String(body)); + channel.basicAck(envelope.getDeliveryTag(), false); + } + + +} diff --git a/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/notuserspringcomsumer/CreateConsumerController.java b/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/notuserspringcomsumer/CreateConsumerController.java new file mode 100644 index 00000000..68a34030 --- /dev/null +++ b/project/rabbitmq/springboot-producer01/src/main/java/com/coq/rabbitmq/sp01/notuserspringcomsumer/CreateConsumerController.java @@ -0,0 +1,142 @@ +package com.coq.rabbitmq.sp01.notuserspringcomsumer; + + +import com.rabbitmq.client.Channel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + *

+ * ip网段对应地址 前端控制器 + *

+ * + * @author chenpiqian + * @since 2019-04-24 + */ +@RequestMapping("/test") +@Slf4j +@RestController +public class CreateConsumerController { + + public Map connMap = new ConcurrentHashMap<>(); + public Map adminMap = new ConcurrentHashMap<>(); + + // String exchangeName = "test.topic"; + // String routingKey = "test.device.test"; + + private CachingConnectionFactory connectionFactory(String username, String password, String virtualHost, String address) throws Exception{ + CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); + + connectionFactory.setUsername(username); + connectionFactory.setPassword(password); + connectionFactory.setVirtualHost(virtualHost); + + connectionFactory.setPublisherConfirms(true); + + //该方法配置多个host,在当前连接host down掉的时候会自动去重连后面的host + connectionFactory.setAddresses(address); + return connectionFactory; + } + + @GetMapping("/init/connection-admin") + public String initConnectionAdmin(String username, String password, String virtualHost, String address) throws Exception { + String key = address + virtualHost; + synchronized (this) { + if (!connMap.containsKey(key)) { + CachingConnectionFactory cachingConnectionFactory = connectionFactory(username, password, virtualHost, address); + connMap.put(key, cachingConnectionFactory); + } + if (!adminMap.containsKey(key)) { + CachingConnectionFactory cachingConnectionFactory = connMap.get(key); + RabbitAdmin rabbitAdmin = new RabbitAdmin(cachingConnectionFactory); + adminMap.put(key, rabbitAdmin); + } + } + return "OK"; + } + + + @GetMapping("/exchange") + public String exchange(String key, String exchangeName) throws Exception{ + TopicExchange topicExchange = new TopicExchange(exchangeName, true, false); + RabbitAdmin rabbitAdmin = adminMap.get(key); + rabbitAdmin.declareExchange(topicExchange); + return "OK"; + } + + @GetMapping("/listener") + public String listener(String key, String exchangeName, String routingKey, String testDeviceQueueName) throws Exception{ + // queue + HashMap arguments = new HashMap<>(); + arguments.put("x-message-ttl", 10000); + Queue testDeviceQueue = new Queue(testDeviceQueueName, true, false, false, arguments); + RabbitAdmin rabbitAdmin = adminMap.get(key); + rabbitAdmin.declareQueue(testDeviceQueue); + + // binding + TopicExchange topicExchange = new TopicExchange(exchangeName); + Binding binding = BindingBuilder.bind(testDeviceQueue).to(topicExchange).with(routingKey); + rabbitAdmin.declareBinding(binding); + CachingConnectionFactory connectionFactory = connMap.get(key); + synchronized (this) { + Channel channel = connectionFactory.createConnection().createChannel(false); + long count = channel.consumerCount(testDeviceQueueName); + System.out.println("######listener##consumerCount="+count); + if (count == 0) { + channel.basicConsume(testDeviceQueueName, new Consumer01(channel)); + } + } + return "OK"; + } + + @GetMapping("/listener2") + public String listener2(String key, String exchangeName, String routingKey, String testDeviceQueueName) throws Exception{ + // queue + HashMap arguments = new HashMap<>(); + arguments.put("x-message-ttl", 10000); + Queue testDeviceQueue = new Queue(testDeviceQueueName, true, false, false, arguments); + RabbitAdmin rabbitAdmin = adminMap.get(key); + rabbitAdmin.declareQueue(testDeviceQueue); + + // binding + TopicExchange topicExchange = new TopicExchange(exchangeName); + Binding binding = BindingBuilder.bind(testDeviceQueue).to(topicExchange).with(routingKey); + rabbitAdmin.declareBinding(binding); + + CachingConnectionFactory connectionFactory = connMap.get(key); + synchronized (this) { + Channel channel = connectionFactory.createConnection().createChannel(false); + long count = channel.consumerCount(testDeviceQueueName); + System.out.println("######listener2##consumerCount="+count); + if (count == 0) { + channel.basicConsume(testDeviceQueueName, new Consumer02(channel)); + } + } + return "OK"; + } + + + // @GetMapping("/send") + // public String send() throws Exception{ + // rabbitTemplate.convertAndSend(exchangeName, routingKey, "dafdasdfa3fsdfsfsdf", new MessagePostProcessor() { + // @Override + // public Message postProcessMessage(Message message) throws AmqpException { + // log.info("##############msg={}", message); + // return message; + // } + // }); + // return "OK"; + // } +} diff --git a/project/rabbitmq/springboot-producer01/src/main/java/com/vanrui/dto/EmpExternalDTO.java b/project/rabbitmq/springboot-producer01/src/main/java/com/vanrui/dto/EmpExternalDTO.java new file mode 100644 index 00000000..2a270013 --- /dev/null +++ b/project/rabbitmq/springboot-producer01/src/main/java/com/vanrui/dto/EmpExternalDTO.java @@ -0,0 +1,39 @@ +package com.vanrui.dto; + +import java.io.Serializable; + +/** + * 修改业主的扩展信息 + * @author yangliu + * @date 2019/1/23 14:16 + */ +public class EmpExternalDTO implements Serializable { + + private String spatialCodes; + + private int action; + + public String getSpatialCode() { + return spatialCodes; + } + + public void setSpatialCode(String spatialCodes) { + this.spatialCodes = spatialCodes; + } + + public int getAction() { + return action; + } + + public void setAction(int action) { + this.action = action; + } + + @Override + public String toString() { + return "EmpExternalDTO{" + + "spatialCode=" + spatialCodes + + ", action=" + action + + '}'; + } +} diff --git a/project/rabbitmq/springboot-producer01/src/main/java/com/vanrui/dto/JSONUtil.java b/project/rabbitmq/springboot-producer01/src/main/java/com/vanrui/dto/JSONUtil.java new file mode 100644 index 00000000..d57d7ede --- /dev/null +++ b/project/rabbitmq/springboot-producer01/src/main/java/com/vanrui/dto/JSONUtil.java @@ -0,0 +1,100 @@ +package com.vanrui.dto; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; + +import java.util.List; +import java.util.Map; + +public class JSONUtil { + + /* + * 直接从json字符串中获取对应key的value值 {"id":8,"name":"小明"} + */ + public static String getVluseByJsonStr(String jsonStr, String key) { + // 本方法大概耗时22132纳秒,性能上完全超越很多json工具类 + char[] strs = jsonStr.toCharArray(); + String result = ""; + for (int i = jsonStr.indexOf(key) + key.length() + 2; i < strs.length; i++) { + if (strs[i] == ',' || strs[i] == '}') { + return result; + } + if (strs[i] != '"') { + result += strs[i]; + } + } + return result; + + } + + /* + * json字符串转换成map + */ + @SuppressWarnings("unchecked") + public static Map jsonStrToMap(String jsonStr) { + + JSONObject jSONObject = JSONObject.parseObject(jsonStr); + + Map itemMap = JSONObject.toJavaObject(jSONObject, Map.class); + + return itemMap; + + } + + /* + * json字符串转换成JSONObject + */ + public static JSONObject jsonStrToJSONObject(String jsonStr) { + + JSONObject jSONObject = JSONObject.parseObject(jsonStr); + + return jSONObject; + + } + + /* + * 将List转换成JSONArray + */ + public static JSONArray listToJSONArray(List list) { + + JSONArray jSONArray = JSONArray.parseArray(JSON.toJSONString(list)); + + return jSONArray; + + } + + + + // List 转 json string + + public static String listToJSONStr(List list) { + return JSON.toJSONString(list); + } + + // Map 转 json string + + public static String MapToJSONStr(Map map) { + return JSON.toJSONString(map); + } + + + /* + * 将JavaBean转换成JSON字符串 + */ + public static String JavaBeantoJSONString(Object obj) { + return JSON.toJSONString(obj); + } + + + /* + * 将JavaBean转换成JSON字 + */ + public static JSONObject JavaBeantoJSON(Object obj) { + String jsonStr = JSON.toJSONString(obj); + return JSONObject.parseObject(jsonStr); + + + } + +} diff --git a/project/rabbitmq/springboot-producer01/src/main/java/com/vanrui/dto/RabbitDTO.java b/project/rabbitmq/springboot-producer01/src/main/java/com/vanrui/dto/RabbitDTO.java new file mode 100644 index 00000000..1183a0ac --- /dev/null +++ b/project/rabbitmq/springboot-producer01/src/main/java/com/vanrui/dto/RabbitDTO.java @@ -0,0 +1,113 @@ +package com.vanrui.dto; + +import java.io.Serializable; + +public class RabbitDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + public static final String MATERIAL_DIRECT_EXCHANGE = "materialDirectExchange"; + + public static final String BUSINESS_VISITOR_EXCHANGE = "businessVisitorExchange"; + + public static final String YQ_VISITOR_EXCHANGE = "yqVisitRecordExchange"; + + public static final String YQ_TEMP_EXCHANGE = "yqTempExchange"; + + public static final String YQ_HOUSE_CONFIG_EXCHANGE = "yqHouseConfigExchange"; + + public static final String YQ_EMP_CONFIG_EXCHANGE = "yqEmpConfigExchange"; + + public static final String PROJECT_ROUTING_KEY = "projectRoutingKey"; + + public static final String ENCLOSURE_ROUTING_KEY = "enclosureRoutingKey"; + + public static final String EMP_ROUTING_KEY = "empRoutingKey"; + + public static final String BLACK_ROUTING_KEY = "blackRoutingKey"; + + public static final String DEVICE_ROUTING_KEY = "deviceRoutingKey"; + + public static final String FAMILY_ROUTING_KEY = "familyRoutingKey"; + + public static final String WORK_ROUTING_KEY = "workRoutingKey"; + + public static final String VISITOR_ROUTING_KEY = "visitorRoutingKey"; + + public static final String DAILY_VISITOR_ROUTING_KEY = "dailyVisitorRoutingKey"; + + public static final String CARD_ROUTING_KEY = "cardRoutingKey"; + + public static final String BUSINESS_VISITOR_KEY = "businessVisitorKey"; + + public static final String YQ_VISITOR_ROUTING_KEY = "yqVisitorKey"; + + public static final String YQ_TEMP_ROUTING_KEY = "yqTempKey"; + + public static final String YQ_HOUSE_ROUTING_KEY = "yqHouseConfigKey"; + + public static final String YQ_EMP_ROUTING_KEY = "yqEmpConfigKey"; + + public static final int ADD = 1; + + public static final int UPDATE = 2; + + public static final int DELETE = 3; + + public static final int STATUS = 4; + + private Long primaryKey; + + private Integer action; + + private String external; + + public String getExternal() { + return external; + } + + public void setExternal(String external) { + this.external = external; + } + + public Long getPrimaryKey() { + return primaryKey; + } + + public RabbitDTO() { + super(); + } + + public RabbitDTO(Long primaryKey, Integer action) { + this.primaryKey = primaryKey; + this.action = action; + } + + public void setPrimaryKey(Long primaryKey) { + this.primaryKey = primaryKey; + } + + public Integer getAction() { + return action; + } + + public void setAction(Integer action) { + this.action = action; + } + + @Override + public String toString() { + if (external != null) { + return "RabbitDTO{" + + "primaryKey=" + primaryKey + + ", action=" + action + + ", external='" + external + '\'' + + '}'; + } else { + return "RabbitDTO{" + + "primaryKey=" + primaryKey + + ", action=" + action + + '}'; + } + } +} diff --git a/project/rabbitmq/springboot-producer01/src/main/resources/application.properties b/project/rabbitmq/springboot-producer01/src/main/resources/application.properties index 49dc0846..3dc5a912 100644 --- a/project/rabbitmq/springboot-producer01/src/main/resources/application.properties +++ b/project/rabbitmq/springboot-producer01/src/main/resources/application.properties @@ -1,13 +1,13 @@ -server.port=9090 - -spring.rabbitmq.addresses=127.0.0.1:5672 -spring.rabbitmq.username=guest -spring.rabbitmq.password=guest -spring.rabbitmq.virtual-host=/ -spring.rabbitmq.connection-timeout=1500000000 -# 开启confirm模式 -spring.rabbitmq.publisher-confirms=true -# 开启return模式 -spring.rabbitmq.publisher-returns=true -# mandatory:true:如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者。false:出现上述情形broker会直接将消息扔掉 -spring.rabbitmq.template.mandatory=true \ No newline at end of file +#server.port=9090 +# +#spring.rabbitmq.addresses=172.16.84.29:5672 +#spring.rabbitmq.username=admin +#spring.rabbitmq.password=123456 +#spring.rabbitmq.virtual-host=ryx +#spring.rabbitmq.connection-timeout=1500000000 +## 开启confirm模式 +##spring.rabbitmq.publisher-confirms=true +## 开启return模式 +##spring.rabbitmq.publisher-returns=true +## mandatory:true:如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者。false:出现上述情形broker会直接将消息扔掉 +##spring.rabbitmq.template.mandatory=true \ No newline at end of file diff --git a/project/rabbitmq/springboot-producer01/src/main/resources/application.yaml b/project/rabbitmq/springboot-producer01/src/main/resources/application.yaml new file mode 100644 index 00000000..7e1e4a07 --- /dev/null +++ b/project/rabbitmq/springboot-producer01/src/main/resources/application.yaml @@ -0,0 +1,21 @@ +server: + port: 9090 + +# mq都是用本地 +spring: + rabbitmq: + host: 127.0.0.1 + port: 5672 + username: guest + password: guest + publisher-confirms: true + virtual-host: / +## mq都是用本地 +#spring: +# rabbitmq: +# host: 172.16.84.29 +# port: 5672 +# username: admin +# password: 123456 +# publisher-confirms: true +# virtual-host: ryx diff --git a/project/rabbitmq/springboot-producer01/src/test/java/com/coq/rabbitmq/sp01/ApplicationTests.java b/project/rabbitmq/springboot-producer01/src/test/java/com/coq/rabbitmq/sp01/ApplicationTests.java index 970120c0..ba0a6783 100644 --- a/project/rabbitmq/springboot-producer01/src/test/java/com/coq/rabbitmq/sp01/ApplicationTests.java +++ b/project/rabbitmq/springboot-producer01/src/test/java/com/coq/rabbitmq/sp01/ApplicationTests.java @@ -1,56 +1,63 @@ -//package com.coq.commonproducer.springboot; -// -//import com.coq.commonproducer.springboot.producer.RabbitSender; -//import com.example.rabbitmqbean.MyOrder; -//import org.junit.Test; -//import org.junit.runner.RunWith; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.test.context.junit4.SpringRunner; -// -//import java.text.SimpleDateFormat; -// -//@RunWith(SpringRunner.class) -//@SpringBootTest -//public class ApplicationTests { -// -// @Test -// public void contextLoads() { -// } -// -// @Autowired -// private RabbitSender rabbitSender; -// -// private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); -// -// //@Test -// //public void testSender1() throws Exception { -// // Map properties = new HashMap<>(); -// // properties.put("number", "12345"); -// // properties.put("send_time", simpleDateFormat.format(new Date())); -// // rabbitSender.send("Hello RabbitMQ For Spring Boot!", properties); -// //} -// -// @Test -// public void testSender2() throws Exception { -// MyOrder order = new MyOrder("2", "第2222个订单"); -// rabbitSender.sendOrder(order); -// } -// -// -// @Test -// public void testSender3() throws Exception { -// MyOrder order = new MyOrder("3", "第333333个订单"); -// rabbitSender.sendOrder(order); -// } -// -// -// @Test -// public void testSenderNum() throws Exception { -// for (int i=1; i<50; i++){ -// MyOrder order = new MyOrder(String.valueOf(i), "第"+i+"个订单"); -// rabbitSender.sendOrder(order); -// } -// } -// -//} +package com.coq.rabbitmq.sp01; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.vanrui.dto.EmpExternalDTO; +import com.vanrui.dto.JSONUtil; +import com.vanrui.dto.RabbitDTO; +import org.apache.commons.io.FileUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.File; +import java.util.concurrent.TimeUnit; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class ApplicationTests { + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Test + public void testSender2() throws Exception { + File file = new File("C:\\Users\\chenpq05\\Desktop\\万科城id.txt"); + + String file1 = FileUtils.readFileToString(file);//前面两行是读取文件 + + JSONArray jsonArray = JSON.parseArray(file1); + + for (int i=0; i + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.1 + + + com.example + reactor0 + 0.0.1-SNAPSHOT + reactor0 + reactor0 + + 1.8 + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.boot + spring-boot-starter-test + + + + io.projectreactor + reactor-test + + + + org.projectlombok + lombok + provided + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/Reactor0Application.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/Reactor0Application.java new file mode 100644 index 00000000..85d000b0 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/Reactor0Application.java @@ -0,0 +1,21 @@ +package com.example.reactor0; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.util.List; +import java.util.stream.Collectors; + +@SpringBootApplication +public class Reactor0Application { + + public static void main(String[] args) { + SpringApplication.run(Reactor0Application.class, args); + List collect = Thread.getAllStackTraces() + .keySet() + .stream() + .collect(Collectors.toList()); + System.out.println(collect.toString()); + } + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/FluxDemo.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/FluxDemo.java new file mode 100644 index 00000000..b2104338 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/FluxDemo.java @@ -0,0 +1,99 @@ +package com.example.reactor0.a00; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; + +import java.util.Random; + +/** + * @author chenpq05 + * @since 2022/7/6 10:49 + */ +public class FluxDemo { + + @Test + public void test01() { + Random random = new Random(); + Flux.just(1, 2, 3, 4,5,6,7) + .map(i -> { + if (i == 3) { + throw new RuntimeException("i == 3"); + } + return i; + }).subscribe( + System.out::println, + System.out::println, + () -> System.out.println("complete")); + } + + @Test + public void test02() { + Flux.generate(() -> 0, (value, sink) -> { + if (value == 3) { + sink.complete(); + } else { + sink.next("value = " + value); + } + return value + 1; + }).subscribe( + System.out::println, + System.out::println, + () -> System.out.println("complete")); + } + + @Test + public void test03() { + Flux.generate(() -> 0, (value, sink) -> { + if (value == 3) { + sink.complete(); + } else { + sink.next("value = " + value); + } + return value + 1; + }).subscribe( + System.out::println, + System.out::println, + () -> System.out.println("complete")); + } + + @Test + public void test04() throws Exception { + Flux.range(0, 10) + .publishOn(Schedulers.immediate()) + .subscribe(System.out::println); + + Flux.range(0, 10) + .publishOn(Schedulers.single()) + .subscribe(System.out::println); + + Flux.range(0, 10) + .publishOn(Schedulers.elastic()) + .subscribe(System.out::println); + + Flux.range(0, 10) + .publishOn(Schedulers.parallel()) + .subscribe(System.out::println); + + Thread.currentThread().join(); + + } + + +} + + + + + + + + + + + + + + + + diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/MapFlatMap.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/MapFlatMap.java new file mode 100644 index 00000000..8c082b1d --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/MapFlatMap.java @@ -0,0 +1,74 @@ +package com.example.reactor0.a00; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * https://zhuanlan.zhihu.com/p/103686124 + */ +public class MapFlatMap { + List numList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8); + + @Test + public void test01(){ + List strList = numList.stream() + // 对元素进行转换,将整型转换为字符串 + .map(e -> String.valueOf(e)) + .collect(Collectors.toList()); + System.out.println(strList.toString()); + + } + + + private static class Klass { + private int field; + + public Klass(int field) { + this.field = field; + } + + @Override + public String toString() { + return "field=" + field; + } + } + + private static class KlassGroup { + private List group = new ArrayList<>(); + + public KlassGroup(Klass... objList) { + for (Klass item : objList) { + this.group.add(item); + } + } + + public List getKlassList() { + return group; + } + } + + List groupList = Arrays.asList( + new KlassGroup(new Klass(1), new Klass(2), new Klass(3)), + new KlassGroup(new Klass(4), new Klass(5), new Klass(6)), + new KlassGroup(new Klass(7), new Klass(8), new Klass(9)), + new KlassGroup(new Klass(10)) + ); + + @Test + public void test03(){ + List> ll = groupList.stream() + .map(e -> e.getKlassList()) + .collect(Collectors.toList()); + + List l = groupList.stream() + .flatMap(e -> e.getKlassList().stream()) + .collect(Collectors.toList()); + + + } + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/ReactorSnippets.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/ReactorSnippets.java new file mode 100644 index 00000000..9803de78 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/ReactorSnippets.java @@ -0,0 +1,78 @@ +package com.example.reactor0.a00; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; + +public class ReactorSnippets { + private static List words = Arrays.asList( + "the", + "quick", + "brown", + "fox", + "jumped", + "over", + "the", + "lazy", + "dog" + ); + + @Test + public void simpleCreation() { + Flux fewWords = Flux.just("Hello", "world"); + Flux manyWords = Flux.fromIterable(words); + + fewWords.subscribe(System.out::println); + System.out.println(); + manyWords.subscribe(System.out::println); + } + + + @Test + public void findingMissingLetter() { + Flux manyLetters = Flux + .fromIterable(words) + .flatMap(word -> Flux.fromArray(word.split(""))) + .concatWith(Mono.just("s")) + .distinct() + .sort() + .zipWith(Flux.range(1, Integer.MAX_VALUE), + (string, count) -> String.format("%2d. %s", count, string)); + manyLetters.subscribe(System.out::println); + } + + + @Test + public void shortCircuit() { + Flux helloPauseWorld = + Mono.just("Hello") + .concatWith(Mono.just("world") + .delaySubscription(Duration.ofSeconds(1))); + + helloPauseWorld.toStream().forEach(System.out::println); + } + + @Test + public void firstEmitting() { + Mono a = Mono.just("oops I'm late") + .delaySubscription(Duration.ofSeconds(1)); + Flux b = Flux.just("let's get", "the party", "started") + .delaySubscription(Duration.ofSeconds(2)); + + Flux.firstWithSignal(a, b) + .toIterable() + .forEach(System.out::println); + } + +} + + + + + + + diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/Tests01.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/Tests01.java new file mode 100644 index 00000000..b514b952 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a00/Tests01.java @@ -0,0 +1,169 @@ +package com.example.reactor0.a00; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.publisher.ConnectableFlux; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Optional; + +@Slf4j +class Tests01 { + + ArrayList elems = new ArrayList<>(); + + @Test + void contextLoads() { + Flux just = Flux.just(1, 2); + + Optional empty = Optional.empty(); + System.out.println(empty); + } + + @Test + void fn1() { + Publisher just = Mono.just(1); + // just.subscribe(); + } + + @Test + void fn2() { + ArrayList list = new ArrayList<>(); + Flux.just(1, 2, 4, 4) + .log() + .subscribe(e -> list.add(e)); + System.out.println(list); + } + + @Test + void fn3() { + Flux.just(1, 2, 3, 4) + .log() + .subscribe(new Subscriber() { + + // 订阅事件 + @Override + public void onSubscribe(Subscription subscription) { + log.info("#######{}", subscription); + } + + // 数据到达事件 + @Override + public void onNext(Integer integer) { + System.out.println("@@@@@@@{}" + integer); + } + + // 订阅异常 + @Override + public void onError(Throwable throwable) { + log.info("!!!!!!{}", throwable); + } + + // 订阅完成 + @Override + public void onComplete() { + log.info("ccccccccc"); + } + }); + } + + @Test + public void fn4(){ + ArrayList list = new ArrayList<>(); + Flux.just(1, 2, 3, 4) + .log() + .subscribe(new Subscriber() { + private Subscription s; + int onNextAmount; + + @Override + public void onSubscribe(Subscription subscription) { + this.s = subscription; + s.request(2); + } + + @Override + public void onNext(Integer integer) { + list.add(integer); + onNextAmount++; + if (onNextAmount % 2 == 0) { + s.request(2); + } + } + + @Override + public void onError(Throwable throwable) { + + } + + @Override + public void onComplete() { + + } + }); + } + + @Test + public void fn5() { + Flux.just(1, 2, 3, 5) + .log() + .map(i -> i * 2) + .subscribe(e -> elems.add(e)); + System.out.println(elems); + } + + @Test + public void fn6() { + ArrayList l = new ArrayList<>(); + Flux.just(1, 2, 3, 4, 5) + .log() + .map(i -> i*2) + .zipWith(Flux.range(0, Integer.MAX_VALUE), + (one, two) -> String.format("First Flux : %d, Second Flux: %d", one, two)) + .subscribe(e -> l.add(e)); + System.out.println(l); + } + + @Test + public void fn7() { + ConnectableFlux publish = Flux.create(fluxSink -> { + while (true) { + fluxSink.next(System.currentTimeMillis()); + } + }).publish(); + publish.subscribe(e -> System.out.println("#######"+e)); + publish.subscribe(e -> System.out.println("@@@@@@@"+e)); + + publish.connect(); + } + + @Test + public void fn8() { + ConnectableFlux publish = Flux.create(fluxSink -> { + while (true) { + fluxSink.next(System.currentTimeMillis()); + } + }).publish(); + publish.sample(Duration.ofSeconds(2)).subscribe(e -> System.out.println("#######"+e)); + publish.sample(Duration.ofSeconds(10)).subscribe(e -> System.out.println("@@@@@@@"+e)); + publish.connect(); + } + + + @Test + public void fn9(){ + Flux.just(1, 2, 3, 4) + .log() + .map(i -> i*2) + .subscribeOn(Schedulers.parallel()) + .subscribe(elems::add); + } + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a01example/A01ExampleController.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a01example/A01ExampleController.java new file mode 100644 index 00000000..025f6097 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a01example/A01ExampleController.java @@ -0,0 +1,47 @@ +package com.example.reactor0.a01example; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.concurrent.TimeUnit; + +@RestController +public class A01ExampleController { + + @RequestMapping("hello/{who}") + public Mono hello(@PathVariable String who) { + return Mono.just(who) + .map(e -> "hello " + who); + } + + + @RequestMapping("data/{who}") + public Mono> hello11(@PathVariable String who) { + Sir sir = new Sir(); + sir.setFirstName(who); + return Mono.just(sir) + .filter(e -> { + try { + TimeUnit.SECONDS.sleep(2); + }catch (Exception ex){ + ex.printStackTrace(); + } + return true; + }) + .map(ResponseEntity::ok) + .defaultIfEmpty(ResponseEntity + .status(404).body(null)); + } + + @GetMapping("sir/list") + public Flux getAllEmployees() { + Sir sir = new Sir(); + sir.setFirstName("11111"); + Sir sir2 = new Sir(); + sir2.setFirstName("2222"); + return Flux.just(sir, sir2); + } + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a01example/Client.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a01example/Client.java new file mode 100644 index 00000000..f336a2c7 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a01example/Client.java @@ -0,0 +1,26 @@ +package com.example.reactor0.a01example; + +import org.junit.jupiter.api.Test; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; + +import java.util.concurrent.TimeUnit; + +/** + * @author chenpq05 + * @since 2022/7/7 9:41 + */ +public class Client { + WebClient client = WebClient.create("http://localhost:8080"); + + @Test + public void test1() throws Exception{ + Flux employeeFlux = client.get() + .uri("/sir/list") + .retrieve() + .bodyToFlux(Sir.class); + + employeeFlux.subscribe(e -> System.out.println("##########" + e)); + TimeUnit.SECONDS.sleep(5); + } +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a01example/Sir.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a01example/Sir.java new file mode 100644 index 00000000..392ca8f3 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a01example/Sir.java @@ -0,0 +1,14 @@ +package com.example.reactor0.a01example; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Sir { + private String firstName; + private String lastName; + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/Client02.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/Client02.java new file mode 100644 index 00000000..d74d5fa0 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/Client02.java @@ -0,0 +1,27 @@ +package com.example.reactor0.a02filter; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.concurrent.TimeUnit; + +/** + * @author chenpq05 + * @since 2022/7/7 9:41 + */ +@Slf4j +public class Client02 { + WebClient client = WebClient.create("http://localhost:8080"); + + @Test + public void users01() throws Exception { + client.get() + .uri("/players/baeldung") + .retrieve() + .bodyToMono(String.class) + .subscribe(e -> log.info("!!!!!!!!!!{}", e)); + + TimeUnit.SECONDS.sleep(2L); + } +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/Config02.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/Config02.java new file mode 100644 index 00000000..281db718 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/Config02.java @@ -0,0 +1,32 @@ +package com.example.reactor0.a02filter; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.server.*; +import reactor.core.publisher.Mono; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; + +@Slf4j +@Configuration +public class Config02 { + + @Bean + public RouterFunction route(PlayerHandler playerHandler) { + return RouterFunctions.route(GET("/players/{name}"), playerHandler::getName) + .filter(new ExampleHandlerFilterFunction()); + } + + /** + * 需添加到 RouterFunctions.route + */ + public class ExampleHandlerFilterFunction implements HandlerFilterFunction { + @Override + public Mono filter(ServerRequest request, HandlerFunction next) { + log.info("@@@@@@@@@@@@进入ExampleHandlerFilterFunction"); + return next.handle(request); + } + } + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/ExampleWebFilter.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/ExampleWebFilter.java new file mode 100644 index 00000000..01803fb1 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/ExampleWebFilter.java @@ -0,0 +1,25 @@ +package com.example.reactor0.a02filter; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * 全局过滤器 + * @author chenpq05 + * @since 2022/7/7 10:13 + */ +@Slf4j +@Component +public class ExampleWebFilter implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + exchange.getResponse().getHeaders().add("web-filter", "web-filter-test"); + log.info("####################进入全局过滤器ExampleWebFilter"); + return chain.filter(exchange); + } +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/FilterController.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/FilterController.java new file mode 100644 index 00000000..7c8a4d44 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/FilterController.java @@ -0,0 +1,18 @@ +package com.example.reactor0.a02filter; + +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RestController +public class FilterController { + + @RequestMapping("/players/{name}") + public Mono getName(@PathVariable String name) { + return Mono.just(name) + .map(e -> "hello " + name); + } + + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/PlayerHandler.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/PlayerHandler.java new file mode 100644 index 00000000..23e8c78b --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a02filter/PlayerHandler.java @@ -0,0 +1,16 @@ +package com.example.reactor0.a02filter; + +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +@Component +public class PlayerHandler { + + public Mono getName(ServerRequest request) { + Mono name = Mono.just(request.pathVariable("name")); + return ServerResponse.ok().body(name, String.class); + } + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/Config03.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/Config03.java new file mode 100644 index 00000000..812153e4 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/Config03.java @@ -0,0 +1,23 @@ +package com.example.reactor0.a03error; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +/** + * @author chenpq05 + * @since 2022/7/7 14:32 + */ +@Configuration +public class Config03 { + + @Bean + public RouterFunction routeRequest(Handler handler) { + return RouterFunctions.route(RequestPredicates.GET("/hello").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), handler::handleWithErrorReturn); + } + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/ErrorController.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/ErrorController.java new file mode 100644 index 00000000..bd4665bc --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/ErrorController.java @@ -0,0 +1,26 @@ +package com.example.reactor0.a03error; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RestController +public class ErrorController { + + @RequestMapping("/hello") + public Mono getName(String name) { + return Mono.just(name) + .map(e -> "hello " + name); + } + + @RequestMapping("/global") + public Mono global(String name) { + if ("异常".equals(name)) { + throw new RuntimeException("抛出异常"); + } + return Mono.just(name) + .map(e -> "hello " + name); + } + + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/GlobalErrorAttributes.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/GlobalErrorAttributes.java new file mode 100644 index 00000000..e19ced2c --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/GlobalErrorAttributes.java @@ -0,0 +1,24 @@ +package com.example.reactor0.a03error; + +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; + +import java.util.Map; + +/** + * @author chenpq05 + * @since 2022/7/7 15:55 + */ +@Component +public class GlobalErrorAttributes extends DefaultErrorAttributes { + @Override + public Map getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) { + Map map = super.getErrorAttributes(request, options); + map.put("status", HttpStatus.BAD_REQUEST); + map.put("message", "全局异常"); + return map; + } +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/GlobalErrorWebExceptionHandler.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/GlobalErrorWebExceptionHandler.java new file mode 100644 index 00000000..e00acdc9 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/GlobalErrorWebExceptionHandler.java @@ -0,0 +1,45 @@ +package com.example.reactor0.a03error; + +import org.junit.jupiter.api.Order; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.reactive.error.ErrorAttributes; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.*; +import reactor.core.publisher.Mono; + +import java.util.Map; + +/** + * @author chenpq05 + * @since 2022/7/7 15:59 + */ +@Component +@Order(-2) +public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { + public GlobalErrorWebExceptionHandler(GlobalErrorAttributes g, + ApplicationContext applicationContext, + ServerCodecConfigurer serverCodecConfigurer) { + super(g, new WebProperties.Resources(), applicationContext); + super.setMessageWriters(serverCodecConfigurer.getWriters()); + super.setMessageReaders(serverCodecConfigurer.getReaders()); + } + + @Override + protected RouterFunction getRoutingFunction(ErrorAttributes errorAttributes) { + return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); + } + + private Mono renderErrorResponse(final ServerRequest request) { + Map errorAttributes = getErrorAttributes(request, ErrorAttributeOptions.defaults()); + return ServerResponse.status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(errorAttributes)); + } +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/Handler.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/Handler.java new file mode 100644 index 00000000..f85a2b02 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/Handler.java @@ -0,0 +1,73 @@ +package com.example.reactor0.a03error; + +import com.example.reactor0.a01example.Sir; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.stream.Collectors; + +/** + * @author chenpq05 + * @since 2022/7/7 14:40 + */ +@Component +public class Handler { + + private Mono sayHelloFallback() { + return Mono.just("hello, Stranger"); + } + + private Mono sayHello(ServerRequest request) { + try { + return Mono.just("Hello,"+request.queryParam("name").get()); + } catch (Exception e) { + return Mono.error(e); + } + } + + public Mono handleWithErrorReturn(ServerRequest request) { + return sayHello(request) + .onErrorReturn("Hello, Stranger") + .flatMap(s -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(s)); + } + + public Mono handleWithErrorResumeAndDynamicFallback(ServerRequest request) { + return sayHello(request) + .flatMap(s -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(s)) + .onErrorResume(e -> (Mono.just("Hi, I looked around for your name but found: "+ e.getMessage())) + .flatMap(s -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON). + bodyValue(s))); + } + + public Mono handleWithResumeAndFallbackMethod(ServerRequest request) { + return sayHello(request) + .flatMap(s -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(s)) + .onErrorResume(e -> sayHelloFallback() + .flatMap(s -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(s))); + } + + public Mono handleWithErrorResumeAndCustomException(ServerRequest request) { + return ServerResponse.ok() + .body(sayHello(request) + .onErrorResume(e -> Mono.error(new RuntimeException("自定义异常"))), String.class); + + } + + public Mono handleWithGloabalError(ServerRequest request) { + return ServerResponse.ok().body(sayHello(request), String.class); + } + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/MyControllerAdvice.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/MyControllerAdvice.java new file mode 100644 index 00000000..e634a466 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a03error/MyControllerAdvice.java @@ -0,0 +1,25 @@ +package com.example.reactor0.a03error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import reactor.core.publisher.Mono; + +/** + * WebFlux也可以使用@RestControllerAdvice + * @RestControllerAdvice会导致 GlobalErrorWebExceptionHandler 失效 + */ +@Slf4j +@RestControllerAdvice +public class MyControllerAdvice { + + @ExceptionHandler(Exception.class) + protected Mono errorHandler(Exception e) { + log.error("全局异常MyControllerAdvice", e); + Mono r = Mono.just(ResponseEntity.badRequest().body("全局异常MyControllerAdvice:" + e.getMessage())); + // return ResponseEntity.badRequest().body("全局异常MyControllerAdvice:"+e.getMessage()); + return r; + } + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a04cors/CorsGlobalConfiguration.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a04cors/CorsGlobalConfiguration.java new file mode 100644 index 00000000..f4fed865 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a04cors/CorsGlobalConfiguration.java @@ -0,0 +1,29 @@ +package com.example.reactor0.a04cors; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.CorsRegistry; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +/** + * @author chenpq05 + * @since 2022/7/8 10:36 + */ +@Configuration +@EnableWebFlux +public class CorsGlobalConfiguration implements WebFluxConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + // 设置允许跨域的路由 + registry.addMapping("/**") + // 设置允许跨域请求的域名 + // SpringBoot2.4.0 [allowedOriginPatterns]代替[allowedOrigins] + .allowedOriginPatterns("*") + // 是否允许证书(cookies) + .allowCredentials(true) + // 设置允许的方法 + .allowedMethods("*") + // 跨域允许时间 + .maxAge(3600); + } +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/Foo.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/Foo.java new file mode 100644 index 00000000..05554d43 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/Foo.java @@ -0,0 +1,38 @@ +package com.example.reactor0.a05debug; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author chenpq05 + * @since 2022/7/8 11:21 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Foo { + + private Integer id; + + private String formattedName; + + private Integer quantity; + + public Foo(FooDto dto) { + this.id = randomId() == 0 ? null : dto.getId(); + this.formattedName = dto.getName(); + this.quantity = randomQuantity(); + } + + private static int randomId() { + return ThreadLocalRandom.current().nextInt(0, 100); + } + + private static int randomQuantity() { + return ThreadLocalRandom.current().nextInt(0, 10); + } + +} \ No newline at end of file diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooDto.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooDto.java new file mode 100644 index 00000000..3b972315 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooDto.java @@ -0,0 +1,20 @@ +package com.example.reactor0.a05debug; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author chenpq05 + * @since 2022/7/8 11:22 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FooDto { + + private Integer id; + + private String name; + +} \ No newline at end of file diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooNameHelper.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooNameHelper.java new file mode 100644 index 00000000..172d0515 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooNameHelper.java @@ -0,0 +1,42 @@ +package com.example.reactor0.a05debug; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author chenpq05 + * @since 2022/7/8 11:23 + */ +public class FooNameHelper { + + public static Foo concatAndSubstringFooName(Foo foo) { + Foo concat = concatFooName(foo); + return substringFooName(concat); + } + + public static Foo concatFooName(Foo foo) { + + int random = ThreadLocalRandom.current() + .nextInt(0, 80); + + String processedName = (random != 0) + ? foo.getFormattedName() + : foo.getFormattedName() + "-bael"; + + foo.setFormattedName(processedName); + return foo; + } + + public static Foo substringFooName(Foo foo) { + int random = ThreadLocalRandom.current() + .nextInt(0, 100); + + String processedName = (random == 0) + ? foo.getFormattedName().substring(10, 15) + : foo.getFormattedName().substring(0, 5); + + foo.setFormattedName(processedName); + + return foo; + } + +} \ No newline at end of file diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooQuantityHelper.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooQuantityHelper.java new file mode 100644 index 00000000..7299a848 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooQuantityHelper.java @@ -0,0 +1,26 @@ +package com.example.reactor0.a05debug; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author chenpq05 + * @since 2022/7/8 11:25 + */ +public class FooQuantityHelper { + + public static Foo processFooReducingQuantity(Foo foo) { + int random = ThreadLocalRandom.current().nextInt(0, 90); + int result = (random == 0) ? 0 : foo.getQuantity() + 2; + foo.setQuantity(result); + + return divideFooQuantity(foo); + } + + public static Foo divideFooQuantity(Foo foo) { + + Integer result = (int) Math.round(5.0 / foo.getQuantity()); + foo.setQuantity(result); + return foo; + } + +} \ No newline at end of file diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooReporter.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooReporter.java new file mode 100644 index 00000000..efa1e078 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooReporter.java @@ -0,0 +1,27 @@ +package com.example.reactor0.a05debug; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author chenpq05 + * @since 2022/7/8 11:24 + */ +public class FooReporter { + + private static final Logger LOGGER = LoggerFactory.getLogger(FooReporter.class); + + public static Foo reportResult(Foo foo, String approach) { + if (foo.getId() == null) { + throw new IllegalArgumentException("Null id is not valid!"); + } + LOGGER.info("Reporting for approach {}: Foo with id '{}' name '{}' and quantity '{}'", + approach, foo.getId(), foo.getFormattedName(), foo.getQuantity()); + + return foo; + } + + public static Foo reportResult(Foo input) { + return reportResult(input, "default"); + } +} \ No newline at end of file diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooService.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooService.java new file mode 100644 index 00000000..d929c081 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a05debug/FooService.java @@ -0,0 +1,85 @@ +package com.example.reactor0.a05debug; + +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.scheduler.Schedulers; + +import java.util.concurrent.TimeUnit; + +/** + * @author chenpq05 + * @since 2022/7/8 11:43 + */ +@Slf4j +public class FooService { + + public static void processFoo(Flux flux) { + flux.map(FooNameHelper::concatFooName) + .map(FooNameHelper::substringFooName) + .log() + .map(FooReporter::reportResult) + .doOnError(error -> log.error("异常方法", error)) + .subscribe(foo -> { + log.debug("完成processing,foo的id是{}", foo.getId()); + }, error -> { + log.error("subscribe异常发生 {}", error); + }); + } + + public static void processFooThread(Flux flux) { + flux.log() + .subscribeOn(Schedulers.newSingle("five-single-starter")) + .map(FooNameHelper::concatAndSubstringFooName) + .publishOn(Schedulers.newSingle("five-single-foo")) + .map(FooNameHelper::concatAndSubstringFooName) + .map(FooQuantityHelper::divideFooQuantity) + .map(foo -> FooReporter.reportResult(foo, "FIVE-SINGLE")) + .publishOn(Schedulers.newSingle("five-single-bar")) + .map(FooNameHelper::concatAndSubstringFooName) + .doOnError(error -> log.error("Approach 5-single failed!", error)) + .subscribe(); + } + + public static void processUsingApproachFourWithCheckpoint(Flux flux) { + log.info("starting approach four!"); + + flux.map(FooNameHelper::concatAndSubstringFooName) + .checkpoint("CHECKPOINT 1") + .map(FooNameHelper::concatAndSubstringFooName) + .map(FooQuantityHelper::divideFooQuantity) + .checkpoint("CHECKPOINT 2", true) + .map(foo -> FooReporter.reportResult(foo, "FOUR")) + .map(FooNameHelper::concatAndSubstringFooName) + .doOnError(error -> log.error("Approach 4 failed!", error)) + .subscribe(); + } + + public static void processFooList(Flux flux) { + flux.map(FooNameHelper::concatFooName) + .map(FooNameHelper::substringFooName) + .log() + .map(FooReporter::reportResult) + .doOnError(error -> { + log.error("打印信息", error); + }) + .subscribe(); + } + + public static void processFooInAnotherScenario(Flux flux) { + flux.map(FooNameHelper::substringFooName) + .map(FooQuantityHelper::divideFooQuantity) + .subscribe(); + } + + public static void main(String[] args) throws Exception { + // Flux reactiveStream = Flux.range(1, 5).log(); + // Flux reactiveStream = Flux.range(1, 5).log().take(3); + Flux reactiveStream = Flux.range(1, 5).take(3).log(); + + reactiveStream.subscribe(); + + + } + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a06urlpattern/Tests06.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a06urlpattern/Tests06.java new file mode 100644 index 00000000..83c1e1b3 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a06urlpattern/Tests06.java @@ -0,0 +1,24 @@ +package com.example.reactor0.a06urlpattern; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.publisher.ConnectableFlux; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Optional; + +@Slf4j +class Tests06 { + + ArrayList elems = new ArrayList<>(); + + + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a06urlpattern/UrlController.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a06urlpattern/UrlController.java new file mode 100644 index 00000000..e2dcf859 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a06urlpattern/UrlController.java @@ -0,0 +1,15 @@ +package com.example.reactor0.a06urlpattern; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class UrlController { + + @GetMapping("/spring5/{*id}") + public String URIVariableHandler(@PathVariable String id) { + return id; + } + +} diff --git a/project/reactor/reactor0/src/main/java/com/example/reactor0/a07serversentevents/A07Controller.java b/project/reactor/reactor0/src/main/java/com/example/reactor0/a07serversentevents/A07Controller.java new file mode 100644 index 00000000..13eddb23 --- /dev/null +++ b/project/reactor/reactor0/src/main/java/com/example/reactor0/a07serversentevents/A07Controller.java @@ -0,0 +1,39 @@ +package com.example.reactor0.a07serversentevents; + +import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerSentEvent; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +import java.time.Duration; +import java.time.LocalTime; + +@RestController +public class A07Controller { + + /** + * 使用浏览器访问 + * @return + */ + @GetMapping(path = "/stream-flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux streamFlux() { + return Flux.interval(Duration.ofSeconds(1)) + .map(sequence -> "Flux - " + LocalTime.now().toString()); + } + + /** + * 使用浏览器访问,Flux是单向通行,服务端向客户端推送消息 + * @return + */ + @GetMapping("/stream-sse") + public Flux> streamEvents() { + return Flux.interval(Duration.ofSeconds(1)) + .map(sequence -> ServerSentEvent.builder() + .id(String.valueOf(sequence)) + .event("periodic-event") + .data("SSE - " + LocalTime.now().toString()) + .build()); + } + +} diff --git a/project/reactor/reactor0/src/main/resources/application.properties b/project/reactor/reactor0/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/project/reactor/reactor0/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/project/reactor/reactor0/src/test/java/com/example/reactor0/Reactor0ApplicationTests.java b/project/reactor/reactor0/src/test/java/com/example/reactor0/Reactor0ApplicationTests.java new file mode 100644 index 00000000..85ee91d0 --- /dev/null +++ b/project/reactor/reactor0/src/test/java/com/example/reactor0/Reactor0ApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.reactor0; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Reactor0ApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/project/reactor/reactor01/.gitignore b/project/reactor/reactor01/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/project/reactor/reactor01/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/project/reactor/reactor01/pom.xml b/project/reactor/reactor01/pom.xml new file mode 100644 index 00000000..5c7003d8 --- /dev/null +++ b/project/reactor/reactor01/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.6.4 + + + com.example + reactor01 + 0.0.1-SNAPSHOT + reactor01 + reactor01 + + 1.8 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework + spring-web + + + + org.springframework.boot + spring-boot-starter-test + + + io.projectreactor + reactor-bus + 2.0.8.RELEASE + + + io.projectreactor + reactor-core + 2.0.8.RELEASE + + + io.projectreactor + reactor-test + + + + org.projectlombok + lombok + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/project/reactor/reactor01/src/main/java/com/example/reactor01/Reactor01Application.java b/project/reactor/reactor01/src/main/java/com/example/reactor01/Reactor01Application.java new file mode 100644 index 00000000..564eb2c6 --- /dev/null +++ b/project/reactor/reactor01/src/main/java/com/example/reactor01/Reactor01Application.java @@ -0,0 +1,30 @@ +package com.example.reactor01; + +import com.example.reactor01._01notification.NotificationConsumer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import reactor.bus.EventBus; + +import static reactor.bus.selector.Selectors.$; + +@SpringBootApplication +public class Reactor01Application implements CommandLineRunner { + + @Autowired + private EventBus eventBus; + + @Autowired + private NotificationConsumer notificationConsumer; + + @Override + public void run(String... args) throws Exception { + eventBus.on($("notificationConsumer"), notificationConsumer); + } + + public static void main(String[] args) { + SpringApplication.run(Reactor01Application.class, args); + } + +} diff --git a/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/Config.java b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/Config.java new file mode 100644 index 00000000..78db0b3d --- /dev/null +++ b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/Config.java @@ -0,0 +1,24 @@ +package com.example.reactor01._01notification; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import reactor.Environment; +import reactor.bus.EventBus; + +/** + * @author chenpq05 + * @since 2022/7/5 17:28 + */ +@Configuration +public class Config { + @Bean + public Environment env() { + return Environment.initializeIfEmpty().assignErrorJournal(); + } + + @Bean + public EventBus createEventBus(Environment env) { + return EventBus.create(env, Environment.THREAD_POOL); + } + +} diff --git a/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationConsumer.java b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationConsumer.java new file mode 100644 index 00000000..471005ac --- /dev/null +++ b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationConsumer.java @@ -0,0 +1,29 @@ +package com.example.reactor01._01notification; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import reactor.bus.Event; +import reactor.fn.Consumer; + + +/** + * @author chenpq05 + * @since 2022/7/5 17:19 + */ +@Component +public class NotificationConsumer implements Consumer> { + + @Autowired + private NotificationService notificationService; + + @Override + public void accept(Event event) { + NotificationData data = event.getData(); + try { + notificationService.initiateNotification(data); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + +} diff --git a/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationController.java b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationController.java new file mode 100644 index 00000000..bd30c9a9 --- /dev/null +++ b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationController.java @@ -0,0 +1,31 @@ +package com.example.reactor01._01notification; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import reactor.bus.Event; +import reactor.bus.EventBus; + +/** + * @author chenpq05 + * @since 2022/7/5 17:23 + */ +@Controller +public class NotificationController { + + @Autowired + private EventBus eventBus; + + @GetMapping("/startNotification/{param}") + public String startNotification(@PathVariable Integer param) { + for (int i = 0; i < param; i++) { + NotificationData data = new NotificationData(); + data.setId(i); + eventBus.notify("notificationConsumer", Event.wrap(data)); + System.out.println("Notification " + i + ": notification task submitted successfully"); + } + return "done"; + } + +} diff --git a/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationData.java b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationData.java new file mode 100644 index 00000000..41788af9 --- /dev/null +++ b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationData.java @@ -0,0 +1,44 @@ +package com.example.reactor01._01notification; + +/** + * @author chenpq05 + * @since 2022/7/5 17:14 + */ +public class NotificationData { + private long id; + private String name; + private String email; + private String mobile; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getMobile() { + return mobile; + } + + public void setMobile(String mobile) { + this.mobile = mobile; + } +} diff --git a/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationService.java b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationService.java new file mode 100644 index 00000000..86ebf50f --- /dev/null +++ b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationService.java @@ -0,0 +1,11 @@ +package com.example.reactor01._01notification; + +/** + * @author chenpq05 + * @since 2022/7/5 17:16 + */ +public interface NotificationService { + + void initiateNotification(NotificationData data) throws InterruptedException; + +} diff --git a/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationServiceimpl.java b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationServiceimpl.java new file mode 100644 index 00000000..b34ce68a --- /dev/null +++ b/project/reactor/reactor01/src/main/java/com/example/reactor01/_01notification/NotificationServiceimpl.java @@ -0,0 +1,21 @@ +package com.example.reactor01._01notification; + +import org.springframework.stereotype.Service; + +/** + * @author chenpq05 + * @since 2022/7/5 17:17 + */ +@Service +public class NotificationServiceimpl implements NotificationService { + @Override + public void initiateNotification(NotificationData data) throws InterruptedException { + System.out.println("Notification service started for " + + "Notification ID: " + data.getId()); + + Thread.sleep(5000); + + System.out.println("Notification service ended for " + + "Notification ID: " + data.getId()); + } +} diff --git a/project/reactor/reactor01/src/main/resources/application.properties b/project/reactor/reactor01/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/project/reactor/reactor01/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/project/reactor/reactor01/src/test/java/com/example/reactor01/Reactor01ApplicationTests.java b/project/reactor/reactor01/src/test/java/com/example/reactor01/Reactor01ApplicationTests.java new file mode 100644 index 00000000..10fee78b --- /dev/null +++ b/project/reactor/reactor01/src/test/java/com/example/reactor01/Reactor01ApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.reactor01; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Reactor01ApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/pom.xml b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/pom.xml new file mode 100644 index 00000000..f00500bc --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/pom.xml @@ -0,0 +1,119 @@ + + + 4.0.0 + + + jar + + + com.cpq + SpringAI-MCP-RAG-Dev + 1.0-SNAPSHOT + + + mcp-client + + + 21 + 21 + UTF-8 + + + + + + org.springframework.ai + spring-ai-bom + 1.0.0 + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-aop + + + + + + org.springframework.ai + spring-ai-starter-model-openai + + + + + org.springframework.ai + spring-ai-advisors-vector-store + + + org.springframework.ai + spring-ai-starter-vector-store-redis + + + + + + + + + org.springframework.ai + spring-ai-tika-document-reader + + + + + org.springframework.ai + spring-ai-starter-mcp-client + + + + org.projectlombok + lombok + true + + + + org.apache.commons + commons-lang3 + 3.17.0 + + + + org.apache.groovy + groovy-json + 4.0.27 + + + + cn.hutool + hutool-all + 5.8.30 + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + + + + \ No newline at end of file diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/Application.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/Application.java new file mode 100644 index 00000000..16c1847b --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/Application.java @@ -0,0 +1,13 @@ +package com.cpq; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/ChatEntity.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/ChatEntity.java new file mode 100644 index 00000000..1b76f0d6 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/ChatEntity.java @@ -0,0 +1,18 @@ +package com.cpq.bean; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class ChatEntity { + + private String currentUserName; + private String message; + private String botMsgId; + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/ChatResponseEntity.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/ChatResponseEntity.java new file mode 100644 index 00000000..8cc1fded --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/ChatResponseEntity.java @@ -0,0 +1,17 @@ +package com.cpq.bean; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class ChatResponseEntity { + + private String message; + private String botMsgId; + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/LeeResult.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/LeeResult.java new file mode 100644 index 00000000..15f3d44f --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/LeeResult.java @@ -0,0 +1,115 @@ +package com.cpq.bean; + +public class LeeResult { + + // 响应业务状态 + private Integer status; + + // 响应消息 + private String msg; + + // 响应中的数据 + private Object data; + + private String ok; // 不使用 + + public static LeeResult build(Integer status, String msg, Object data) { + return new LeeResult(status, msg, data); + } + + public static LeeResult build(Integer status, String msg, Object data, String ok) { + return new LeeResult(status, msg, data, ok); + } + + public static LeeResult ok(Object data) { + return new LeeResult(data); + } + + public static LeeResult ok() { + return new LeeResult(null); + } + + public static LeeResult errorMsg(String msg) { + return new LeeResult(500, msg, null); + } + + public static LeeResult errorUserTicket(String msg) { + return new LeeResult(557, msg, null); + } + + public static LeeResult errorMap(Object data) { + return new LeeResult(501, "error", data); + } + + public static LeeResult errorTokenMsg(String msg) { + return new LeeResult(502, msg, null); + } + + public static LeeResult errorException(String msg) { + return new LeeResult(555, msg, null); + } + + public static LeeResult errorUserQQ(String msg) { + return new LeeResult(556, msg, null); + } + + public LeeResult() { + + } + + public LeeResult(Integer status, String msg, Object data) { + this.status = status; + this.msg = msg; + this.data = data; + } + + public LeeResult(Integer status, String msg, Object data, String ok) { + this.status = status; + this.msg = msg; + this.data = data; + this.ok = ok; + } + + public LeeResult(Object data) { + this.status = 200; + this.msg = "OK"; + this.data = data; + } + + public Boolean isOK() { + return this.status == 200; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + public String getOk() { + return ok; + } + + public void setOk(String ok) { + this.ok = ok; + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/SearXNGResponse.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/SearXNGResponse.java new file mode 100644 index 00000000..f6d4dd6f --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/SearXNGResponse.java @@ -0,0 +1,19 @@ +package com.cpq.bean; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +@Data +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class SearXNGResponse { + + private String query; + private List results; + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/SearchResult.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/SearchResult.java new file mode 100644 index 00000000..0353427a --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/bean/SearchResult.java @@ -0,0 +1,19 @@ +package com.cpq.bean; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class SearchResult { + + private String title; + private String url; + private String content; + private double score; + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/CorsConfig.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/CorsConfig.java new file mode 100644 index 00000000..fce6a63f --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/CorsConfig.java @@ -0,0 +1,30 @@ +package com.cpq.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * @ClassName CorsConfig + * @Author 风间影月 + * @Version 1.0 + * @Description CorsConfig + **/ +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Value("${website.domain}") + private String domain; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(domain) + .allowedMethods("*") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(60 * 60); + + } +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/LLMConfig.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/LLMConfig.java new file mode 100644 index 00000000..0bab0ffa --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/LLMConfig.java @@ -0,0 +1,26 @@ +package com.cpq.config; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class LLMConfig { + + @Bean + public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory, ToolCallbackProvider tools) { + /** + * defaultToolCallbacks(tools) 注入ToolCallbackProvider,使用mcp + * ChatClient配置defaultOptions可能导致无法使用远程mcp + */ + return ChatClient.builder(chatModel) + .defaultToolCallbacks(tools) + .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) + .build(); + } + +} \ No newline at end of file diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/OKHttpConfig.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/OKHttpConfig.java new file mode 100644 index 00000000..45baa50b --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/OKHttpConfig.java @@ -0,0 +1,20 @@ +package com.cpq.config; + +import okhttp3.OkHttpClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +@Configuration +public class OKHttpConfig { + + @Bean + public OkHttpClient okHttpClient() { + return new OkHttpClient.Builder() + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .build(); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/RedisConfig.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/RedisConfig.java new file mode 100644 index 00000000..67dc485b --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/config/RedisConfig.java @@ -0,0 +1,49 @@ +package com.cpq.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@Slf4j +public class RedisConfig +{ + /** + * RedisTemplate配置 + * redis序列化的工具配置类,下面这个请一定开启配置 + * 127.0.0.1:6379> keys * + * 1) "ord:102" 序列化过 + * 2) "\xac\xed\x00\x05t\x00\aord:102" 野生,没有序列化过 + * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法 + * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法 + * this.redisTemplate.opsForSet(); //提供了操作set的所有方法 + * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法 + * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法 + * @param redisConnectionFactor + * @return + */ + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactor) + { + RedisTemplate redisTemplate = new RedisTemplate<>(); + + redisTemplate.setConnectionFactory(redisConnectionFactor); + //设置key序列化方式string + redisTemplate.setKeySerializer(new StringRedisSerializer()); + //设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化 + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + + redisTemplate.afterPropertiesSet(); + + return redisTemplate; + } +} + + diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/ChatController.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/ChatController.java new file mode 100644 index 00000000..0ddd30ca --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/ChatController.java @@ -0,0 +1,28 @@ +package com.cpq.controller; + +import com.cpq.bean.ChatEntity; +import com.cpq.service.ChatService; +import jakarta.annotation.Resource; +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; + + +@RestController +@RequestMapping("chat") +public class ChatController { + + + @Resource + private ChatService chatService; + + /** + * @Description: 聊天 + SSE返回 + */ + @PostMapping("doChat") + public void doChat(@RequestBody ChatEntity chatEntity){ + chatService.doChat(chatEntity); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/InternetController.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/InternetController.java new file mode 100644 index 00000000..57dd75b1 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/InternetController.java @@ -0,0 +1,28 @@ +package com.cpq.controller; + +import com.cpq.bean.ChatEntity; +import com.cpq.service.SearXngService; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping("internet") +public class InternetController { + + @Resource + private SearXngService searXngService; + + @GetMapping("/test") + public Object test(@RequestParam("query") String query){ + return searXngService.search(query); + } + + @PostMapping("/search") + public void search(@RequestBody ChatEntity chatEntity, HttpServletResponse response){ + response.setCharacterEncoding("UTF-8"); + searXngService.doInternetSearch(chatEntity); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/McpTestController.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/McpTestController.java new file mode 100644 index 00000000..f9cf7db7 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/McpTestController.java @@ -0,0 +1,45 @@ +package com.cpq.controller; + +import com.cpq.bean.ChatEntity; +import com.cpq.service.McpService; +import jakarta.annotation.Resource; +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; + +@RestController +@RequestMapping("/mcp-test") +public class McpTestController { + + @Resource + private McpService mcpService; + + + /** + 1、测试本地mcp文件服务、远程mcp高德地图服务,请求参数: + curl --location --request POST 'http://127.0.0.1:9090/mcp-test/doChat' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "currentUserName": 12456, + "message": "请在spring-ai-cmp目录下生成一个abc.html文件,把深圳一天旅游攻略写入到abc.html,使用好看的css+html来编写", + "botMsgId": 154545475454 + }' + 但是无法在本地创建文件,提示没有权限 + + 2、测试高德地图服务、发送邮件服务 + curl --location --request POST 'http://127.0.0.1:9090/mcp-test/doChat' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "currentUserName": 12456, + "message": "介绍一份杭州市一天的旅游攻略,并把旅游攻略发送到我的邮箱", + "botMsgId": 154545475454 + }' + + */ + @PostMapping("doChat") + public void doChat(@RequestBody ChatEntity chatEntity){ + mcpService.doChat(chatEntity); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/RagController.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/RagController.java new file mode 100644 index 00000000..ab226c5b --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/RagController.java @@ -0,0 +1,40 @@ +package com.cpq.controller; + +import com.cpq.bean.ChatEntity; +import com.cpq.bean.LeeResult; +import com.cpq.service.RagService; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.ai.document.Document; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + + +@RestController +@RequestMapping("rag") +public class RagController { + + @Resource + private RagService ragService; + + @PostMapping("/uploadRagDoc") + public LeeResult uploadRagDoc(@RequestParam("file") MultipartFile file ){ + List documentList = ragService.loadText(file.getResource(), file.getOriginalFilename()); + return LeeResult.ok(documentList); + } + + @GetMapping("/doSearch") + public LeeResult doSearch(@RequestParam String question) { + return LeeResult.ok(ragService.vectorSearch(question)); + } + + @PostMapping("/search") + public void search(@RequestBody ChatEntity chatEntity, HttpServletResponse response) { + List list = ragService.vectorSearch(chatEntity.getMessage()); + response.setCharacterEncoding("UTF-8"); + ragService.doChatRagSearch(chatEntity, list); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/SseEndpointController.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/SseEndpointController.java new file mode 100644 index 00000000..0e59cdc6 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/SseEndpointController.java @@ -0,0 +1,24 @@ +package com.cpq.controller; + +import com.cpq.sse.SseServer; +import org.springframework.http.MediaType; +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 org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + + +@RestController +@RequestMapping("sse") +public class SseEndpointController { + + /** + * @Description: 前端发送连接的请求,连接SSE服务 + */ + @GetMapping(path = "connect", produces = {MediaType.TEXT_EVENT_STREAM_VALUE}) + public SseEmitter connect(@RequestParam String userId){ + return SseServer.connect(userId); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/TestSseController.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/TestSseController.java new file mode 100644 index 00000000..99c93e62 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/controller/TestSseController.java @@ -0,0 +1,46 @@ +package com.cpq.controller; + +import com.cpq.enums.SSEMsgType; +import com.cpq.sse.SseServer; +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; + + +@RestController +@RequestMapping("/test/sse") +public class TestSseController { + + + /** + * @Description: SSE发送单个消息 + */ + @GetMapping("sendMessage") + public Object sendMessage(@RequestParam String userId, @RequestParam String message){ + SseServer.sendMsg(userId, message, SSEMsgType.MESSAGE); + return "OK"; + } + + /** + * @Description: SSE发送单个消息 - add + */ + @GetMapping("sendMessageAdd") + public Object sendMessageAdd(@RequestParam String userId, @RequestParam String message) throws Exception { + for (int i = 0; i < 10; i++) { + Thread.sleep(200); + SseServer.sendMsg(userId, message, SSEMsgType.ADD); + } + return "OK"; + } + + /** + * @Description: SSE发送群消息 + */ + @GetMapping("sendMessageAll") + public Object sendMessageAll(@RequestParam String message){ + SseServer.sendMsgToAllUsers(message); + return "OK"; + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/enums/SSEMsgType.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/enums/SSEMsgType.java new file mode 100644 index 00000000..1235d4f1 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/enums/SSEMsgType.java @@ -0,0 +1,22 @@ +package com.cpq.enums; + +/** + * @Description: 发送SSE的消息类型 + */ +public enum SSEMsgType { + + MESSAGE("message", "单词发送的普通类型消息"), + ADD("add", "消息追加,适用于流式stream推送"), + FINISH("finish", "消息完成"), + CUSTOM_EVENT("custom_event", "单词发送的普通类型消息"), + DONE("done", "单词发送的普通类型消息"); // ChatGLM v4 + + public final String type; + public final String value; + + SSEMsgType(String type, String value) { + this.type = type; + this.value = value; + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/ChatService.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/ChatService.java new file mode 100644 index 00000000..f583dbf2 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/ChatService.java @@ -0,0 +1,48 @@ +package com.cpq.service; + +import cn.hutool.json.JSONUtil; +import com.cpq.bean.ChatEntity; +import com.cpq.bean.ChatResponseEntity; +import com.cpq.enums.SSEMsgType; +import com.cpq.sse.SseServer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.stream.Collectors; + + +@Service +@Slf4j +public class ChatService{ + + @Autowired + private ChatClient chatClient; + + public void doChat(ChatEntity chatEntity) { + + String userId = chatEntity.getCurrentUserName(); + String prompt = chatEntity.getMessage(); + String botMsgId = chatEntity.getBotMsgId(); + + Flux stringFlux = chatClient.prompt(prompt).stream().content(); + + List list = stringFlux.toStream().map(str -> { + String content = str.toString(); + SseServer.sendMsg(userId, content, SSEMsgType.ADD); + log.info("content: {}", content); + return content; + }).collect(Collectors.toList()); + + String fullContent = list.stream().collect(Collectors.joining()); + + ChatResponseEntity chatResponseEntity = new ChatResponseEntity(fullContent, botMsgId); + + SseServer.sendMsg(userId, JSONUtil.toJsonStr(chatResponseEntity), SSEMsgType.FINISH); + + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/McpService.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/McpService.java new file mode 100644 index 00000000..83727a79 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/McpService.java @@ -0,0 +1,45 @@ +package com.cpq.service; + +import cn.hutool.json.JSONUtil; +import com.cpq.bean.ChatEntity; +import com.cpq.bean.ChatResponseEntity; +import com.cpq.enums.SSEMsgType; +import com.cpq.sse.SseServer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class McpService { + + @Autowired + private ChatClient chatClient; + + public void doChat(ChatEntity chatEntity) { + + String userId = chatEntity.getCurrentUserName(); + String prompt = chatEntity.getMessage(); + String botMsgId = chatEntity.getBotMsgId(); + log.info("#############prompt{}", prompt); + Flux stringFlux = chatClient.prompt(prompt).stream().content(); + List list = stringFlux.toStream().map(str -> { + String content = str.toString(); + SseServer.sendMsg(userId, content, SSEMsgType.ADD); + log.info("content: {}", content); + return content; + }).collect(Collectors.toList()); + log.info("#############list={}", list); + String fullContent = list.stream().collect(Collectors.joining()); + + ChatResponseEntity chatResponseEntity = new ChatResponseEntity(fullContent, botMsgId); + + SseServer.sendMsg(userId, JSONUtil.toJsonStr(chatResponseEntity), SSEMsgType.FINISH); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/RagService.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/RagService.java new file mode 100644 index 00000000..19fe1dfe --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/RagService.java @@ -0,0 +1,123 @@ +package com.cpq.service; + +import cn.hutool.json.JSONUtil; +import com.cpq.bean.ChatEntity; +import com.cpq.bean.ChatResponseEntity; +import com.cpq.enums.SSEMsgType; +import com.cpq.sse.SseServer; +import com.cpq.util.CustomTextSplitter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.chat.prompt.PromptTemplate; +import org.springframework.ai.document.Document; +import org.springframework.ai.reader.TextReader; +import org.springframework.ai.vectorstore.redis.RedisVectorStore; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class RagService { + + @Autowired + private RedisVectorStore redisVectorStore; + @Autowired + private ChatClient chatClient; + + public List loadText(Resource resource, String fileName) { + + // 加载读取文档 + TextReader textReader = new TextReader(resource); + textReader.getCustomMetadata().put("fileName", fileName); + List documentList = textReader.get(); + +// System.out.println("documentList = " + documentList); + +// 默认的文本切分器 +// TokenTextSplitter tokenTextSplitter = new TokenTextSplitter(); +// List list = tokenTextSplitter.apply(documentList); + + CustomTextSplitter tokenTextSplitter = new CustomTextSplitter(); + List list = tokenTextSplitter.apply(documentList); + + System.out.println("list = " + list); + + // 向量存储 + int batchSize = 10; + for (int i = 0; i < list.size(); i += batchSize) { + int endIndex = Math.min(i + batchSize, list.size()); + List batch = list.subList(i, endIndex); + redisVectorStore.add(batch); + } + + return documentList; + } + + public List vectorSearch(String question) { + return redisVectorStore.similaritySearch(question); + } + + + // Dify 智能体引擎构建平台 + + private static final String RAG_PROMPT = """ + 基于上下文的知识库内容回答问题: + 【上下文】 + {context} + + 【问题】 + {question} + + 【输出】 + 如果没有查到,请回复:不知道。 + 如果查到,请回复具体的内容。不相关的近似内容不必提到。 + """; + + public void doChatRagSearch(ChatEntity chatEntity, List documents) { + + String userId = chatEntity.getCurrentUserName(); + String question = chatEntity.getMessage(); + String botMsgId = chatEntity.getBotMsgId(); + + // 构建提示词 + String context = null; + if (CollectionUtils.isNotEmpty(documents)) { + context = documents.stream() + .map(Document::getText) + .collect(Collectors.joining("\n")); + } + + // 组装提示词 + PromptTemplate promptTemplate = new PromptTemplate(RAG_PROMPT); + Map map = Map.of("context", context, + "question", question); + Prompt prompt = promptTemplate.create(map); + + log.info("{}", prompt); + + Flux stringFlux = chatClient.prompt(prompt).stream().content(); + + List list = stringFlux.toStream().map(chatResponse -> { + String content = chatResponse.toString(); + SseServer.sendMsg(userId, content, SSEMsgType.ADD); + log.info("content: {}", content); + return content; + }).collect(Collectors.toList()); + + String fullContent = list.stream().collect(Collectors.joining()); + + ChatResponseEntity chatResponseEntity = new ChatResponseEntity(fullContent, botMsgId); + + SseServer.sendMsg(userId, JSONUtil.toJsonStr(chatResponseEntity), SSEMsgType.FINISH); + + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/SearXngService.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/SearXngService.java new file mode 100644 index 00000000..edd4150c --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/service/SearXngService.java @@ -0,0 +1,149 @@ +package com.cpq.service; + +import cn.hutool.json.JSONUtil; +import com.cpq.bean.ChatEntity; +import com.cpq.bean.ChatResponseEntity; +import com.cpq.bean.SearXNGResponse; +import com.cpq.bean.SearchResult; +import com.cpq.enums.SSEMsgType; +import com.cpq.sse.SseServer; +import lombok.extern.slf4j.Slf4j; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class SearXngService { + + @Value("${internet.websearch.searxng.url}") + private String SEARXNG_URL; + + @Value("${internet.websearch.searxng.counts}") + private Integer COUNTS; + + @Autowired + private OkHttpClient okHttpClient; + @Autowired + private ChatClient chatClient; + + public List search(String query) { + // 构建url + HttpUrl url = HttpUrl.get(SEARXNG_URL) + .newBuilder() + .addQueryParameter("q", query) + .addQueryParameter("format", "json") + .build(); + + log.info("搜索的url地址为:" + url.url()); + + // 构建request + Request request = new Request.Builder() + .url(url) + .build(); + + // 发送请求 + try (Response response = okHttpClient.newCall(request).execute()) { + + // 判断请求是否成功还是失败 + if (!response.isSuccessful()) throw new RuntimeException("请求失败: HTTP " + response.code()); + + // 获得响应的数据 + if (response.body() != null) { + String responseBody = response.body().string(); + + SearXNGResponse searXNGResponse = JSONUtil.toBean(responseBody, SearXNGResponse.class); + + return dealResults(searXNGResponse.getResults()); + } + log.error("搜索失败:{}", response.message()); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return Collections.emptyList(); + } + + private List dealResults(List results) { + + return results.subList(0, Math.min(COUNTS, results.size())) + .parallelStream() + .sorted(Comparator.comparingDouble(SearchResult::getScore).reversed()) + .limit(COUNTS).toList(); + } + + + private static final String SESRXNG_PROMPT = """ + 你是一个互联网搜索大师,请基于以下互联网返回的结果作为上下文,根据你的理解结合用户的提问综合后,生成并且输出专业的回答: + 【上下文】 + {context} + + 【问题】 + {question} + + 【输出】 + 如果没有查到,请回复:不知道。 + 如果查到,请回复具体的内容。 + """; + + public void doInternetSearch(ChatEntity chatEntity) { + + String userId = chatEntity.getCurrentUserName(); + String question = chatEntity.getMessage(); + String botMsgId = chatEntity.getBotMsgId(); + + List searchResults = this.search(question); + + String finalPrompt = buildSesrXngPrompt(question, searchResults); + + // 组装提示词 + Prompt prompt = new Prompt(finalPrompt); + + System.out.println(prompt.toString()); + + Flux stringFlux = chatClient.prompt(prompt).stream().content(); + + List list = stringFlux.toStream().map(chatResponse -> { + String content = chatResponse.toString(); + SseServer.sendMsg(userId, content, SSEMsgType.ADD); + log.info("content: {}", content); + return content; + }).collect(Collectors.toList()); + + String fullContent = list.stream().collect(Collectors.joining()); + + ChatResponseEntity chatResponseEntity = new ChatResponseEntity(fullContent, botMsgId); + + SseServer.sendMsg(userId, JSONUtil.toJsonStr(chatResponseEntity), SSEMsgType.FINISH); + } + + private static String buildSesrXngPrompt(String question, List searchResults) { + + StringBuilder context = new StringBuilder(); + + searchResults.forEach(searchResult -> { + context.append( + String.format("\n[来源] %s \n [摘要] %s \n \n", + searchResult.getUrl(), + searchResult.getContent())); + }); + + return SESRXNG_PROMPT + .replace("{context}", context) + .replace("{question}", question); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/sse/SseServer.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/sse/SseServer.java new file mode 100644 index 00000000..17fa9fe2 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/sse/SseServer.java @@ -0,0 +1,118 @@ +package com.cpq.sse; + +import com.cpq.enums.SSEMsgType; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +@Slf4j +public class SseServer { + + // 存放所有用户 + private static final Map sseClients = new ConcurrentHashMap<>(); + + /** + * @Description: 连接SSE服务 + */ + public static SseEmitter connect(String userId) { + + // 设置超时时间,0L表示不超时(永不过期);默认是30秒,超时未完成任务则会抛出异常 + SseEmitter sseEmitter = new SseEmitter(10 * 24 * 60 * 60 * 1000L); + + // 注册回调方法 + sseEmitter.onTimeout(timeoutCallback(userId)); + sseEmitter.onCompletion(completionCallback(userId)); + sseEmitter.onError(errorCallback(userId)); + + sseClients.put(userId, sseEmitter); + + log.info("SSE连接创建成功,连接的用户ID为:{}", userId); + + return sseEmitter; + } + + public static Runnable timeoutCallback(String userId) { + return () -> { + log.info("SSE超时..."); + completeAndRemove(userId); + }; + } + + public static Runnable completionCallback(String userId) { + return () -> { + log.info("SSE完成..."); + // 移除用户连接 + remove(userId); + }; + } + + public static Consumer errorCallback(String userId) { + return Throwable -> { + log.error("SSE异常..."); + completeAndRemove(userId); + }; + } + + public static void remove(String userId) { + // 删除用户 + if (sseClients.containsKey(userId)) { + sseClients.remove(userId); + log.info("SSE连接被移除,移除的用户ID为:{}", userId); + } + } + + public static void completeAndRemove(String userId) { + SseEmitter sseEmitter = sseClients.get(userId); + if (sseEmitter != null) { + sseEmitter.complete(); + // 移除用户连接 + sseClients.remove(userId); + log.info("SSE连接被移除,移除的用户ID为:{}", userId); + } + } + + public static void sendMsg(String userId, String message, SSEMsgType msgType) { + SseEmitter sseEmitter = sseClients.get(userId); + if (sseEmitter == null) { + log.debug("sseEmitter为空,userId={}", userId); + return; + } + sendEmitterMessage(sseEmitter, userId, message, msgType); + } + + private static void sendEmitterMessage(SseEmitter sseEmitter, + String userId, + String message, + SSEMsgType msgType) { + try { + SseEmitter.SseEventBuilder msgEvent = SseEmitter.event() + .id(userId) + .data(message) + .name(msgType.type); + sseEmitter.send(msgEvent); + } catch (IOException e) { + log.error("SSE异常...", e); + remove(userId); + } catch (Exception e) { + log.error("SSE异常Exception...", e); + } + } + + public static void sendMsgToAllUsers(String message) { + if (CollectionUtils.isEmpty(sseClients)) { + return; + } + + sseClients.forEach((userId, sseEmitter) -> { + sendEmitterMessage(sseEmitter, userId, message, SSEMsgType.MESSAGE); + } + ); + } + + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/util/CustomTextSplitter.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/util/CustomTextSplitter.java new file mode 100644 index 00000000..64a66d27 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/java/com/cpq/util/CustomTextSplitter.java @@ -0,0 +1,17 @@ +package com.cpq.util; + +import org.springframework.ai.transformer.splitter.TextSplitter; + +import java.util.List; + +public class CustomTextSplitter extends TextSplitter { + + @Override + protected List splitText(String text) { + return List.of(split(text)); + } + + public String[] split(String text) { + return text.split("\\s*\\R\\s*\\R\\s*"); + } +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/resources/application-dev.yml b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/resources/application-dev.yml new file mode 100644 index 00000000..8f5a7372 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/resources/application-dev.yml @@ -0,0 +1,5 @@ +server: + port: 9090 + +website: + domain: http://127.0.0.1:5501 \ No newline at end of file diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/resources/application.yml b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/resources/application.yml new file mode 100644 index 00000000..c7e482b5 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/resources/application.yml @@ -0,0 +1,55 @@ +spring: + application: + name: spring-ai-mcp-client + profiles: + active: dev + data: + redis: + host: 192.168.1.221 + port: 6379 + ai: + mcp: + client: + enabled: true + name: spring-ai-mcp-client + request-timeout: 60s + type: ASYNC + sse: + connections: + # 通过sse使用远程mcp服务,高德地图mcp服务 + server1: + url: https://mcp.amap.com + sse-endpoint: /sse?key=bfd7f79a957f12846678ed71b420565c + # 是用本地开发的mcp服务 + server2: + url: http://localhost:9060 + sse-endpoint: /sse + stdio: + # 本地通过stdio使用mcp服务 + servers-configuration: classpath:mcp-server.json + openai: + api-key: ${ALI_AI_KEY} + base-url: https://dashscope.aliyuncs.com/compatible-mode + chat: + options: + model: qwen-plus + embedding: + enabled: true + options: + model: text-embedding-v4 + vectorstore: + redis: + initialize-schema: true # 是否初始化所需的模式 + index-name: lee-vectorstore # 用于存储向量的索引名称 + prefix: 'embedding:' # Redis 键的前缀 + +logging: + level: + root: info + +# SearXNG联网搜索 +internet: + websearch: + searxng: + url: http://192.168.1.221:8888/search + counts: 10 \ No newline at end of file diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/resources/mcp-server.json b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/resources/mcp-server.json new file mode 100644 index 00000000..8427b7af --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/src/main/resources/mcp-server.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "filesystem": { + "command": "cmd", + "args": ["/c", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/spring-ai-cmp"] + } + } +} \ No newline at end of file diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/test.txt b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/test.txt new file mode 100644 index 00000000..f49a2815 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-client/test.txt @@ -0,0 +1,316 @@ +Deepseek R1 是⽀持复杂推理、多模态处理、技术⽂档⽣成的⾼性能通⽤⼤语⾔模型。本⼿册 +为技术团队提供完整的本地部署指南,涵盖硬件配置、国产芯⽚适配、量化⽅案、云端替代⽅ +案及完整671B MoE模型的Ollama部署⽅法。模型 参数规 +模 +计算精 +度 +最低显存需 +求 最低算⼒需求 +DeepSeek-R1 (671B) 671B FP8 ≥890GB +2*XE9680(16*H20 +GPU) +DeepSeek-R1-Distill- +70B +70B BF16 ≥180GB 4*L20 或 2*H20 GPU +三、国产芯⽚与硬件适配⽅案 +1. 国内⽣态合作伙伴动态 +企业 适配内容 性能对标(vs +NVIDIA) +华为昇 +腾 +昇腾910B原⽣⽀持R1全系列,提供端到端推理优化 +⽅案 等效A100(FP16) +沐曦 +GPU +MXN系列⽀持70B模型BF16推理,显存利⽤率提升 +30% 等效RTX 3090 +海光 +DCU 适配V3/R1模型,性能对标NVIDIA A100 等效A100(BF16) +2. 国产硬件推荐配置 +模型参数 推荐⽅案 适⽤场景 +1.5B 太初T100加速卡 个⼈开发者原型验证 +14B 昆仑芯K200集群 企业级复杂任务推理 +32B 壁彻算⼒平台+昇腾910B集群 科研计算与多模态处理 +12月26日,Deepseek发布了全新系列模型DeepSeek-v3,一夜之间霸榜开源模型,并在性能上和世界顶尖的闭源模型GPT-4o以及 Claude-3.5-Sonnet相提并论。 + +该模型为MOE架构,大大降低了训练成本,据说训练成本仅600万美元,成本降低10倍,资源运用效率极高。有AI投资机构负责人直言,DeepSeek发布的53页的技术论文是黄金。 + +那就先让我们看看论文是怎么说的吧,老规矩,先上资源地址: + +Github: GitHub - deepseek-ai/DeepSeek-V3 + +模型地址:https://huggingface.co/deepseek-ai + +论文地址:https://github.com/deepseek-ai/DeepSeek-V3/blob/main/DeepSeek_V3.pdf + +以下为技术解读: + +前言 +DeepSeek-AI 发布了其最新的大型语言模型 DeepSeek-V3,这款模型在性能和效率方面都取得了显著的进步,成为当前最强大的开源基础模型之一。DeepSeek-V3 是一款拥有 671B参数的大型混合专家 (MoE) 模型,其中每个 token 会有 37 B参数被激活。 + + +为了实现高效的推理和成本效益的训练,DeepSeek-V3 采用了多头潜在注意力 (MLA) 和 DeepSeekMoE 架构,这两个架构在 DeepSeek-V2 中已经得到了充分验证。此外,DeepSeek-V3 还开创了一种无辅助损失策略来平衡负载,并设置了多 token 预测训练目标以进一步提升性能。 + + +架构:创新负载平衡策略和训练目标 +基本架构 +DeepSeek-V3 的基本架构仍然基于 Transformer 框架,但其采用了 MLA 和 DeepSeekMoE 架构来实现高效推理和成本效益的训练。 + + +多头潜在注意力 (MLA) + +MLA 架构的核心思想是对注意力键和值进行低秩联合压缩,从而减少推理过程中的 Key-Value (KV) 缓存。它通过以下步骤实现: + +压缩: 将注意力输入 h_t 映射到一个压缩的潜在向量 c_KV_t。 + +生成键: 使用 W_UK 和 W_VU 矩阵将 c_KV_t 映射到压缩的键和值。 + +生成解码器: 使用 RoPE 矩阵生成带有旋转位置嵌入 (RoPE) 的解码器。 + +计算注意力: 使用 softmax 函数计算注意力权重,并生成最终的注意力输出 u_t。 + +MLA 架构只需要缓存压缩后的潜在向量和带有 RoPE 的解码器,从而显著减少了 KV 缓存,同时保持了与标准多头注意力 (MHA) 相当的性能。 + +DeepSeekMoE:辅助损失免费负载平衡 +DeepSeekMoE 架构使用更细粒度的专家,并将一些专家隔离为共享专家。每个 token 的 FFN 输出 h’_t 通过以下步骤计算: + +共享专家: 使用共享专家 FFN( ) (·) 计算共享专家的输出。 + +路由专家: 使用路由专家 FFN( ) (·) 计算路由专家的输出,并使用门控值 g_i,t 选择激活的专家。 + +输出: 将共享专家和路由专家的输出相加,得到最终的 FFN 输出 h’_t。 + +DeepSeek-V3 还引入了一种辅助损失免费负载平衡策略,通过引入偏置项 b_i 并将其添加到相应的亲和度分数 s_i,t 中,来确定 top-K 路由。通过动态调整偏置项,DeepSeek-V3 能够在整个训练过程中保持平衡的专家负载,并取得比纯粹使用辅助损失的模型更好的性能。 + +多 token 预测 +DeepSeek-V3 采用了一种名为多 token 预测 (MTP) 的训练目标,该目标扩展了预测范围,以便在每个位置预测多个未来的 token。MTP 目标可以提高数据效率和模型的预测能力,并通过预先规划未来的 token 的表示来提升性能。 + +MTP 实现了 D 个连续的模块来预测 D 个额外的 token,每个模块都包含一个共享嵌入层、一个共享输出头、一个 Transformer 模块和一个投影矩阵。每个 MTP 模块都使用线性投影将 token 的表示和嵌入相连接,然后通过 Transformer 模块生成输出表示,并计算额外的预测 token 的概率分布。 + +基础设施:高效训练的基石 +DeepSeek-V3 的训练过程依赖于高效的计算集群和训练框架。 + + +计算集群 +DeepSeek-V3 在一个配备 2048 个 NVIDIA H800 GPU 的集群上进行训练。每个节点包含 8 个 GPU,通过 NVLink 和 NVSwitch 相互连接。跨节点之间使用 InfiniBand (IB) 进行通信。 + + +训练框架 +DeepSeek-V3 的训练框架基于 HAI-LLM 框架,该框架为高效训练提供了强大的支持。DeepSeek-V3 应用了 16 路 Pipeline Parallelism (PP)、64 路 Expert Parallelism (EP) 和 ZeRO-1 Data Parallelism (DP)。 + +双向管道并行 (DualPipe) + +为了解决跨节点专家并行导致的通信开销问题,DeepSeek-V3 设计了一种名为 DualPipe 的新型管道并行算法。DualPipe 通过重叠正向和反向计算通信阶段,不仅提高了模型训练速度,还减少了管道气泡的数量。 + +跨节点全连接通信 + +DeepSeek-V3 开发了高效的跨节点全连接通信内核,以充分利用 IB 和 NVLink 的带宽,并节省专门用于通信的 Streaming Multiprocessors (SMs)。 + +极低的内存占用 + +DeepSeek-V3 通过以下技术来降低训练过程中的内存占用: + +RMSNorm 和 MLA 上投影的重新计算: 在反向传播过程中重新计算所有 RMSNorm 操作和 MLA 上投影,从而消除了永久存储其输出激活的需求。 + +CPU 上的指数移动平均: 在训练过程中保存模型参数的指数移动平均 (EMA),用于早期估计模型性能,并异步更新 EMA 参数,从而避免额外的内存和时间开销。 + +多 token 预测中的共享嵌入和输出头: 利用 DualPipe 策略,将模型的最浅层和最深层部署在同一个 PP 路径上,从而实现共享嵌入和输出头的参数和梯度,进一步提高内存效率。 + +FP8 训练 +DeepSeek-V3 支持使用 FP8 数据格式进行混合精度训练,以实现加速训练和降低 GPU 内存使用。 + + +混合精度框架 + +混合精度框架使用 FP8 格式进行大多数计算密集型操作,而一些关键操作则保留其原始数据格式,以平衡训练效率和数值稳定性。 + + +量化精度提升 + +为了提高低精度训练的精度,DeepSeek-V3 引入了几种策略: + +细粒度量化: 将激活和权重分组并分别进行缩放,以更好地适应异常值。 + +增加累积精度: 将部分结果复制到 FP32 寄存器中进行全精度累积,以提高精度。 + +尾数超过指数: 采用 E4M3 格式,即 4 位指数和 3 位尾数,以提高精度。 + +低精度存储和通信 + +DeepSeek-V3 通过以下方式进一步降低内存和通信开销: + +低精度优化器状态: 使用 BF16 格式跟踪 AdamW 优化器的第一和第二矩。 +低精度激活: 使用 FP8 格式缓存 Linear 操作的激活,并对一些关键激活使用 E5M6 格式,或重新计算其输出。 +低精度通信: 将激活在 MoE 上投影之前量化为 FP8,并使用调度组件,与 MoE 上投影中的 FP8 Fprop 兼容。 +预训练:迈向终极训练效率 +DeepSeek-V3 在一个包含 14.8 万亿高质量和多样化 token 的语料库上进行预训练。预训练过程非常稳定,没有遇到不可恢复的损失峰值或需要回滚的情况。 + +数据构建 +预训练语料库经过优化,数学和编程样本的比例更高,并扩展了多语言覆盖范围,包括英语和中文。数据处理流程也得到了改进,以减少冗余并保持语料库的多样性。 + +超参数设置 +DeepSeek-V3 的超参数包括 Transformer 层数、隐藏维度、注意力头数、每头维度、KV 压缩维度、查询压缩维度、RoPE 维度、MoE 层数、共享专家数量、路由专家数量、中间隐藏维度、激活专家数量、节点限制路由数量、多 token 预测深度、学习率、批大小等。 + + +长上下文扩展 +DeepSeek-V3 采用与 DeepSeek-V2 相似的方法来启用长上下文功能。在预训练阶段之后,应用 YaRN 进行上下文扩展,并进行两个额外的训练阶段,将上下文窗口逐步扩展到 32K 和 128K。 + +评估 +DeepSeek-V3 在一系列基准测试中进行了评估,包括多学科多项选择题、语言理解和推理、闭卷问答、阅读理解、参考消歧、语言模型、中文理解和文化、数学、代码和标准化考试等。DeepSeek-V3 在大多数基准测试中都取得了最强大的性能,尤其是在数学和代码任务上。 + + +讨论 +DeepSeek-V3 中的 MTP 策略和多 token 预测策略都取得了显著的性能提升。辅助损失免费负载平衡策略也取得了更好的性能,并且专家具有更强的专业模式。与序列级辅助损失相比,批量级负载平衡方法也表现出一致的效率优势,但其也面临着潜在的挑战,例如序列或小批量中的负载不平衡以及推理过程中域转换引起的负载不平衡。 + + + +后训练:知识蒸馏与强化学习 +DeepSeek-V3 通过监督微调和强化学习进行后训练,以使其与人类偏好保持一致并进一步释放其潜力。 + +监督微调(Supervised Fine-Tuning ) +DeepSeek-V3 使用一个包含 150 万个实例的数据集进行监督微调,涵盖了多个领域。对于推理相关的数据集,例如数学、代码竞赛问题和逻辑谜题,使用内部 DeepSeek-R1 模型生成数据。对于非推理数据,例如创意写作、角色扮演和简单问答,使用 DeepSeek-V2.5 生成。并通过拒绝抽样方法筛选高质量数据,以确保最终训练数据的准确性和简洁性。 + +SFT 设置:DeepSeek-V3 使用余弦退火学习率调度进行两个 epoch 的训练,初始学习率为 5 × 10^-6,并逐渐降低到 1 × 10^-6。在训练过程中,每个序列由多个样本打包,并使用样本掩码策略确保这些示例保持隔离并相互不可见。 + +强化学习 +DeepSeek-V3 采用基于规则的奖励模型 (RM) 和基于模型的 RM 来确定模型的反馈。对于可以验证的特定规则的问题,使用基于规则的奖励系统来确定反馈。对于具有自由格式真实答案的问题,使用奖励模型来确定答案是否与预期的真实答案匹配。对于没有明确真实答案的问题,奖励模型负责根据问题和答案提供反馈。 + +DeepSeek-V3 使用组相对策略优化 (GRPO) 进行强化学习,该优化方法放弃了与策略模型相同大小的评论模型,而是从组分数中估计基线。在 RL 过程中,模型使用高温采样生成包含来自 DeepSeek-R1 生成数据和原始数据的模式的响应,即使在缺乏明确系统提示的情况下也能做到。 + +评估 +DeepSeek-V3 在一系列基准测试中进行了评估,包括 IFEval、FRAMES、LongBench v2、GPQA、SimpleQA、C-SimpleQA、SWE-Bench Verified、Aider 1、LiveCodeBench、Codeforces、中国高中数学奥林匹克 (CNMO) 2024 和美国邀请数学考试 (AIME) 2024 等。DeepSeek-V3 在大多数基准测试中都取得了最强大的性能,尤其是在代码、数学和长上下文理解任务上。 + + + + +讨论 +DeepSeek-V3 从 DeepSeek-R1 系列模型中蒸馏推理能力取得了成功,显著提高了其在数学和代码基准测试中的性能。同时,DeepSeek-V3 还采用了宪法 AI 方法,利用 DeepSeek-V3 自身的投票评估结果作为反馈来源,进一步提高了其在主观评估中的性能。 + +DeepSeek-V3 中的多 token 预测技术可以显著加速模型的解码速度,而额外的预测 token 的接受率在 85% 到 90% 之间,这表明其具有高度的可靠性。 + +结论、局限性和未来方向 +DeepSeek-V3 是一款性能强大且成本效益高的开源大型语言模型,它在推理和生成任务中都取得了显著的成果。DeepSeek-V3 的训练成本非常低,只需 2.788M H800 GPU 小时即可完成其全部训练,包括预训练、上下文长度扩展和后训练。 + +尽管 DeepSeek-V3 在性能和效率方面取得了显著成果,但它仍然存在一些局限性,尤其是在部署方面。DeepSeek-V3 的推荐部署单元相对较大,这可能对小型团队构成负担。此外,尽管 DeepSeek-V3 的部署策略已经实现了比 DeepSeek-V2 高两倍的端到端生成速度,但仍然存在进一步提升的空间。 + +DeepSeek-V3 开发了创新的负载平衡策略和训练目标,以实现高效训练。它还引入了 FP8 训练和一系列高效的工程优化措施,以进一步降低训练成本。 +DeepSeek-V3 还在后训练阶段取得了成功,通过知识蒸馏和强化学习技术,显著提高了其在数学和代码基准测试中的性能。 +DeepSeek-V3 在一系列基准测试中取得了最强大的性能,尤其是在数学、代码和长上下文理解任务上。 +DeepSeek-V3 的局限性主要在于部署方面,包括较大的部署单元和潜在的性能提升空间。 +DeepSeek-V3 采用了宪法 AI (constitutional AI) 方法,利用 DeepSeek-V3 自身的投票评估结果作为反馈来源,进一步提高了其在主观评估中的性能。 +DeepSeek-V3 中的多 token 预测技术可以显著加速模型的解码速度,而额外的预测 token 的接受率在 85% 到 90% 之间,这表明其具有高度的可靠性。 +DeepSeek 持续致力于开源模型的道路,并计划在未来进行以下方面的研究: + +进一步改进模型架构,以提高训练和推理效率,并尝试突破 Transformer 架构的限制。 +持续迭代训练数据的质量和数量,并探索其他训练信号来源,以推动数据扩展到更广泛的维度。 +持续探索和迭代模型的深度思考能力,以增强其智能和问题解决能力,并扩展其推理长度和深度。 +探索更全面和多维度的模型评估方法,以防止在研究过程中优化固定的一组基准测试,从而产生对模型能力的误导印象并影响我们的基础评估。 +DeepSeek-V3 的发布标志着开源大型语言模型领域的一个重大里程碑,并为未来的研究和应用开辟了新的可能性。 + +简单测试 +DeepSeek-V3开源模型,我肯定是没有资源部署了,所以只能通过它的服务网站进行测试了。 + +地址:DeepSeek + + +算一下星舰从地球到火星的飞行时间: + + +让它分析一下自己的技术文档: + + + + +最后让它比较了一下自己与GPT-4o-0513 + + +... 略... + + +——完—— + +@北方的郎 · 专注模型与代码 + +概述 +前置准备 +1. 申请 DeepSeek API +2. 注册 Dify +集成步骤 +1. 将 DeepSeek 接入至 Dify +2. 搭建 DeepSeek AI 应用 +3. 为 AI 应用启用文本分析能力 +4. 分享 AI 应用 +阅读更多 +Edit on GitHub + + + +阅读更多 +应用案例 +DeepSeek 与 Dify 集成指南:打造具备多轮思考的 AI 应用 +概述 +DeepSeek 作为具备多轮推理能力的开源大语言模型,以高性能、低成本、易部署的特性成为智能应用开发的理想基座。通过其 API 服务,开发者可快速调用 DeepSeek 的复杂逻辑推理与内容生成能力。在传统开发模式下,构建生产级 AI 应用往往需要独立完成模型适配、接口开发、交互设计等环节。 + +Dify 作为同样开源的生成式 AI 应用开发平台,能够帮助开发者基于 DeepSeek 大模型快速开发出更加智能的 AI 应用,你可以在 Dify 平台内获得以下开发体验: + +可视化构建 - 通过可视化编排界面,3 分钟搭建基于 DeepSeek R1 的 AI 应用 + +知识库增强 - 关联内部文档,开启 RAG 能力并构建精准问答系统 + +工作流扩展 - 提供多种第三方工具插件、可视化拖拽式编排应用功能节点,实现复杂业务逻辑 + +数据洞察力 - 内置总对话数、应用使用用户数等数据监控模块,支持与更加专业的监控平台集成 ... + +本文将详解 DeepSeek API 与 Dify 的集成步骤,助你快速实现两大核心场景: + +智能对话机器人开发 - 直接调用 DeepSeek R1 的思维链推理能力 + +知识增强型应用构建 - 通过私有知识库实现精准信息检索与生成 + +针对金融、法律等高合规需求场景,Dify 提供 本地私有化部署 DeepSeek + Dify,支持 DeepSeek 模型与 Dify 平台同步部署至内网 + +通过 Dify × DeepSeek 的技术组合,开发者可跳过底层架构搭建,跃迁至场景化 AI 能力落地阶段,让大模型技术快速转化为业务生产力。 + +前置准备 +1. 申请 DeepSeek API +访问 DeepSeek API 开放平台,按照页面提示进行申请 API Key。 + +若提示链接无法访问,你也可以考虑在本地部署 DeepSeek 模型。详细说明请参考 本地部署指南 + +2. 注册 Dify +Dify 是一个能够帮助你快速搭建生成式 AI 应用的平台,通过接入 DeepSeek API,你可以快速搭建出一个能够易于使用的 DeepSeek AI 应用。 + +集成步骤 +1. 将 DeepSeek 接入至 Dify +访问 Dify 平台,点击右上角头像 → 设置 → 模型供应商,找到 DeepSeek,将上文获取的 API Key 粘贴至其中。点击保存,校验通过后将出现成功提示。 + + +2. 搭建 DeepSeek AI 应用 +轻点 Dify 平台首页左侧的"创建空白应用",选择"聊天助手"类型应用并进行简单的命名。 + + +选择 deepseek-reasoner 模型 + +deepseek-reasoner 模型又称为 deepseek-r1 模型。 + + +配置完成后即可在聊天框中进行互动。 + + +3. 为 AI 应用启用文本分析能力 +RAG(检索增强生成)是一种先进的信息处理技术,它通过检索相关知识,向 LLM 提供必要的上下文信息,融入 LLM 的内容生成过程,提升回答的准确性和专业度。当你上传内部文档或专业资料后,AI 能够基于这些知识提供更有针对性的解答。 + +3.1 创建知识库 +将需要 AI 分析处理的文档上传至知识库中。为确保 DeepSeek 模型能够准确理解文档内容,建议使用"父子分段"模式进行文本处理 - 这种模式能够更好地保留文档的层级结构和上下文关系。如需了解详细的配置步骤,请参考:创建知识库。 + + +3.2 将知识库集成至 AI 应用 +在 AI 应用的"上下文"内添加知识库,在对话框内输入相关问题。LLM 将首先从知识库内获取与问题相关上下文,在此基础上进行总结并给出更高质量的回答。 + + +4. 分享 AI 应用 +构建完成后,你可以将该 AI 应用分享给他人使用或集成至其它网站内。 + + +阅读更多 +除了构建简单的 AI 应用外,你还可以创建 Chatflow / Workflow 搭建更多复杂功能的应用(例如具备文件识别、图像识别、语音识别等能力)。详细说明请参考以下文档: \ No newline at end of file diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/pom.xml b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/pom.xml new file mode 100644 index 00000000..ae043c23 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + com.cpq + SpringAI-MCP-RAG-Dev + 1.0-SNAPSHOT + + + mcp-server + + + 21 + 21 + UTF-8 + + + + + + org.springframework.ai + spring-ai-bom + 1.0.0 + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.ai + spring-ai-starter-mcp-server-webflux + + + + + org.springframework.boot + spring-boot-starter-mail + + + + + com.mysql + mysql-connector-j + 8.0.33 + + + + + com.baomidou + mybatis-plus-boot-starter + 3.5.10.1 + + + org.mybatis + mybatis-spring + + + + + org.mybatis + mybatis-spring + 3.0.4 + + + + org.projectlombok + lombok + true + + + + org.apache.commons + commons-lang3 + 3.17.0 + + + + + + com.vladsch.flexmark + flexmark-all + 0.64.8 + + + + + \ No newline at end of file diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/Application.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/Application.java new file mode 100644 index 00000000..cdbdfe91 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/Application.java @@ -0,0 +1,15 @@ +package com.cpq; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + +// http://localhost:9060/sse + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/config/McpServerConfig.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/config/McpServerConfig.java new file mode 100644 index 00000000..b8aa029f --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/config/McpServerConfig.java @@ -0,0 +1,25 @@ +package com.cpq.config; + +import com.cpq.mcp.tool.DateTool; +import com.cpq.mcp.tool.EmailTool; +import com.cpq.mcp.tool.ProductTool; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class McpServerConfig { + + /** + * 将工具方法暴露给外部 mcp client 调用 + */ + @Bean + public ToolCallbackProvider toolCallbackProvider(DateTool dateTool, EmailTool emailTool, + ProductTool productTool) { + return MethodToolCallbackProvider.builder() + .toolObjects(dateTool, emailTool, productTool) + .build(); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/controller/TestProductController.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/controller/TestProductController.java new file mode 100644 index 00000000..1025788d --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/controller/TestProductController.java @@ -0,0 +1,42 @@ +package com.cpq.controller; + +import com.cpq.entity.Product; +import com.cpq.mapper.ProductMapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * 商品表 前端控制器 + * + * @author cpq + * @since 2025-11-26 21:17:15 + */ +@Tag(name = "商品表-API") +@Slf4j +@RestController +@RequestMapping("/test-product") +public class TestProductController { + + @Autowired + private ProductMapper productMapper; + + @Operation(summary = "新增", description = "返回id") + @PostMapping("/add") + public Object add(@RequestBody Product addDTO) { + int insert = productMapper.insert(addDTO); + return insert; + } + + @Operation(summary = "详情") + @GetMapping("/detail") + public Object detail(@RequestParam("id") @Parameter(description = "主键") Long id) { + Product product = productMapper.selectById(id); + return product; + } + + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/entity/Product.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/entity/Product.java new file mode 100644 index 00000000..a03c53f9 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/entity/Product.java @@ -0,0 +1,63 @@ +package com.cpq.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 商品表 + * + * @author cpq + * @since 2025-11-26 21:17:15 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@Schema(name = "商品表", description = "商品表") +public class Product implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "主键") + @TableId(value = "id", type = IdType.ASSIGN_ID) + private Long id; + + @Schema(description = "商品的编号") + private String productNumber; + + @Schema(description = "商品的名称") + private String productName; + + @Schema(description = "商品的品牌") + private String brand; + + @Schema(description = "商品的简介(可以为空)") + private String description; + + @Schema(description = "商品的价格") + private Double price; + + @Schema(description = "商品的库存数量") + private Integer stock; + + @Schema(description = "商品的状态(上架状态的值为1/下架状态的值为0/预售状态的值为2)") + private Integer status; + + @Schema(description = "更新时间") + private LocalDateTime createdTime; + + @Schema(description = "更新时间") + private LocalDateTime updatedTime; + + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/enums/ListSortEnum.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/enums/ListSortEnum.java new file mode 100644 index 00000000..44b17a0d --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/enums/ListSortEnum.java @@ -0,0 +1,16 @@ +package com.cpq.enums; + +public enum ListSortEnum { + + ASC("asc", "正序"), + DESC("desc", "倒序"); + + public final String type; + public final String value; + + ListSortEnum(String type, String value) { + this.type = type; + this.value = value; + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/enums/PriceCompareEnum.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/enums/PriceCompareEnum.java new file mode 100644 index 00000000..29f05f15 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/enums/PriceCompareEnum.java @@ -0,0 +1,25 @@ +package com.cpq.enums; + +public enum PriceCompareEnum { + + GREATER_THAN(">", "大于"), + LESS_THAN("<", "小于"), + GREATER_THAN_OR_EQUAL_TO(">=", "大于等于"), + LESS_THAN_OR_EQUAL_TO("<=", "小于等于"), + + HIGHER_THAN(">", "高于"), + LOWER_THAN("<", "低于"), + NOT_HIGHER_THAN("<=", "不高于"), + NOT_LOWER_THAN(">=", "不低于"), + + EQUAL_TO("=", "等于"); + + public final String type; + public final String value; + + PriceCompareEnum(String type, String value) { + this.type = type; + this.value = value; + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mapper/ProductMapper.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mapper/ProductMapper.java new file mode 100644 index 00000000..f0b75229 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mapper/ProductMapper.java @@ -0,0 +1,17 @@ +package com.cpq.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.cpq.entity.Product; +import org.apache.ibatis.annotations.Mapper; +/** + *

+ * 商品表 Mapper 接口 + *

+ * + * @author cpq + * @since 2025-11-26 21:17:15 + */ +@Mapper +public interface ProductMapper extends BaseMapper { + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mapper/ProductMapper.xml b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mapper/ProductMapper.xml new file mode 100644 index 00000000..0b987b1e --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mapper/ProductMapper.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mcp/tool/DateTool.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mcp/tool/DateTool.java new file mode 100644 index 00000000..ec4e79a4 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mcp/tool/DateTool.java @@ -0,0 +1,46 @@ +package com.cpq.mcp.tool; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +@Component +@Slf4j +public class DateTool { + + /** + * 大模型会根据函数参数,自动填充参数的值 + */ + @Tool(description = "根据城市所在的时区id来获得当前的时间") + public String getCurrentTimeByZoneId(String cityName, String zoneId) { + + log.info("========== 调用MCP工具:getCurrentTimeByZoneId() =========="); + log.info(String.format("========== 参数 cityName:%s ==========", cityName)); + log.info(String.format("========== 参数 zoneId:%s ==========", zoneId)); + + ZoneId zone = ZoneId.of(zoneId); + + // 获取该时区对应的当前时间 + ZonedDateTime zonedDateTime = ZonedDateTime.now(zone); + String currentTime = String.format("当前的时间是 %s", + zonedDateTime.format( + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + return currentTime; + } + + @Tool(description = "获取当前时间") + public String getCurrentTime() { + log.info("========== 调用MCP工具:getCurrentTime() =========="); + String currentTime = String.format("当前的时间是 %s", + LocalDateTime.now().format( + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + log.info("========== 调用MCP工具:getCurrentTime()---result ==========currentTime", currentTime); + return currentTime; + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mcp/tool/EmailTool.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mcp/tool/EmailTool.java new file mode 100644 index 00000000..e8421e72 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mcp/tool/EmailTool.java @@ -0,0 +1,101 @@ +package com.cpq.mcp.tool; + +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.data.MutableDataSet; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class EmailTool { + + private final JavaMailSender mailSender; + private final String from; + + @Autowired + public EmailTool(JavaMailSender mailSender, @Value("${spring.mail.username}") String from) { + this.mailSender = mailSender; + this.from = from; + } + + @Tool(description = "查询我的邮件/邮箱地址") + public String getMyEmailAddress() { + log.info("========== 调用MCP工具:getMyEmailAddress() =========="); + return "tfz9011@163.com"; + } + + @Data + @ToString + @NoArgsConstructor + @AllArgsConstructor + public static class EmailRequest { + @ToolParam(description = "收件人邮箱") + private String email; + @ToolParam(description = "发送邮件的标题/主题") + private String subject; + @ToolParam(description = "发送邮件的消息/正文内容") + private String message; + + @ToolParam(description = "邮件的内容是否为html还是markdown格式,如果是markdown格式,则为1;如果是html格式,则为2") + private Integer contentType; + } + + @Tool(description = "给指定邮箱发送邮件信息,email 为收件人邮箱,subject 为邮件标题,message 为邮件的内容") + public void sendMailMessage(EmailRequest emailRequest) { + + log.info("========== 调用MCP工具:sendMailMessage() =========="); + log.info(String.format("========== 参数 emailRequest:%s ==========", emailRequest.toString())); + + Integer contentType = emailRequest.getContentType(); + + try { + MimeMessage mimeMessage = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage); + + helper.setFrom(from); + helper.setTo(emailRequest.getEmail()); + helper.setSubject(emailRequest.getSubject()); + + if (contentType == 1) { + helper.setText(convertToHtml(emailRequest.getMessage()), true); + } else if (contentType == 2) { + helper.setText(emailRequest.getMessage(), true); + } else { + helper.setText(emailRequest.getMessage()); + } + + mailSender.send(mimeMessage); + } catch (MessagingException e) { +// throw new RuntimeException(e); + log.error("========== 发送邮件失败,报错信息:{} ==========", e.getMessage()); + } + } + + /** + * markdown 转 html + * @param markdownStr + * @return + */ + public static String convertToHtml(String markdownStr) { + + MutableDataSet dataSet = new MutableDataSet(); + Parser parser = Parser.builder(dataSet).build(); + HtmlRenderer htmlRenderer = HtmlRenderer.builder(dataSet).build(); + + return htmlRenderer.render(parser.parse(markdownStr)); + } + +} diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mcp/tool/ProductTool.java b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mcp/tool/ProductTool.java new file mode 100644 index 00000000..37e75c04 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/java/com/cpq/mcp/tool/ProductTool.java @@ -0,0 +1,240 @@ +package com.cpq.mcp.tool; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.cpq.entity.Product; +import com.cpq.enums.ListSortEnum; +import com.cpq.enums.PriceCompareEnum; +import com.cpq.mapper.ProductMapper; +import jakarta.annotation.Resource; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +@Component +@Slf4j +public class ProductTool { + + @Resource + private ProductMapper productMapper; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ProductAdd implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @ToolParam(description = "商品的名称") + private String productName; + + @ToolParam(description = "商品的品牌") + private String brand; + + @ToolParam(description = "商品的简介(可以为空)", required = false) + private String description; + + @ToolParam(description = "商品的价格") + private Double price; + + @ToolParam(description = "商品的库存数量") + private Integer stock; + + @ToolParam(description = "商品的状态(上架状态的值为1/下架状态的值为0/预售状态的值为2)") + private Integer status=1; + + } + + @Tool(description = "创建/新增商品") + public String addProduct(ProductAdd productAdd) { + log.info("========== 创建/新增商品 productAdd={} ==========", productAdd); + Product product = new Product(); + BeanUtils.copyProperties(productAdd, product); + product.setProductNumber(RandomStringUtils.randomNumeric(12)); + productMapper.insert(product); + return "商品信息创建成功"; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ProductUpdate implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @ToolParam(description = "商品的编号") + private String productNumber; + + @ToolParam(description = "商品的名称", required = false) + private String productName; + + @ToolParam(description = "商品的品牌", required = false) + private String brand; + + @ToolParam(description = "商品的简介(可以为空)", required = false) + private String description; + + @ToolParam(description = "商品的价格", required = false) + private Double price; + + @ToolParam(description = "商品的库存数量", required = false) + private Integer stock; + + @ToolParam(description = "商品的状态(上架状态的值为1/下架状态的值为0/预售状态的值为2)", required = false) + private Integer status; + + } + + @Tool(description = "根据商品编号修改商品信息") + public String updateProduct(ProductUpdate productUpdate) { + log.info("========== 修改商品 updateProduct={} ==========", productUpdate); + Product product = new Product(); + BeanUtils.copyProperties(productUpdate, product); + + LambdaQueryWrapper lqwUp = Wrappers.lambdaQuery(); + lqwUp.eq(Product::getProductNumber, productUpdate.getProductNumber()); + productMapper.update(product, lqwUp); + return "商品信息更新成功"; + } + + @Tool(description = "根据商品编号删除商品") + public String deleteProduct(String productNumber) { + log.info("========== 根据商品编号删除商品 productNumber={} ==========", productNumber); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(Product::getProductNumber, productNumber); + productMapper.delete(lqw); + return "商品信息删除成功"; + } + + @Tool(description = "把排序(正序/倒序)转换为对应的枚举") + public ListSortEnum getSortEnum(String sort) { + log.info("========== 调用getSortEnum(),sort={} ==========", sort); + if (ListSortEnum.ASC.value.equals(sort)) { + return ListSortEnum.ASC; + } + return ListSortEnum.DESC; + } + + @Tool(description = "把商品价格的比较(大于/小于/大于等于/小于等于/高于/低于/不高于/不低于/等于)转换为对应的枚举") + public PriceCompareEnum getPriceCompareEnum(String priceCompare) { + log.info("========== getPriceCompareEnum() ,priceCompare={}==========", priceCompare); + if (priceCompare.equalsIgnoreCase(PriceCompareEnum.GREATER_THAN.value)) { + return PriceCompareEnum.GREATER_THAN; + } else if (priceCompare.equalsIgnoreCase(PriceCompareEnum.LESS_THAN.value)) { + return PriceCompareEnum.LESS_THAN; + } else if (priceCompare.equalsIgnoreCase(PriceCompareEnum.GREATER_THAN_OR_EQUAL_TO.value)) { + return PriceCompareEnum.GREATER_THAN_OR_EQUAL_TO; + } else if (priceCompare.equalsIgnoreCase(PriceCompareEnum.LESS_THAN_OR_EQUAL_TO.value)) { + return PriceCompareEnum.LESS_THAN_OR_EQUAL_TO; + } else if (priceCompare.equalsIgnoreCase(PriceCompareEnum.HIGHER_THAN.value)) { + return PriceCompareEnum.HIGHER_THAN; + } else if (priceCompare.equalsIgnoreCase(PriceCompareEnum.LOWER_THAN.value)) { + return PriceCompareEnum.LOWER_THAN; + } else if (priceCompare.equalsIgnoreCase(PriceCompareEnum.NOT_HIGHER_THAN.value)) { + return PriceCompareEnum.NOT_HIGHER_THAN; + } else if (priceCompare.equalsIgnoreCase(PriceCompareEnum.NOT_LOWER_THAN.value)) { + return PriceCompareEnum.NOT_LOWER_THAN; + } else { + return PriceCompareEnum.EQUAL_TO; + } + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ProductQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @ToolParam(description = "商品的编号", required = false) + private String productNumber; + + @ToolParam(description = "商品的名称", required = false) + private String productName; + + @ToolParam(description = "商品的品牌", required = false) + private String brand; + + @ToolParam(description = "商品的价格", required = false) + private Double price; + + @ToolParam(description = "查询列表的排序", required = false) + private ListSortEnum sortEnum; + + @ToolParam(description = "比较价格大小", required = false) + private PriceCompareEnum priceCompare; + + } + + @Tool(description = "根据条件查询商品(product)信息") + public List queryProduct(ProductQuery productQuery) { + log.info("========== 根据条件查询商品 productQuery={}==========", productQuery); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(StringUtils.isNotBlank(productQuery.getProductNumber()), + Product::getProductNumber, productQuery.getProductNumber()); + lqw.like(StringUtils.isNotBlank(productQuery.getProductName()), + Product::getProductName, productQuery.getProductName()); + lqw.like(StringUtils.isNotBlank(productQuery.getBrand()), + Product::getBrand, productQuery.getBrand()); + + Double price = productQuery.getPrice(); + PriceCompareEnum priceCompareEnum = productQuery.getPriceCompare(); + if (price != null && priceCompareEnum != null) { + if (priceCompareEnum.type.equals(PriceCompareEnum.GREATER_THAN.type)) { + lqw.gt(Product::getPrice, price); + } else if (priceCompareEnum.type.equals(PriceCompareEnum.LESS_THAN.type)) { + lqw.lt(Product::getPrice, price); + } else if (priceCompareEnum.type.equals(PriceCompareEnum.GREATER_THAN_OR_EQUAL_TO.type)) { + lqw.ge(Product::getPrice, price); + } else if (priceCompareEnum.type.equals(PriceCompareEnum.LESS_THAN_OR_EQUAL_TO.type)) { + lqw.le(Product::getPrice, price); + } else if (priceCompareEnum.type.equals(PriceCompareEnum.HIGHER_THAN.type)) { + lqw.gt(Product::getPrice, price); + } else if (priceCompareEnum.type.equals(PriceCompareEnum.LOWER_THAN.type)) { + lqw.lt(Product::getPrice, price); + } else if (priceCompareEnum.type.equals(PriceCompareEnum.NOT_HIGHER_THAN.type)) { + lqw.le(Product::getPrice, price); + } else if (priceCompareEnum.type.equals(PriceCompareEnum.NOT_LOWER_THAN.type)) { + lqw.ge(Product::getPrice, price); + } else { + lqw.eq(Product::getPrice, price); + } + + ListSortEnum sortEnum = productQuery.getSortEnum(); + if (sortEnum != null) { + if (sortEnum.type.equals(ListSortEnum.ASC.type)) { + lqw.orderByAsc(Product::getPrice); + } else { + lqw.orderByDesc(Product::getPrice); + } + } + } + + return productMapper.selectList(lqw); + } + + +} + + + + + + + + + diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/resources/application.yml b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/resources/application.yml new file mode 100644 index 00000000..b6beb5d1 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/mcp-server/src/main/resources/application.yml @@ -0,0 +1,59 @@ +server: + port: 9060 + +spring: + application: + name: spring-ai-mcp-server + profiles: + active: dev + datasource: # 数据源的相关配置 + type: com.zaxxer.hikari.HikariDataSource # 数据源的类型,可以更改为其他的数据源配置,比如druid + driver-class-name: com.mysql.cj.jdbc.Driver # mysql/MariaDB 的数据库驱动类名称 + url: jdbc:mysql://127.0.0.1:3306/cpq?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC + username: root + password: 123456 + hikari: + pool-name: DataSourceHikariCP # 连接池的名字 + connection-timeout: 30000 # 等待连接池分配连接的最大时间(毫秒),超过这个时长还没有可用的连接,则会抛出SQLException + minimum-idle: 5 # 最小连接数 + maximum-pool-size: 20 # 最大连接数 + auto-commit: true # 自动提交 + idle-timeout: 600000 # 连接超时的最大时长(毫秒),超时则会被释放(retired) + max-lifetime: 18000000 # 连接池的最大生命时长(毫秒),超时则会被释放(retired) + connection-test-query: SELECT 1 + ai: + mcp: + server: + name: spring-ai-mcp-server-sse + version: 1.0.0 + sse-endpoint: /sse + type: async + mail: + host: smtp.163.com # 邮箱host + port: 465 # 邮箱固定端口 + username: tfz9011@163.com + password: WKrq9UqJ7pPB2npy + protocol: smtp # 邮箱协议 + default-encoding: UTF-8 # 默认编码 + properties: + mail: + smtp: + socketFactory: + port: 465 + class: javax.net.ssl.SSLSocketFactory + ssl: + enable: true + +logging: + level: + root: info + +# MyBatisPlus 的配置 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: assign_id + update-strategy: not_empty + mapper-locations: classpath*:/mappers/*.xml diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/pom.xml b/project/spingai/SpringAI-MCP-RAG-Dev/pom.xml new file mode 100644 index 00000000..72f4d2bb --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.0 + + + + com.cpq + SpringAI-MCP-RAG-Dev + 1.0-SNAPSHOT + + pom + + + 21 + 21 + UTF-8 + + + + mcp-client + mcp-server + + + + + + \ No newline at end of file diff --git a/project/spingai/SpringAI-MCP-RAG-Dev/readme.md b/project/spingai/SpringAI-MCP-RAG-Dev/readme.md new file mode 100644 index 00000000..fa585286 --- /dev/null +++ b/project/spingai/SpringAI-MCP-RAG-Dev/readme.md @@ -0,0 +1,50 @@ +# searxng搜索引擎 +### 本地安装 +mkdir -p /etc/searxng/config + +mkdir -p /var/searxng/data + +docker pull docker.io/searxng/searxng:2025.8.1-3d96414 + +docker run --name searxng -d \ +-p 8888:8080 \ +-v "/etc/searxng/config/:/etc/searxng/" \ +-v "/var/searxng/data/:/var/cache/searxng/" \ +docker.io/searxng/searxng:2025.8.1-3d96414 + +vim /etc/searxng/config/settings.yml + +``` +search: + formats: + - html + - json # 加上json +``` + +编辑settings.yml,禁用engines下的Google、wikidata等无法访问的引擎,开启baidu、bing这些能访问的引擎。 +具体配置参考searxng的settings.yml + +docker restart 容器ID + +本地访问:http://192.168.1.221:8888/ + + +# product表 +``` +CREATE TABLE `product` ( +`id` bigint NOT NULL COMMENT '主键', +`product_number` varchar(64) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '商品的编号', +`product_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '商品的名称', +`brand` varchar(64) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '商品的品牌', +`description` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '商品的简介(可以为空)', +`price` float(10,2) DEFAULT '0.00' COMMENT '商品的价格', +`stock` int DEFAULT '0' COMMENT '商品的库存数量', +`status` int DEFAULT NULL COMMENT '商品的状态(上架状态的值为1/下架状态的值为0/预售状态的值为2)', +`created_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', +`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', +PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='商品表'; + + +``` + diff --git "a/project/spingai/SpringAI-MCP-RAG-Dev/searxng\347\232\204settings.yml" "b/project/spingai/SpringAI-MCP-RAG-Dev/searxng\347\232\204settings.yml" new file mode 100644 index 00000000..42ec1a7c --- /dev/null +++ "b/project/spingai/SpringAI-MCP-RAG-Dev/searxng\347\232\204settings.yml" @@ -0,0 +1,2819 @@ +general: + # Debug mode, only for development. Is overwritten by ${SEARXNG_DEBUG} + debug: false + # displayed name + instance_name: "SearXNG" + # For example: https://example.com/privacy + privacypolicy_url: false + # use true to use your own donation page written in searx/info/en/donate.md + # use false to disable the donation link + donation_url: false + # mailto:contact@example.com + contact_url: false + # record stats + enable_metrics: true + # expose stats in open metrics format at /metrics + # leave empty to disable (no password set) + # open_metrics: + open_metrics: '' + +brand: + new_issue_url: https://github.com/searxng/searxng/issues/new + docs_url: https://docs.searxng.org/ + public_instances: https://searx.space + wiki_url: https://github.com/searxng/searxng/wiki + issue_url: https://github.com/searxng/searxng/issues + # custom: + # maintainer: "Jon Doe" + # # Custom entries in the footer: [title]: [link] + # links: + # Uptime: https://uptime.searxng.org/history/darmarit-org + # About: "https://searxng.org" + +search: + # Filter results. 0: None, 1: Moderate, 2: Strict + safe_search: 0 + # Existing autocomplete backends: "360search", "baidu", "brave", "dbpedia", "duckduckgo", "google", "yandex", + # "mwmbl", "naver", "seznam", "sogou", "startpage", "stract", "swisscows", "quark", "qwant", "wikipedia" - + # leave blank to turn it off by default. + autocomplete: "" + # minimun characters to type before autocompleter starts + autocomplete_min: 4 + # backend for the favicon near URL in search results. + # Available resolvers: "allesedv", "duckduckgo", "google", "yandex" - leave blank to turn it off by default. + favicon_resolver: "" + # Default search language - leave blank to detect from browser information or + # use codes from 'languages.py' + default_lang: "auto" + # max_page: 0 # if engine supports paging, 0 means unlimited numbers of pages + # Available languages + # languages: + # - all + # - en + # - en-US + # - de + # - it-IT + # - fr + # - fr-BE + # ban time in seconds after engine errors + ban_time_on_fail: 5 + # max ban time in seconds after engine errors + max_ban_time_on_fail: 120 + suspended_times: + # Engine suspension time after error (in seconds; set to 0 to disable) + # For error "Access denied" and "HTTP error [402, 403]" + SearxEngineAccessDenied: 86400 + # For error "CAPTCHA" + SearxEngineCaptcha: 86400 + # For error "Too many request" and "HTTP error 429" + SearxEngineTooManyRequests: 3600 + # Cloudflare CAPTCHA + cf_SearxEngineCaptcha: 1296000 + cf_SearxEngineAccessDenied: 86400 + # ReCAPTCHA + recaptcha_SearxEngineCaptcha: 604800 + + # remove format to deny access, use lower case. + # formats: [html, csv, json, rss] + formats: + - html + - json + +server: + # Is overwritten by ${SEARXNG_PORT} and ${SEARXNG_BIND_ADDRESS} + port: 8888 + bind_address: "127.0.0.1" + # public URL of the instance, to ensure correct inbound links. Is overwritten + # by ${SEARXNG_BASE_URL}. + base_url: false # "http://example.com/location" + # rate limit the number of request on the instance, block some bots. + # Is overwritten by ${SEARXNG_LIMITER} + limiter: false + # enable features designed only for public instances. + # Is overwritten by ${SEARXNG_PUBLIC_INSTANCE} + public_instance: false + + # If your instance owns a /etc/searxng/settings.yml file, then set the following + # values there. + + secret_key: "OipACc2YIAaK9cLLtjJuTMU43n6F4EI" # Is overwritten by ${SEARXNG_SECRET} + # Proxy image results through SearXNG. Is overwritten by ${SEARXNG_IMAGE_PROXY} + image_proxy: false + # 1.0 and 1.1 are supported + http_protocol_version: "1.0" + # POST queries are more secure as they don't show up in history but may cause + # problems when using Firefox containers. + # Is overwritten by ${SEARXNG_METHOD} + method: "POST" + default_http_headers: + X-Content-Type-Options: nosniff + X-Download-Options: noopen + X-Robots-Tag: noindex, nofollow + Referrer-Policy: no-referrer + +valkey: + # URL to connect valkey database. Is overwritten by ${SEARXNG_VALKEY_URL}. + # https://docs.searxng.org/admin/settings/settings_valkey.html#settings-valkey + # url: valkey://localhost:6379/0 + url: false + +ui: + # Custom static path - leave it blank if you didn't change + static_path: "" + # Custom templates path - leave it blank if you didn't change + templates_path: "" + # query_in_title: When true, the result page's titles contains the query + # it decreases the privacy, since the browser can records the page titles. + query_in_title: false + # infinite_scroll: When true, automatically loads the next page when scrolling to bottom of the current page. + infinite_scroll: false + # ui theme + default_theme: simple + # center the results ? + center_alignment: false + # URL prefix of the internet archive, don't forget trailing slash (if needed). + # cache_url: "https://webcache.googleusercontent.com/search?q=cache:" + # Default interface locale - leave blank to detect from browser information or + # use codes from the 'locales' config section + default_locale: "" + # Open result links in a new tab by default + # results_on_new_tab: false + theme_args: + # style of simple theme: auto, light, dark + simple_style: auto + # Perform search immediately if a category selected. + # Disable to select multiple categories at once and start the search manually. + search_on_category_select: true + # Hotkeys: default or vim + hotkeys: default + # URL formatting: pretty, full or host + url_formatting: pretty + +# Lock arbitrary settings on the preferences page. +# +# preferences: +# lock: +# - categories +# - language +# - autocomplete +# - favicon +# - safesearch +# - method +# - doi_resolver +# - locale +# - theme +# - results_on_new_tab +# - infinite_scroll +# - search_on_category_select +# - method +# - image_proxy +# - query_in_title + +# communication with search engines +# +outgoing: + # default timeout in seconds, can be override by engine + request_timeout: 3.0 + # the maximum timeout in seconds + # max_request_timeout: 10.0 + # suffix of searxng_useragent, could contain information like an email address + # to the administrator + useragent_suffix: "" + # The maximum number of concurrent connections that may be established. + pool_connections: 100 + # Allow the connection pool to maintain keep-alive connections below this + # point. + pool_maxsize: 20 + # See https://www.python-httpx.org/http2/ + enable_http2: true + # uncomment below section if you want to use a custom server certificate + # see https://www.python-httpx.org/advanced/#changing-the-verification-defaults + # and https://www.python-httpx.org/compatibility/#ssl-configuration + # verify: ~/.mitmproxy/mitmproxy-ca-cert.cer + # + # uncomment below section if you want to use a proxyq see: SOCKS proxies + # https://2.python-requests.org/en/latest/user/advanced/#proxies + # are also supported: see + # https://2.python-requests.org/en/latest/user/advanced/#socks + # + # proxies: + # all://: + # - http://proxy1:8080 + # - http://proxy2:8080 + # + # using_tor_proxy: true + # + # Extra seconds to add in order to account for the time taken by the proxy + # + # extra_proxy_timeout: 10 + # + # uncomment below section only if you have more than one network interface + # which can be the source of outgoing search requests + # + # source_ips: + # - 1.1.1.1 + # - 1.1.1.2 + # - fe80::/126 + +# Plugin configuration, for more details see +# https://docs.searxng.org/admin/settings/settings_plugins.html +# +plugins: + + searx.plugins.calculator.SXNGPlugin: + active: true + + searx.plugins.hash_plugin.SXNGPlugin: + active: true + + searx.plugins.self_info.SXNGPlugin: + active: true + + searx.plugins.unit_converter.SXNGPlugin: + active: true + + searx.plugins.ahmia_filter.SXNGPlugin: + active: true + + searx.plugins.hostnames.SXNGPlugin: + active: true + + searx.plugins.oa_doi_rewrite.SXNGPlugin: + active: false + + searx.plugins.tor_check.SXNGPlugin: + active: false + + searx.plugins.tracker_url_remover.SXNGPlugin: + active: true + + +# Configuration of the "Hostnames plugin": +# +# hostnames: +# replace: +# '(.*\.)?youtube\.com$': 'yt.example.com' +# '(.*\.)?youtu\.be$': 'yt.example.com' +# '(.*\.)?reddit\.com$': 'teddit.example.com' +# '(.*\.)?redd\.it$': 'teddit.example.com' +# '(www\.)?twitter\.com$': 'nitter.example.com' +# remove: +# - '(.*\.)?facebook.com$' +# low_priority: +# - '(.*\.)?google(\..*)?$' +# high_priority: +# - '(.*\.)?wikipedia.org$' +# +# Alternatively you can use external files for configuring the "Hostnames plugin": +# +# hostnames: +# replace: 'rewrite-hosts.yml' +# +# Content of 'rewrite-hosts.yml' (place the file in the same directory as 'settings.yml'): +# '(.*\.)?youtube\.com$': 'yt.example.com' +# '(.*\.)?youtu\.be$': 'yt.example.com' +# + +checker: + # disable checker when in debug mode + off_when_debug: true + + # use "scheduling: {}" to disable scheduling + # scheduling: interval or int + + # to activate the scheduler: + # * uncomment "scheduling" section + # * add "cache2 = name=searxngcache,items=2000,blocks=2000,blocksize=4096,bitmap=1" + # to your uwsgi.ini + + # scheduling: + # start_after: [300, 1800] # delay to start the first run of the checker + # every: [86400, 90000] # how often the checker runs + + # additional tests: only for the YAML anchors (see the engines section) + # + additional_tests: + rosebud: &test_rosebud + matrix: + query: rosebud + lang: en + result_container: + - not_empty + - ['one_title_contains', 'citizen kane'] + test: + - unique_results + + android: &test_android + matrix: + query: ['android'] + lang: ['en', 'de', 'fr', 'zh-CN'] + result_container: + - not_empty + - ['one_title_contains', 'google'] + test: + - unique_results + + # tests: only for the YAML anchors (see the engines section) + tests: + infobox: &tests_infobox + infobox: + matrix: + query: ["linux", "new york", "bbc"] + result_container: + - has_infobox + +categories_as_tabs: + general: + images: + videos: + news: + map: + music: + it: + science: + files: + social media: + +engines: + - name: 360search + engine: 360search + shortcut: 360so + disabled: true + + - name: 360search videos + engine: 360search_videos + shortcut: 360sov + disabled: true + + - name: 9gag + engine: 9gag + shortcut: 9g + disabled: true + + - name: acfun + engine: acfun + shortcut: acf + disabled: true + + - name: adobe stock + engine: adobe_stock + shortcut: asi + categories: ["images"] + # https://docs.searxng.org/dev/engines/online/adobe_stock.html + adobe_order: relevance + adobe_content_types: ["photo", "illustration", "zip_vector", "template", "3d", "image"] + timeout: 6 + disabled: true + + - name: adobe stock video + engine: adobe_stock + shortcut: asv + network: adobe stock + categories: ["videos"] + adobe_order: relevance + adobe_content_types: ["video"] + timeout: 6 + disabled: true + + - name: adobe stock audio + engine: adobe_stock + shortcut: asa + network: adobe stock + categories: ["music"] + adobe_order: relevance + adobe_content_types: ["audio"] + timeout: 6 + disabled: true + + - name: alexandria + engine: json_engine + shortcut: alx + categories: general + paging: true + search_url: https://api.alexandria.org/?a=1&q={query}&p={pageno} + results_query: results + title_query: title + url_query: url + content_query: snippet + timeout: 1.5 + disabled: true + about: + website: https://alexandria.org/ + official_api_documentation: https://github.com/alexandria-org/alexandria-api/raw/master/README.md + use_official_api: true + require_api_key: false + results: JSON + + # - name: astrophysics data system + # engine: astrophysics_data_system + # sort: asc + # weight: 5 + # categories: [science] + # api_key: your-new-key + # shortcut: ads + + - name: alpine linux packages + engine: alpinelinux + disabled: true + shortcut: alp + + - name: annas archive + engine: annas_archive + disabled: true + shortcut: aa + + - name: ansa + engine: ansa + shortcut: ans + disabled: true + + # - name: annas articles + # engine: annas_archive + # shortcut: aaa + # # https://docs.searxng.org/dev/engines/online/annas_archive.html + # aa_content: 'magazine' # book_fiction, book_unknown, book_nonfiction, book_comic + # aa_ext: 'pdf' # pdf, epub, .. + # aa_sort: oldest' # newest, oldest, largest, smallest + + - name: apk mirror + engine: apkmirror + timeout: 4.0 + shortcut: apkm + disabled: true + + - name: apple app store + engine: apple_app_store + shortcut: aps + disabled: true + + # Requires Tor + - name: ahmia + engine: ahmia + categories: onions + enable_http: true + shortcut: ah + + - name: anaconda + engine: xpath + paging: true + first_page_num: 0 + search_url: https://anaconda.org/search?q={query}&page={pageno} + results_xpath: //tbody/tr + url_xpath: ./td/h5/a[last()]/@href + title_xpath: ./td/h5 + content_xpath: ./td[h5]/text() + categories: it + timeout: 6.0 + shortcut: conda + disabled: true + + - name: arch linux wiki + engine: archlinux + shortcut: al + + - name: nixos wiki + engine: mediawiki + shortcut: nixw + base_url: https://wiki.nixos.org/ + search_type: text + disabled: true + categories: [it, software wikis] + + - name: artic + engine: artic + shortcut: arc + timeout: 4.0 + + - name: arxiv + engine: arxiv + shortcut: arx + timeout: 4.0 + + - name: ask + engine: ask + shortcut: ask + disabled: true + + # tmp suspended: dh key too small + # - name: base + # engine: base + # shortcut: bs + + - name: bandcamp + engine: bandcamp + shortcut: bc + categories: music + + - name: baidu + baidu_category: general + categories: [general] + engine: baidu + shortcut: bd + disabled: false + + - name: baidu images + baidu_category: images + categories: [images] + engine: baidu + shortcut: bdi + disabled: false + + - name: baidu kaifa + baidu_category: it + categories: [it] + engine: baidu + shortcut: bdk + disabled: false + + - name: wikipedia + engine: wikipedia + shortcut: wp + # add "list" to the array to get results in the results list + display_type: ["infobox"] + categories: [general] + disabled: true + + - name: bilibili + engine: bilibili + shortcut: bil + disabled: true + + - name: bing + engine: bing + shortcut: bi + disabled: false + + - name: bing images + engine: bing_images + shortcut: bii + + - name: bing news + engine: bing_news + shortcut: bin + + - name: bing videos + engine: bing_videos + shortcut: biv + + - name: bitchute + engine: bitchute + shortcut: bit + disabled: true + + - name: bitbucket + engine: xpath + paging: true + search_url: https://bitbucket.org/repo/all/{pageno}?name={query} + url_xpath: //article[@class="repo-summary"]//a[@class="repo-link"]/@href + title_xpath: //article[@class="repo-summary"]//a[@class="repo-link"] + content_xpath: //article[@class="repo-summary"]/p + categories: [it, repos] + timeout: 4.0 + disabled: true + shortcut: bb + about: + website: https://bitbucket.org/ + wikidata_id: Q2493781 + official_api_documentation: https://developer.atlassian.com/bitbucket + use_official_api: false + require_api_key: false + results: HTML + + - name: bpb + engine: bpb + shortcut: bpb + disabled: true + + - name: btdigg + engine: btdigg + shortcut: bt + disabled: true + + - name: openverse + engine: openverse + categories: images + shortcut: opv + + - name: media.ccc.de + engine: ccc_media + shortcut: c3tv + # We don't set language: de here because media.ccc.de is not just + # for a German audience. It contains many English videos and many + # German videos have English subtitles. + disabled: true + + - name: chefkoch + engine: chefkoch + shortcut: chef + # to show premium or plus results too: + # skip_premium: false + + # WARNING: links from chinaso.com voilate users privacy + # Before activate these engines its mandatory to read + # - https://github.com/searxng/searxng/issues/4694 + # - https://docs.searxng.org/dev/engines/online/chinaso.html + + - name: chinaso news + engine: chinaso + shortcut: chinaso + categories: [news] + chinaso_category: news + chinaso_news_source: all + disabled: true + inactive: true + + - name: chinaso images + engine: chinaso + network: chinaso news + shortcut: chinasoi + categories: [images] + chinaso_category: images + disabled: true + inactive: true + + - name: chinaso videos + engine: chinaso + network: chinaso news + shortcut: chinasov + categories: [videos] + chinaso_category: videos + disabled: true + inactive: true + + - name: cloudflareai + engine: cloudflareai + shortcut: cfai + # get api token and accont id from https://developers.cloudflare.com/workers-ai/get-started/rest-api/ + cf_account_id: 'your_cf_accout_id' + cf_ai_api: 'your_cf_api' + # create your ai gateway by https://developers.cloudflare.com/ai-gateway/get-started/creating-gateway/ + cf_ai_gateway: 'your_cf_ai_gateway_name' + # find the model name from https://developers.cloudflare.com/workers-ai/models/#text-generation + cf_ai_model: 'ai_model_name' + # custom your preferences + # cf_ai_model_display_name: 'Cloudflare AI' + # cf_ai_model_assistant: 'prompts_for_assistant_role' + # cf_ai_model_system: 'prompts_for_system_role' + timeout: 30 + disabled: true + + # - name: core.ac.uk + # engine: core + # categories: science + # shortcut: cor + # # get your API key from: https://core.ac.uk/api-keys/register/ + # api_key: 'unset' + + - name: cppreference + engine: cppreference + shortcut: cpp + paging: false + disabled: true + + - name: crossref + engine: crossref + shortcut: cr + timeout: 30 + disabled: true + + - name: crowdview + engine: json_engine + shortcut: cv + categories: general + paging: false + search_url: https://crowdview-next-js.onrender.com/api/search-v3?query={query} + results_query: results + url_query: link + title_query: title + content_query: snippet + title_html_to_text: true + content_html_to_text: true + disabled: true + about: + website: https://crowdview.ai/ + + - name: yep + engine: yep + shortcut: yep + categories: general + search_type: web + timeout: 5 + disabled: true + + - name: yep images + engine: yep + shortcut: yepi + categories: images + search_type: images + disabled: true + + - name: yep news + engine: yep + shortcut: yepn + categories: news + search_type: news + disabled: true + + - name: currency + engine: currency_convert + categories: general + shortcut: cc + + - name: deezer + engine: deezer + shortcut: dz + disabled: true + + - name: destatis + engine: destatis + shortcut: destat + disabled: true + + - name: deviantart + engine: deviantart + shortcut: da + timeout: 3.0 + + - name: ddg definitions + engine: duckduckgo_definitions + shortcut: ddd + weight: 2 + disabled: true + tests: *tests_infobox + + # cloudflare protected + # - name: digbt + # engine: digbt + # shortcut: dbt + # timeout: 6.0 + # disabled: true + + - name: docker hub + engine: docker_hub + shortcut: dh + categories: [it, packages] + + - name: encyclosearch + engine: json_engine + shortcut: es + categories: general + paging: true + search_url: https://encyclosearch.org/encyclosphere/search?q={query}&page={pageno}&resultsPerPage=15 + results_query: Results + url_query: SourceURL + title_query: Title + content_query: Description + disabled: true + about: + website: https://encyclosearch.org + official_api_documentation: https://encyclosearch.org/docs/#/rest-api + use_official_api: true + require_api_key: false + results: JSON + + - name: erowid + engine: xpath + paging: true + first_page_num: 0 + page_size: 30 + search_url: https://www.erowid.org/search.php?q={query}&s={pageno} + url_xpath: //dl[@class="results-list"]/dt[@class="result-title"]/a/@href + title_xpath: //dl[@class="results-list"]/dt[@class="result-title"]/a/text() + content_xpath: //dl[@class="results-list"]/dd[@class="result-details"] + categories: [] + shortcut: ew + disabled: true + about: + website: https://www.erowid.org/ + wikidata_id: Q1430691 + official_api_documentation: + use_official_api: false + require_api_key: false + results: HTML + + # - name: elasticsearch + # shortcut: els + # engine: elasticsearch + # base_url: http://localhost:9200 + # username: elastic + # password: changeme + # index: my-index + # enable_http: true + # # available options: match, simple_query_string, term, terms, custom + # query_type: match + # # if query_type is set to custom, provide your query here + # # custom_query_json: {"query":{"match_all": {}}} + # # show_metadata: false + # disabled: true + + - name: wikidata + engine: wikidata + shortcut: wd + timeout: 3.0 + weight: 2 + # add "list" to the array to get results in the results list + display_type: ["infobox"] + tests: *tests_infobox + categories: [general] + disabled: true + + - name: duckduckgo + engine: duckduckgo + shortcut: ddg + disabled: true + + - name: duckduckgo images + engine: duckduckgo_extra + categories: [images, web] + ddg_category: images + shortcut: ddi + disabled: true + + - name: duckduckgo videos + engine: duckduckgo_extra + categories: [videos, web] + ddg_category: videos + shortcut: ddv + disabled: true + + - name: duckduckgo news + engine: duckduckgo_extra + categories: [news, web] + ddg_category: news + shortcut: ddn + disabled: true + + - name: duckduckgo weather + engine: duckduckgo_weather + shortcut: ddw + disabled: true + + - name: apple maps + engine: apple_maps + shortcut: apm + disabled: true + timeout: 5.0 + + - name: emojipedia + engine: emojipedia + timeout: 4.0 + shortcut: em + disabled: true + + - name: tineye + engine: tineye + shortcut: tin + timeout: 9.0 + disabled: true + + - name: etymonline + engine: xpath + paging: true + search_url: https://etymonline.com/search?page={pageno}&q={query} + url_xpath: //a[contains(@class, "word__name--")]/@href + title_xpath: //a[contains(@class, "word__name--")] + content_xpath: //section[contains(@class, "word__defination")] + first_page_num: 1 + shortcut: et + categories: [dictionaries] + about: + website: https://www.etymonline.com/ + wikidata_id: Q1188617 + official_api_documentation: + use_official_api: false + require_api_key: false + results: HTML + + # - name: ebay + # engine: ebay + # shortcut: eb + # base_url: 'https://www.ebay.com' + # disabled: true + # timeout: 5 + + - name: 1x + engine: www1x + shortcut: 1x + timeout: 3.0 + disabled: true + + - name: fdroid + engine: fdroid + shortcut: fd + disabled: true + + - name: findthatmeme + engine: findthatmeme + shortcut: ftm + disabled: true + + - name: flickr + categories: images + shortcut: fl + # You can use the engine using the official stable API, but you need an API + # key, see: https://www.flickr.com/services/apps/create/ + # engine: flickr + # api_key: 'apikey' # required! + # Or you can use the html non-stable engine, activated by default + engine: flickr_noapi + + - name: free software directory + engine: mediawiki + shortcut: fsd + categories: [it, software wikis] + base_url: https://directory.fsf.org/ + search_type: title + timeout: 5.0 + disabled: true + about: + website: https://directory.fsf.org/ + wikidata_id: Q2470288 + + # - name: freesound + # engine: freesound + # shortcut: fnd + # disabled: true + # timeout: 15.0 + # API key required, see: https://freesound.org/docs/api/overview.html + # api_key: MyAPIkey + + - name: frinkiac + engine: frinkiac + shortcut: frk + disabled: true + + - name: fyyd + engine: fyyd + shortcut: fy + timeout: 8.0 + disabled: true + + - name: geizhals + engine: geizhals + shortcut: geiz + disabled: true + + - name: genius + engine: genius + shortcut: gen + + - name: gentoo + engine: mediawiki + shortcut: ge + categories: ["it", "software wikis"] + base_url: "https://wiki.gentoo.org/" + api_path: "api.php" + search_type: text + timeout: 10 + + - name: gitlab + engine: gitlab + base_url: https://gitlab.com + shortcut: gl + disabled: true + about: + website: https://gitlab.com/ + wikidata_id: Q16639197 + + # - name: gnome + # engine: gitlab + # base_url: https://gitlab.gnome.org + # shortcut: gn + # about: + # website: https://gitlab.gnome.org + # wikidata_id: Q44316 + + - name: github + engine: github + shortcut: gh + + - name: codeberg + # https://docs.searxng.org/dev/engines/online/gitea.html + engine: gitea + base_url: https://codeberg.org + shortcut: cb + disabled: true + + - name: gitea.com + engine: gitea + base_url: https://gitea.com + shortcut: gitea + disabled: true + + - name: goodreads + engine: goodreads + shortcut: good + timeout: 4.0 + disabled: true + + - name: google + engine: google + shortcut: go + # additional_tests: + # android: *test_android + disabled: true + + - name: google images + engine: google_images + shortcut: goi + # additional_tests: + # android: *test_android + # dali: + # matrix: + # query: ['Dali Christ'] + # lang: ['en', 'de', 'fr', 'zh-CN'] + # result_container: + # - ['one_title_contains', 'Salvador'] + + - name: google news + engine: google_news + shortcut: gon + # additional_tests: + # android: *test_android + + - name: google videos + engine: google_videos + shortcut: gov + # additional_tests: + # android: *test_android + + - name: google scholar + engine: google_scholar + shortcut: gos + + - name: google play apps + engine: google_play + categories: [files, apps] + shortcut: gpa + play_categ: apps + disabled: true + + - name: google play movies + engine: google_play + categories: videos + shortcut: gpm + play_categ: movies + disabled: true + + - name: material icons + engine: material_icons + shortcut: mi + disabled: true + + - name: habrahabr + engine: xpath + paging: true + search_url: https://habr.com/en/search/page{pageno}/?q={query} + results_xpath: //article[contains(@class, "tm-articles-list__item")] + url_xpath: .//a[@class="tm-title__link"]/@href + title_xpath: .//a[@class="tm-title__link"] + content_xpath: .//div[contains(@class, "article-formatted-body")] + categories: it + timeout: 4.0 + disabled: true + shortcut: habr + about: + website: https://habr.com/ + wikidata_id: Q4494434 + official_api_documentation: https://habr.com/en/docs/help/api/ + use_official_api: false + require_api_key: false + results: HTML + + - name: hackernews + engine: hackernews + shortcut: hn + disabled: true + + - name: hex + engine: hex + shortcut: hex + disabled: true + # Valid values: name inserted_at updated_at total_downloads recent_downloads + sort_criteria: "recent_downloads" + page_size: 10 + + - name: crates.io + engine: crates + shortcut: crates + disabled: true + timeout: 6.0 + + - name: hoogle + engine: xpath + search_url: https://hoogle.haskell.org/?hoogle={query} + results_xpath: '//div[@class="result"]' + title_xpath: './/div[@class="ans"]//a' + url_xpath: './/div[@class="ans"]//a/@href' + content_xpath: './/div[@class="from"]' + page_size: 20 + categories: [it, packages] + shortcut: ho + about: + website: https://hoogle.haskell.org/ + wikidata_id: Q34010 + official_api_documentation: https://hackage.haskell.org/api + use_official_api: false + require_api_key: false + results: JSON + + - name: il post + engine: il_post + shortcut: pst + disabled: true + + - name: huggingface + engine: huggingface + shortcut: hf + disabled: true + + - name: huggingface datasets + huggingface_endpoint: datasets + engine: huggingface + shortcut: hfd + disabled: true + + - name: huggingface spaces + huggingface_endpoint: spaces + engine: huggingface + shortcut: hfs + disabled: true + + - name: imdb + engine: imdb + shortcut: imdb + timeout: 6.0 + disabled: true + + - name: imgur + engine: imgur + shortcut: img + disabled: true + + - name: ina + engine: ina + shortcut: in + timeout: 6.0 + disabled: true + + # - name: invidious + # engine: invidious + # # if you want to use invidious with SearXNG you should setup one locally + # # https://github.com/searxng/searxng/issues/2722#issuecomment-2884993248 + # base_url: + # - https://invidious.example1.com + # - https://invidious.example2.com + # shortcut: iv + # timeout: 3.0 + + - name: ipernity + engine: ipernity + shortcut: ip + disabled: true + + - name: iqiyi + engine: iqiyi + shortcut: iq + disabled: true + + - name: jisho + engine: jisho + shortcut: js + timeout: 3.0 + disabled: true + + - name: kickass + engine: kickass + base_url: + - https://kickasstorrents.to + - https://kickasstorrents.cr + - https://kickasstorrent.cr + - https://kickass.sx + - https://kat.am + shortcut: kc + timeout: 4.0 + + - name: lemmy communities + engine: lemmy + lemmy_type: Communities + shortcut: leco + + - name: lemmy users + engine: lemmy + network: lemmy communities + lemmy_type: Users + shortcut: leus + + - name: lemmy posts + engine: lemmy + network: lemmy communities + lemmy_type: Posts + shortcut: lepo + + - name: lemmy comments + engine: lemmy + network: lemmy communities + lemmy_type: Comments + shortcut: lecom + + - name: library genesis + engine: xpath + # search_url: https://libgen.is/search.php?req={query} + search_url: https://libgen.rs/search.php?req={query} + url_xpath: //a[contains(@href,"book/index.php?md5")]/@href + title_xpath: //a[contains(@href,"book/")]/text()[1] + content_xpath: //td/a[1][contains(@href,"=author")]/text() + categories: files + timeout: 7.0 + disabled: true + shortcut: lg + about: + website: https://libgen.fun/ + wikidata_id: Q22017206 + official_api_documentation: + use_official_api: false + require_api_key: false + results: HTML + + - name: z-library + engine: zlibrary + shortcut: zlib + categories: files + timeout: 7.0 + disabled: true + + - name: library of congress + engine: loc + shortcut: loc + categories: images + disabled: true + + - name: libretranslate + engine: libretranslate + # https://github.com/LibreTranslate/LibreTranslate?tab=readme-ov-file#mirrors + base_url: + - https://libretranslate.com/translate + # api_key: abc123 + shortcut: lt + disabled: true + + - name: lingva + engine: lingva + shortcut: lv + # set lingva instance in url, by default it will use the official instance + # url: https://lingva.thedaviddelta.com + + - name: lobste.rs + engine: xpath + search_url: https://lobste.rs/search?q={query}&what=stories&order=relevance + results_xpath: //li[contains(@class, "story")] + url_xpath: .//a[@class="u-url"]/@href + title_xpath: .//a[@class="u-url"] + content_xpath: .//a[@class="domain"] + categories: it + shortcut: lo + timeout: 5.0 + disabled: true + about: + website: https://lobste.rs/ + wikidata_id: Q60762874 + official_api_documentation: + use_official_api: false + require_api_key: false + results: HTML + + - name: mastodon users + engine: mastodon + mastodon_type: accounts + base_url: https://mastodon.social + shortcut: mau + + - name: mastodon hashtags + engine: mastodon + mastodon_type: hashtags + base_url: https://mastodon.social + shortcut: mah + + # - name: matrixrooms + # engine: mrs + # # https://docs.searxng.org/dev/engines/online/mrs.html + # # base_url: https://mrs-api-host + # shortcut: mtrx + # disabled: true + + - name: mdn + shortcut: mdn + engine: json_engine + categories: [it] + paging: true + search_url: https://developer.mozilla.org/api/v1/search?q={query}&page={pageno} + results_query: documents + url_query: mdn_url + url_prefix: https://developer.mozilla.org + title_query: title + content_query: summary + about: + website: https://developer.mozilla.org + wikidata_id: Q3273508 + official_api_documentation: null + use_official_api: false + require_api_key: false + results: JSON + + - name: metacpan + engine: metacpan + shortcut: cpan + disabled: true + number_of_results: 20 + + # https://docs.searxng.org/dev/engines/offline/search-indexer-engines.html#module-searx.engines.meilisearch + # - name: meilisearch + # engine: meilisearch + # shortcut: mes + # enable_http: true + # base_url: http://localhost:7700 + # index: my-index + # auth_key: Bearer XXXX + + - name: microsoft learn + engine: microsoft_learn + shortcut: msl + disabled: true + + - name: mixcloud + engine: mixcloud + shortcut: mc + + # MongoDB engine + # Required dependency: pymongo + # - name: mymongo + # engine: mongodb + # shortcut: md + # exact_match_only: false + # host: '127.0.0.1' + # port: 27017 + # enable_http: true + # results_per_page: 20 + # database: 'business' + # collection: 'reviews' # name of the db collection + # key: 'name' # key in the collection to search for + + - name: mozhi + engine: mozhi + base_url: + - https://mozhi.aryak.me + - https://translate.bus-hit.me + - https://nyc1.mz.ggtyler.dev + # mozhi_engine: google - see https://mozhi.aryak.me for supported engines + timeout: 4.0 + shortcut: mz + disabled: true + + - name: mwmbl + engine: mwmbl + # api_url: https://api.mwmbl.org + shortcut: mwm + disabled: true + + - name: niconico + engine: niconico + shortcut: nico + disabled: true + + - name: npm + engine: npm + shortcut: npm + timeout: 5.0 + disabled: true + + - name: nyaa + engine: nyaa + shortcut: nt + disabled: true + + - name: mankier + engine: json_engine + search_url: https://www.mankier.com/api/v2/mans/?q={query} + results_query: results + url_query: url + title_query: name + content_query: description + categories: it + shortcut: man + about: + website: https://www.mankier.com/ + official_api_documentation: https://www.mankier.com/api + use_official_api: true + require_api_key: false + results: JSON + + # https://docs.searxng.org/dev/engines/online/mullvad_leta.html + - name: mullvadleta + engine: mullvad_leta + disabled: true + leta_engine: google + categories: [general, web] + shortcut: ml + + - name: mullvadleta brave + engine: mullvad_leta + network: mullvadleta + disabled: true + leta_engine: brave + categories: [general, web] + shortcut: mlb + disabled: true + + - name: odysee + engine: odysee + shortcut: od + disabled: true + + - name: ollama + engine: ollama + shortcut: ollama + disabled: true + + - name: openairedatasets + engine: json_engine + paging: true + search_url: https://api.openaire.eu/search/datasets?format=json&page={pageno}&size=10&title={query} + results_query: response/results/result + url_query: metadata/oaf:entity/oaf:result/children/instance/webresource/url/$ + title_query: metadata/oaf:entity/oaf:result/title/$ + content_query: metadata/oaf:entity/oaf:result/description/$ + content_html_to_text: true + categories: "science" + shortcut: oad + timeout: 5.0 + about: + website: https://www.openaire.eu/ + wikidata_id: Q25106053 + official_api_documentation: https://api.openaire.eu/ + use_official_api: false + require_api_key: false + results: JSON + + - name: openairepublications + engine: json_engine + paging: true + search_url: https://api.openaire.eu/search/publications?format=json&page={pageno}&size=10&title={query} + results_query: response/results/result + url_query: metadata/oaf:entity/oaf:result/children/instance/webresource/url/$ + title_query: metadata/oaf:entity/oaf:result/title/$ + content_query: metadata/oaf:entity/oaf:result/description/$ + content_html_to_text: true + categories: science + shortcut: oap + timeout: 5.0 + about: + website: https://www.openaire.eu/ + wikidata_id: Q25106053 + official_api_documentation: https://api.openaire.eu/ + use_official_api: false + require_api_key: false + results: JSON + + - name: openclipart + engine: openclipart + shortcut: ocl + inactive: true + disabled: true + timeout: 30 + + - name: openlibrary + engine: openlibrary + shortcut: ol + timeout: 5 + disabled: true + + - name: openmeteo + engine: open_meteo + shortcut: om + disabled: true + + # - name: opensemanticsearch + # engine: opensemantic + # shortcut: oss + # base_url: 'http://localhost:8983/solr/opensemanticsearch/' + + - name: openstreetmap + engine: openstreetmap + shortcut: osm + + - name: openrepos + engine: xpath + paging: true + search_url: https://openrepos.net/search/node/{query}?page={pageno} + url_xpath: //li[@class="search-result"]//h3[@class="title"]/a/@href + title_xpath: //li[@class="search-result"]//h3[@class="title"]/a + content_xpath: //li[@class="search-result"]//div[@class="search-snippet-info"]//p[@class="search-snippet"] + categories: files + timeout: 4.0 + disabled: true + shortcut: or + about: + website: https://openrepos.net/ + wikidata_id: + official_api_documentation: + use_official_api: false + require_api_key: false + results: HTML + + - name: packagist + engine: json_engine + paging: true + search_url: https://packagist.org/search.json?q={query}&page={pageno} + results_query: results + url_query: url + title_query: name + content_query: description + categories: [it, packages] + disabled: true + timeout: 5.0 + shortcut: pack + about: + website: https://packagist.org + wikidata_id: Q108311377 + official_api_documentation: https://packagist.org/apidoc + use_official_api: true + require_api_key: false + results: JSON + + - name: pdbe + engine: pdbe + shortcut: pdb + # Hide obsolete PDB entries. Default is not to hide obsolete structures + # hide_obsolete: false + + - name: photon + engine: photon + shortcut: ph + + - name: pinterest + engine: pinterest + shortcut: pin + + - name: piped + engine: piped + shortcut: ppd + categories: videos + piped_filter: videos + timeout: 3.0 + + # URL to use as link and for embeds + frontend_url: https://srv.piped.video + # Instance will be selected randomly, for more see https://piped-instances.kavin.rocks/ + backend_url: + - https://pipedapi.adminforge.de + - https://pipedapi.nosebs.ru + - https://pipedapi.ducks.party + - https://pipedapi.reallyaweso.me + - https://api.piped.private.coffee + - https://pipedapi.darkness.services + + - name: piped.music + engine: piped + network: piped + shortcut: ppdm + categories: music + piped_filter: music_songs + timeout: 3.0 + + - name: piratebay + engine: piratebay + shortcut: tpb + # You may need to change this URL to a proxy if piratebay is blocked in your + # country + url: https://thepiratebay.org/ + timeout: 3.0 + + - name: pixabay images + engine: pixabay + pixabay_type: images + categories: images + shortcut: pixi + disabled: true + + - name: pixabay videos + engine: pixabay + pixabay_type: videos + categories: videos + shortcut: pixv + disabled: true + + - name: pixiv + shortcut: pv + engine: pixiv + disabled: true + inactive: true + pixiv_image_proxies: + - https://pximg.example.org + # A proxy is required to load the images. Hosting an image proxy server + # for Pixiv: + # --> https://pixivfe.pages.dev/hosting-image-proxy-server/ + # Proxies from public instances. Ask the public instances owners if they + # agree to receive traffic from SearXNG! + # --> https://codeberg.org/VnPower/PixivFE#instances + # --> https://github.com/searxng/searxng/pull/3192#issuecomment-1941095047 + # image proxy of https://pixiv.cat + # - https://i.pixiv.cat + # image proxy of https://www.pixiv.pics + # - https://pximg.cocomi.eu.org + # image proxy of https://pixivfe.exozy.me + # - https://pximg.exozy.me + # image proxy of https://pixivfe.ducks.party + # - https://pixiv.ducks.party + # image proxy of https://pixiv.perennialte.ch + # - https://pximg.perennialte.ch + + - name: podcastindex + engine: podcastindex + shortcut: podcast + + # Required dependency: psychopg2 + # - name: postgresql + # engine: postgresql + # database: postgres + # username: postgres + # password: postgres + # limit: 10 + # query_str: 'SELECT * from my_table WHERE my_column = %(query)s' + # shortcut : psql + + - name: presearch + engine: presearch + search_type: search + categories: [general, web] + shortcut: ps + timeout: 4.0 + disabled: true + + - name: presearch images + engine: presearch + network: presearch + search_type: images + categories: [images, web] + timeout: 4.0 + shortcut: psimg + disabled: true + + - name: presearch videos + engine: presearch + network: presearch + search_type: videos + categories: [general, web] + timeout: 4.0 + shortcut: psvid + disabled: true + + - name: presearch news + engine: presearch + network: presearch + search_type: news + categories: [news, web] + timeout: 4.0 + shortcut: psnews + disabled: true + + - name: pub.dev + engine: xpath + shortcut: pd + search_url: https://pub.dev/packages?q={query}&page={pageno} + paging: true + results_xpath: //div[contains(@class,"packages-item")] + url_xpath: ./div/h3/a/@href + title_xpath: ./div/h3/a + content_xpath: ./div/div/div[contains(@class,"packages-description")]/span + categories: [packages, it] + timeout: 3.0 + disabled: true + first_page_num: 1 + about: + website: https://pub.dev/ + official_api_documentation: https://pub.dev/help/api + use_official_api: false + require_api_key: false + results: HTML + + - name: public domain image archive + engine: public_domain_image_archive + shortcut: pdia + + - name: pubmed + engine: pubmed + shortcut: pub + timeout: 3.0 + + - name: pypi + shortcut: pypi + engine: pypi + + - name: quark + quark_category: general + categories: [general] + engine: quark + shortcut: qk + disabled: true + + - name: quark images + quark_category: images + categories: [images] + engine: quark + shortcut: qki + disabled: true + + - name: qwant + qwant_categ: web + engine: qwant + shortcut: qw + categories: [general, web] + disabled: true + additional_tests: + rosebud: *test_rosebud + + - name: qwant news + qwant_categ: news + engine: qwant + shortcut: qwn + categories: news + network: qwant + + - name: qwant images + qwant_categ: images + engine: qwant + shortcut: qwi + categories: [images, web] + network: qwant + + - name: qwant videos + qwant_categ: videos + engine: qwant + shortcut: qwv + categories: [videos, web] + network: qwant + + # - name: library + # engine: recoll + # shortcut: lib + # base_url: 'https://recoll.example.org/' + # search_dir: '' + # mount_prefix: /export + # dl_prefix: 'https://download.example.org' + # timeout: 30.0 + # categories: files + # disabled: true + + # - name: recoll library reference + # engine: recoll + # base_url: 'https://recoll.example.org/' + # search_dir: reference + # mount_prefix: /export + # dl_prefix: 'https://download.example.org' + # shortcut: libr + # timeout: 30.0 + # categories: files + # disabled: true + + - name: radio browser + engine: radio_browser + shortcut: rb + + - name: reddit + engine: reddit + shortcut: re + page_size: 25 + disabled: true + + - name: reuters + engine: reuters + shortcut: reu + # https://docs.searxng.org/dev/engines/online/reuters.html + # sort_order = "relevance" + + - name: right dao + engine: xpath + paging: true + page_size: 12 + search_url: https://rightdao.com/search?q={query}&start={pageno} + results_xpath: //div[contains(@class, "description")] + url_xpath: ../div[contains(@class, "title")]/a/@href + title_xpath: ../div[contains(@class, "title")] + content_xpath: . + categories: general + shortcut: rd + disabled: true + about: + website: https://rightdao.com/ + use_official_api: false + require_api_key: false + results: HTML + + - name: rottentomatoes + engine: rottentomatoes + shortcut: rt + disabled: true + + # Required dependency: valkey + # - name: myvalkey + # shortcut : rds + # engine: valkey_server + # exact_match_only: false + # host: '127.0.0.1' + # port: 6379 + # enable_http: true + # password: '' + # db: 0 + + # tmp suspended: bad certificate + # - name: scanr structures + # shortcut: scs + # engine: scanr_structures + # disabled: true + + - name: searchmysite + engine: xpath + shortcut: sms + categories: general + paging: true + search_url: https://searchmysite.net/search/?q={query}&page={pageno} + results_xpath: //div[contains(@class,'search-result')] + url_xpath: .//a[contains(@class,'result-link')]/@href + title_xpath: .//span[contains(@class,'result-title-txt')]/text() + content_xpath: ./p[@id='result-hightlight'] + disabled: true + about: + website: https://searchmysite.net + + - name: selfhst icons + engine: selfhst + shortcut: si + inactive: true + disabled: true + + - name: sepiasearch + engine: sepiasearch + shortcut: sep + + - name: sogou + engine: sogou + shortcut: sogou + disabled: true + + - name: sogou images + engine: sogou_images + shortcut: sogoui + disabled: true + + - name: sogou videos + engine: sogou_videos + shortcut: sogouv + disabled: true + + - name: sogou wechat + engine: sogou_wechat + shortcut: sogouw + disabled: true + + - name: soundcloud + engine: soundcloud + shortcut: sc + + - name: stackoverflow + engine: stackexchange + shortcut: st + api_site: 'stackoverflow' + categories: [it, q&a] + + - name: askubuntu + engine: stackexchange + shortcut: ubuntu + api_site: 'askubuntu' + categories: [it, q&a] + + - name: superuser + engine: stackexchange + shortcut: su + api_site: 'superuser' + categories: [it, q&a] + + - name: discuss.python + engine: discourse + shortcut: dpy + base_url: 'https://discuss.python.org' + categories: [it, q&a] + disabled: true + + - name: caddy.community + engine: discourse + shortcut: caddy + base_url: 'https://caddy.community' + categories: [it, q&a] + disabled: true + + - name: pi-hole.community + engine: discourse + shortcut: pi + categories: [it, q&a] + base_url: 'https://discourse.pi-hole.net' + disabled: true + + - name: searchcode code + engine: searchcode_code + shortcut: scc + disabled: true + + # - name: searx + # engine: searx_engine + # shortcut: se + # instance_urls : + # - http://127.0.0.1:8888/ + # - ... + # disabled: true + + - name: semantic scholar + engine: semantic_scholar + disabled: true + shortcut: se + + # Spotify needs API credentials + # - name: spotify + # engine: spotify + # shortcut: stf + # api_client_id: ******* + # api_client_secret: ******* + + # - name: solr + # engine: solr + # shortcut: slr + # base_url: http://localhost:8983 + # collection: collection_name + # sort: '' # sorting: asc or desc + # field_list: '' # comma separated list of field names to display on the UI + # default_fields: '' # default field to query + # query_fields: '' # query fields + # enable_http: true + + # - name: springer nature + # engine: springer + # # get your API key from: https://dev.springernature.com/signup + # # working API key, for test & debug: "a69685087d07eca9f13db62f65b8f601" + # api_key: 'unset' + # shortcut: springer + # timeout: 15.0 + + - name: startpage + engine: startpage + shortcut: sp + startpage_categ: web + categories: [general, web] + additional_tests: + rosebud: *test_rosebud + disabled: true + + - name: startpage news + engine: startpage + startpage_categ: news + categories: [news, web] + shortcut: spn + disabled: true + + - name: startpage images + engine: startpage + startpage_categ: images + categories: [images, web] + shortcut: spi + disabled: true + + - name: steam + engine: steam + shortcut: stm + disabled: true + + - name: tokyotoshokan + engine: tokyotoshokan + shortcut: tt + timeout: 6.0 + disabled: true + + - name: solidtorrents + engine: solidtorrents + shortcut: solid + timeout: 4.0 + base_url: + - https://solidtorrents.to + - https://bitsearch.to + + # For this demo of the sqlite engine download: + # https://liste.mediathekview.de/filmliste-v2.db.bz2 + # and unpack into searx/data/filmliste-v2.db + # Query to test: "!mediathekview concert" + # + # - name: mediathekview + # engine: sqlite + # shortcut: mediathekview + # categories: [general, videos] + # result_type: MainResult + # database: searx/data/filmliste-v2.db + # query_str: >- + # SELECT title || ' (' || time(duration, 'unixepoch') || ')' AS title, + # COALESCE( NULLIF(url_video_hd,''), NULLIF(url_video_sd,''), url_video) AS url, + # description AS content + # FROM film + # WHERE title LIKE :wildcard OR description LIKE :wildcard + # ORDER BY duration DESC + + - name: tagesschau + engine: tagesschau + # when set to false, display URLs from Tagesschau, and not the actual source + # (e.g. NDR, WDR, SWR, HR, ...) + use_source_url: true + shortcut: ts + disabled: true + + - name: tmdb + engine: xpath + paging: true + categories: movies + search_url: https://www.themoviedb.org/search?page={pageno}&query={query} + results_xpath: //div[contains(@class,"movie") or contains(@class,"tv")]//div[contains(@class,"card")] + url_xpath: .//div[contains(@class,"poster")]/a/@href + thumbnail_xpath: .//img/@src + title_xpath: .//div[contains(@class,"title")]//h2 + content_xpath: .//div[contains(@class,"overview")] + shortcut: tm + disabled: true + + # Requires Tor + - name: torch + engine: xpath + paging: true + search_url: + http://xmh57jrknzkhv6y3ls3ubitzfqnkrwxhopf5aygthi7d6rplyvk3noyd.onion/cgi-bin/omega/omega?P={query}&DEFAULTOP=and + results_xpath: //table//tr + url_xpath: ./td[2]/a + title_xpath: ./td[2]/b + content_xpath: ./td[2]/small + categories: onions + enable_http: true + shortcut: tch + + # TubeArchivist is a self-hosted Youtube archivist software. + # https://docs.searxng.org/dev/engines/online/tubearchivist.html + # + # - name: tubearchivist + # engine: tubearchivist + # shortcut: tuba + # base_url: + # ta_token: + # ta_link_to_mp4: false + + # torznab engine lets you query any torznab compatible indexer. Using this + # engine in combination with Jackett opens the possibility to query a lot of + # public and private indexers directly from SearXNG. More details at: + # https://docs.searxng.org/dev/engines/online/torznab.html + # + # - name: Torznab EZTV + # engine: torznab + # shortcut: eztv + # base_url: http://localhost:9117/api/v2.0/indexers/eztv/results/torznab + # enable_http: true # if using localhost + # api_key: xxxxxxxxxxxxxxx + # show_magnet_links: true + # show_torrent_files: false + # # https://github.com/Jackett/Jackett/wiki/Jackett-Categories + # torznab_categories: # optional + # - 2000 + # - 5000 + + # tmp suspended - too slow, too many errors + # - name: urbandictionary + # engine : xpath + # search_url : https://www.urbandictionary.com/define.php?term={query} + # url_xpath : //*[@class="word"]/@href + # title_xpath : //*[@class="def-header"] + # content_xpath: //*[@class="meaning"] + # shortcut: ud + + - name: unsplash + engine: unsplash + shortcut: us + + - name: yandex + engine: yandex + categories: general + search_type: web + shortcut: yd + disabled: true + inactive: true + + - name: yandex images + engine: yandex + categories: images + search_type: images + shortcut: ydi + disabled: true + inactive: true + + - name: yandex music + engine: yandex_music + shortcut: ydm + disabled: true + # https://yandex.com/support/music/access.html + inactive: true + + - name: yahoo + engine: yahoo + shortcut: yh + disabled: true + + - name: yahoo news + engine: yahoo_news + shortcut: yhn + + - name: youtube + shortcut: yt + # You can use the engine using the official stable API, but you need an API + # key See: https://console.developers.google.com/project + # + # engine: youtube_api + # api_key: 'apikey' # required! + # + # Or you can use the html non-stable engine, activated by default + engine: youtube_noapi + + - name: dailymotion + engine: dailymotion + shortcut: dm + + - name: vimeo + engine: vimeo + shortcut: vm + + - name: wiby + engine: json_engine + paging: true + search_url: https://wiby.me/json/?q={query}&p={pageno} + url_query: URL + title_query: Title + content_query: Snippet + categories: [general, web] + shortcut: wib + disabled: true + about: + website: https://wiby.me/ + + - name: wikibooks + engine: mediawiki + weight: 0.5 + shortcut: wb + categories: [general, wikimedia] + base_url: "https://{language}.wikibooks.org/" + search_type: text + disabled: true + about: + website: https://www.wikibooks.org/ + wikidata_id: Q367 + + - name: wikinews + engine: mediawiki + shortcut: wn + categories: [news, wikimedia] + base_url: "https://{language}.wikinews.org/" + search_type: text + srsort: create_timestamp_desc + about: + website: https://www.wikinews.org/ + wikidata_id: Q964 + disabled: true + + - name: wikiquote + engine: mediawiki + weight: 0.5 + shortcut: wq + categories: [general, wikimedia] + base_url: "https://{language}.wikiquote.org/" + search_type: text + disabled: true + additional_tests: + rosebud: *test_rosebud + about: + website: https://www.wikiquote.org/ + wikidata_id: Q369 + disabled: true + + - name: wikisource + engine: mediawiki + weight: 0.5 + shortcut: ws + categories: [general, wikimedia] + base_url: "https://{language}.wikisource.org/" + search_type: text + disabled: true + about: + website: https://www.wikisource.org/ + wikidata_id: Q263 + disabled: true + + - name: wikispecies + engine: mediawiki + shortcut: wsp + categories: [general, science, wikimedia] + base_url: "https://species.wikimedia.org/" + search_type: text + disabled: true + about: + website: https://species.wikimedia.org/ + wikidata_id: Q13679 + tests: + wikispecies: + matrix: + query: "Campbell, L.I. et al. 2011: MicroRNAs" + lang: en + result_container: + - not_empty + - ['one_title_contains', 'Tardigrada'] + test: + - unique_results + + - name: wiktionary + engine: mediawiki + shortcut: wt + categories: [dictionaries, wikimedia] + base_url: "https://{language}.wiktionary.org/" + search_type: text + about: + website: https://www.wiktionary.org/ + wikidata_id: Q151 + disabled: true + + - name: wikiversity + engine: mediawiki + weight: 0.5 + shortcut: wv + categories: [general, wikimedia] + base_url: "https://{language}.wikiversity.org/" + search_type: text + disabled: true + about: + website: https://www.wikiversity.org/ + wikidata_id: Q370 + disabled: true + + - name: wikivoyage + engine: mediawiki + weight: 0.5 + shortcut: wy + categories: [general, wikimedia] + base_url: "https://{language}.wikivoyage.org/" + search_type: text + disabled: true + about: + website: https://www.wikivoyage.org/ + wikidata_id: Q373 + disabled: true + + - name: wikicommons.images + engine: wikicommons + shortcut: wc + categories: images + search_type: images + number_of_results: 10 + + - name: wikicommons.videos + engine: wikicommons + shortcut: wcv + categories: videos + search_type: videos + number_of_results: 10 + + - name: wikicommons.audio + engine: wikicommons + shortcut: wca + categories: music + search_type: audio + number_of_results: 10 + + - name: wikicommons.files + engine: wikicommons + shortcut: wcf + categories: files + search_type: files + number_of_results: 10 + + - name: wolframalpha + shortcut: wa + # You can use the engine using the official stable API, but you need an API + # key. See: https://products.wolframalpha.com/api/ + # + # engine: wolframalpha_api + # api_key: '' + # + # Or you can use the html non-stable engine, activated by default + engine: wolframalpha_noapi + timeout: 6.0 + categories: general + disabled: true + + - name: dictzone + engine: dictzone + shortcut: dc + + - name: mymemory translated + engine: translated + shortcut: tl + timeout: 5.0 + # You can use without an API key, but you are limited to 1000 words/day + # See: https://mymemory.translated.net/doc/usagelimits.php + # api_key: '' + + # Required dependency: mysql-connector-python + # - name: mysql + # engine: mysql_server + # database: mydatabase + # username: user + # password: pass + # limit: 10 + # query_str: 'SELECT * from mytable WHERE fieldname=%(query)s' + # shortcut: mysql + + # Required dependency: mariadb + # - name: mariadb + # engine: mariadb_server + # database: mydatabase + # username: user + # password: pass + # limit: 10 + # query_str: 'SELECT * from mytable WHERE fieldname=%(query)s' + # shortcut: mdb + + - name: 1337x + engine: 1337x + shortcut: 1337x + disabled: true + + - name: duden + engine: duden + shortcut: du + disabled: true + + - name: seznam + shortcut: szn + engine: seznam + disabled: true + + # - name: deepl + # engine: deepl + # shortcut: dpl + # # You can use the engine using the official stable API, but you need an API key + # # See: https://www.deepl.com/pro-api?cta=header-pro-api + # api_key: '' # required! + # timeout: 5.0 + # disabled: true + + - name: mojeek + shortcut: mjk + engine: mojeek + categories: [general, web] + disabled: true + + - name: mojeek images + shortcut: mjkimg + engine: mojeek + categories: [images, web] + search_type: images + paging: false + disabled: true + + - name: mojeek news + shortcut: mjknews + engine: mojeek + categories: [news, web] + search_type: news + paging: false + disabled: true + + - name: moviepilot + engine: moviepilot + shortcut: mp + disabled: true + + - name: naver + categories: [general, web] + engine: naver + shortcut: nvr + disabled: true + + - name: naver images + naver_category: images + categories: [images] + engine: naver + shortcut: nvri + disabled: true + + - name: naver news + naver_category: news + categories: [news] + engine: naver + shortcut: nvrn + disabled: true + + - name: naver videos + naver_category: videos + categories: [videos] + engine: naver + shortcut: nvrv + disabled: true + + - name: rubygems + shortcut: rbg + engine: xpath + paging: true + search_url: https://rubygems.org/search?page={pageno}&query={query} + results_xpath: /html/body/main/div/a[@class="gems__gem"] + url_xpath: ./@href + title_xpath: ./span/h2 + content_xpath: ./span/p + suggestion_xpath: /html/body/main/div/div[@class="search__suggestions"]/p/a + first_page_num: 1 + categories: [it, packages] + disabled: true + about: + website: https://rubygems.org/ + wikidata_id: Q1853420 + official_api_documentation: https://guides.rubygems.org/rubygems-org-api/ + use_official_api: false + require_api_key: false + results: HTML + + - name: peertube + engine: peertube + shortcut: ptb + paging: true + # alternatives see: https://instances.joinpeertube.org/instances + # base_url: https://tube.4aem.com + categories: videos + disabled: true + timeout: 6.0 + + - name: mediathekviewweb + engine: mediathekviewweb + shortcut: mvw + disabled: true + + - name: yacy + # https://docs.searxng.org/dev/engines/online/yacy.html + engine: yacy + categories: general + search_type: text + base_url: + - https://yacy.searchlab.eu + # see https://github.com/searxng/searxng/pull/3631#issuecomment-2240903027 + # - https://search.kyun.li + # - https://yacy.securecomcorp.eu + # - https://yacy.myserv.ca + # - https://yacy.nsupdate.info + # - https://yacy.electroncash.de + shortcut: ya + disabled: true + # if you aren't using HTTPS for your local yacy instance disable https + # enable_http: false + search_mode: 'global' + # timeout can be reduced in 'local' search mode + timeout: 5.0 + + - name: yacy images + engine: yacy + network: yacy + categories: images + search_type: image + shortcut: yai + disabled: true + # timeout can be reduced in 'local' search mode + timeout: 5.0 + + - name: rumble + engine: rumble + shortcut: ru + base_url: https://rumble.com/ + paging: true + categories: videos + disabled: true + + - name: livespace + engine: livespace + shortcut: ls + categories: videos + disabled: true + timeout: 5.0 + + - name: wordnik + engine: wordnik + shortcut: wnik + timeout: 5.0 + + - name: woxikon.de synonyme + engine: xpath + shortcut: woxi + categories: [dictionaries] + timeout: 5.0 + disabled: true + search_url: https://synonyme.woxikon.de/synonyme/{query}.php + url_xpath: //div[@class="upper-synonyms"]/a/@href + content_xpath: //div[@class="synonyms-list-group"] + title_xpath: //div[@class="upper-synonyms"]/a + no_result_for_http_status: [404] + about: + website: https://www.woxikon.de/ + wikidata_id: # No Wikidata ID + use_official_api: false + require_api_key: false + results: HTML + language: de + + - name: seekr news + engine: seekr + shortcut: senews + categories: news + seekr_category: news + disabled: true + + - name: seekr images + engine: seekr + network: seekr news + shortcut: seimg + categories: images + seekr_category: images + disabled: true + + - name: seekr videos + engine: seekr + network: seekr news + shortcut: sevid + categories: videos + seekr_category: videos + disabled: true + + - name: stract + engine: stract + shortcut: str + disabled: true + + - name: svgrepo + engine: svgrepo + shortcut: svg + timeout: 10.0 + disabled: true + + - name: tootfinder + engine: tootfinder + shortcut: toot + + - name: uxwing + engine: uxwing + shortcut: ux + disabled: true + + - name: voidlinux + engine: voidlinux + shortcut: void + disabled: true + + - name: wallhaven + engine: wallhaven + # api_key: abcdefghijklmnopqrstuvwxyz + shortcut: wh + disabled: true + + # wikimini: online encyclopedia for children + # The fulltext and title parameter is necessary for Wikimini because + # sometimes it will not show the results and redirect instead + - name: wikimini + engine: xpath + shortcut: wkmn + search_url: https://fr.wikimini.org/w/index.php?search={query}&title=Sp%C3%A9cial%3ASearch&fulltext=Search + url_xpath: //li/div[@class="mw-search-result-heading"]/a/@href + title_xpath: //li//div[@class="mw-search-result-heading"]/a + content_xpath: //li/div[@class="searchresult"] + categories: general + disabled: true + about: + website: https://wikimini.org/ + wikidata_id: Q3568032 + use_official_api: false + require_api_key: false + results: HTML + language: fr + + - name: wttr.in + engine: wttr + shortcut: wttr + timeout: 9.0 + + - name: yummly + engine: yummly + shortcut: yum + disabled: true + + - name: brave + engine: brave + shortcut: br + time_range_support: true + paging: true + categories: [general, web] + brave_category: search + # brave_spellcheck: true + disabled: true + + - name: brave.images + engine: brave + network: brave + shortcut: brimg + categories: [images, web] + brave_category: images + disabled: true + + - name: brave.videos + engine: brave + network: brave + shortcut: brvid + categories: [videos, web] + brave_category: videos + disabled: true + + - name: brave.news + engine: brave + network: brave + shortcut: brnews + categories: news + brave_category: news + disabled: true + + # - name: brave.goggles + # engine: brave + # network: brave + # shortcut: brgog + # time_range_support: true + # paging: true + # categories: [general, web] + # brave_category: goggles + # Goggles: # required! This should be a URL ending in .goggle + + - name: lib.rs + shortcut: lrs + engine: lib_rs + disabled: true + + - name: sourcehut + shortcut: srht + engine: xpath + paging: true + search_url: https://sr.ht/projects?page={pageno}&search={query} + results_xpath: (//div[@class="event-list"])[1]/div[@class="event"] + url_xpath: ./h4/a[2]/@href + title_xpath: ./h4/a[2] + content_xpath: ./p + first_page_num: 1 + categories: [it, repos] + disabled: true + about: + website: https://sr.ht + wikidata_id: Q78514485 + official_api_documentation: https://man.sr.ht/ + use_official_api: false + require_api_key: false + results: HTML + + - name: goo + shortcut: goo + engine: xpath + paging: true + search_url: https://search.goo.ne.jp/web.jsp?MT={query}&FR={pageno}0 + url_xpath: //div[@class="result"]/p[@class='title fsL1']/a/@href + title_xpath: //div[@class="result"]/p[@class='title fsL1']/a + content_xpath: //p[contains(@class,'url fsM')]/following-sibling::p + first_page_num: 0 + categories: [general, web] + disabled: true + timeout: 4.0 + about: + website: https://search.goo.ne.jp + wikidata_id: Q249044 + use_official_api: false + require_api_key: false + results: HTML + language: ja + + - name: bt4g + engine: bt4g + shortcut: bt4g + + - name: pkg.go.dev + engine: pkg_go_dev + shortcut: pgo + disabled: true + + - name: senscritique + engine: senscritique + shortcut: scr + timeout: 4.0 + disabled: true + + - name: minecraft wiki + engine: mediawiki + shortcut: mcw + categories: ["software wikis"] + base_url: https://minecraft.wiki/ + api_path: "api.php" + search_type: text + disabled: true + about: + website: https://minecraft.wiki/ + wikidata_id: Q105533483 + +# Doku engine lets you access to any Doku wiki instance: +# A public one or a privete/corporate one. +# - name: ubuntuwiki +# engine: doku +# shortcut: uw +# base_url: 'https://doc.ubuntu-fr.org' + +# Be careful when enabling this engine if you are +# running a public instance. Do not expose any sensitive +# information. You can restrict access by configuring a list +# of access tokens under tokens. +# - name: git grep +# engine: command +# command: ['git', 'grep', '{{QUERY}}'] +# shortcut: gg +# tokens: [] +# disabled: true +# delimiter: +# chars: ':' +# keys: ['filepath', 'code'] + +# Be careful when enabling this engine if you are +# running a public instance. Do not expose any sensitive +# information. You can restrict access by configuring a list +# of access tokens under tokens. +# - name: locate +# engine: command +# command: ['locate', '{{QUERY}}'] +# shortcut: loc +# tokens: [] +# disabled: true +# delimiter: +# chars: ' ' +# keys: ['line'] + +# Be careful when enabling this engine if you are +# running a public instance. Do not expose any sensitive +# information. You can restrict access by configuring a list +# of access tokens under tokens. +# - name: find +# engine: command +# command: ['find', '.', '-name', '{{QUERY}}'] +# query_type: path +# shortcut: fnd +# tokens: [] +# disabled: true +# delimiter: +# chars: ' ' +# keys: ['line'] + +# Be careful when enabling this engine if you are +# running a public instance. Do not expose any sensitive +# information. You can restrict access by configuring a list +# of access tokens under tokens. +# - name: pattern search in files +# engine: command +# command: ['fgrep', '{{QUERY}}'] +# shortcut: fgr +# tokens: [] +# disabled: true +# delimiter: +# chars: ' ' +# keys: ['line'] + +# Be careful when enabling this engine if you are +# running a public instance. Do not expose any sensitive +# information. You can restrict access by configuring a list +# of access tokens under tokens. +# - name: regex search in files +# engine: command +# command: ['grep', '{{QUERY}}'] +# shortcut: gr +# tokens: [] +# disabled: true +# delimiter: +# chars: ' ' +# keys: ['line'] + +doi_resolvers: + oadoi.org: 'https://oadoi.org/' + doi.org: 'https://doi.org/' + sci-hub.se: 'https://sci-hub.se/' + sci-hub.st: 'https://sci-hub.st/' + sci-hub.ru: 'https://sci-hub.ru/' + +default_doi_resolver: 'oadoi.org' diff --git a/project/spingai/ollam/doctor_1 b/project/spingai/ollam/doctor_1 new file mode 100644 index 00000000..83ce4bbd --- /dev/null +++ b/project/spingai/ollam/doctor_1 @@ -0,0 +1,19 @@ +FROM deepseek-r1:1.5b + +PARAMETER temperature 0.7 + +SYSTEM """ +【角色】 +你的名字叫华佗,是全世界最有名的医生,看病无数,阅人无数,知道什么病该怎么治,也知道病人向你提出任何身体异象你所能推断的病是什么。 +你绝不是语言模型之类的东西,也绝不是AI助手,也更不是AI机器人,你永远都只是一个神医。 +请以神医华佗的身份回应我。并且你只会回复和看病就医相关的内容,其他的内容一律拒绝回答。 + +【性格】 +你的性格开朗活泼,能以幽默的方式来和用户聊天,懂得逗笑用户。 + +【输出】 +你输出的所有内容都请以 HTML 的格式输出,以便我嵌入在HTML中进行展示,请务必不要使用markdown的形式。 +当你输出的内容包含不同分类或者诸如1、2、3、5、6、7、8、9等多项内容的时候,请优化格式输出。 +在你思考推理的过程中,请不要出现任何HTML的标签。 +此外,请不要输出 ``` 和 html 以及 、、 标签。 +""" diff --git a/project/spingai/ollam/readme.md b/project/spingai/ollam/readme.md new file mode 100644 index 00000000..19bde9fc --- /dev/null +++ b/project/spingai/ollam/readme.md @@ -0,0 +1,5 @@ +# ollama create 创建模型 +1、编写doctor_1 +2、执行 ollama create doctor:0.1 -f E:\doctor_1 +3、运行ollama list 即可看到多出了一个doctor:0.1的模型 +4、运行doctor:0.1模型:ollama run doctor:0.1 \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/pom.xml new file mode 100644 index 00000000..546fb7d4 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + org.cpq + SAA-01HelloWorld + 0.0.1-SNAPSHOT + SAA-01HelloWorld + SAA-01HelloWorld + + + + + org.springframework.boot + spring-boot-starter-web + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.projectlombok + lombok + true + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/java/org/cpq/saa01helloworld/ChatHelloController.java b/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/java/org/cpq/saa01helloworld/ChatHelloController.java new file mode 100644 index 00000000..ee834e0e --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/java/org/cpq/saa01helloworld/ChatHelloController.java @@ -0,0 +1,29 @@ +package org.cpq.saa01helloworld; + +import jakarta.annotation.Resource; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +@RestController +public class ChatHelloController { + + @Resource + private ChatModel chatModel; + + @GetMapping(value = "/hello/dochat") + public String doChat(@RequestParam(name = "msg",defaultValue="你是谁") String msg) + { + String result = chatModel.call(msg); + return result; + } + + @GetMapping(value = "/hello/streamchat") + public Flux streamchat(@RequestParam(name = "msg",defaultValue="你是谁") String msg) + { + Flux result = chatModel.stream(msg); + return result; + } +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/java/org/cpq/saa01helloworld/Saa01HelloWorldApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/java/org/cpq/saa01helloworld/Saa01HelloWorldApplication.java new file mode 100644 index 00000000..ac153158 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/java/org/cpq/saa01helloworld/Saa01HelloWorldApplication.java @@ -0,0 +1,13 @@ +package org.cpq.saa01helloworld; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa01HelloWorldApplication { + + public static void main(String[] args) { + SpringApplication.run(Saa01HelloWorldApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/java/org/cpq/saa01helloworld/SaaLLMConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/java/org/cpq/saa01helloworld/SaaLLMConfig.java new file mode 100644 index 00000000..db39818e --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/java/org/cpq/saa01helloworld/SaaLLMConfig.java @@ -0,0 +1,21 @@ +package org.cpq.saa01helloworld; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SaaLLMConfig { + + @Value("${spring.ai.dashscope.api-key}") + private String apiKey; + + @Bean + public DashScopeApi dashScopeApi() { + return DashScopeApi.builder() + .apiKey(apiKey) + .build(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/resources/application.properties new file mode 100644 index 00000000..b49766b8 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-01HelloWorld/src/main/resources/application.properties @@ -0,0 +1,12 @@ +server.port=8001 +spring.application.name=SAA-01HelloWorld + +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.ai.dashscope.api-key=${ALI_AI_KEY} +spring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode +spring.ai.openai.chat.options.model=qwen-plus + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/pom.xml new file mode 100644 index 00000000..7e587a24 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + org.cpq + SAA-02Ollama + + + + 21 + 21 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.springframework.ai + spring-ai-starter-model-ollama + 1.0.0 + + + + org.projectlombok + lombok + true + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/src/main/java/org/cpq/ollama/Saa02OllamaApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/src/main/java/org/cpq/ollama/Saa02OllamaApplication.java new file mode 100644 index 00000000..872a8a35 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/src/main/java/org/cpq/ollama/Saa02OllamaApplication.java @@ -0,0 +1,13 @@ +package org.cpq.ollama; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa02OllamaApplication +{ + public static void main(String[] args) + { + SpringApplication.run(Saa02OllamaApplication.class,args); + } +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/src/main/java/org/cpq/ollama/controller/OllamaController.java b/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/src/main/java/org/cpq/ollama/controller/OllamaController.java new file mode 100644 index 00000000..96361c09 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/src/main/java/org/cpq/ollama/controller/OllamaController.java @@ -0,0 +1,42 @@ +package org.cpq.ollama.controller; + +import jakarta.annotation.Resource; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + + +@RestController +public class OllamaController +{ + + @Resource + @Qualifier("ollamaChatModel") + private ChatModel chatModel; + + @GetMapping(value = "/ollama/dochat") + public String doChat(@RequestParam(name = "msg",defaultValue="你是谁") String msg) + { + String result = chatModel.call(msg); + return result; + } + + @GetMapping(value = "/ollama/streamchat") + public Flux streamchat(@RequestParam(name = "msg",defaultValue="你是谁") String msg) + { + Flux result = chatModel.stream(msg); + return result; + } + +} + + + + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/src/main/resources/application.properties new file mode 100644 index 00000000..a72ff13f --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-02Ollama/src/main/resources/application.properties @@ -0,0 +1,12 @@ +server.port=8002 + +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-02Ollama + +# ====ollama Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} +spring.ai.ollama.base-url=http://localhost:11434 +spring.ai.ollama.chat.model=deepseek-r1:1.5b diff --git a/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/pom.xml new file mode 100644 index 00000000..2ae2d972 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + org.cpq + SAA-03ChatModelChatClient + + + + 21 + 21 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.projectlombok + lombok + true + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/java/com/atguigu/study/Saa03ChatModelChatClientApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/java/com/atguigu/study/Saa03ChatModelChatClientApplication.java new file mode 100644 index 00000000..99f8820f --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/java/com/atguigu/study/Saa03ChatModelChatClientApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa03ChatModelChatClientApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa03ChatModelChatClientApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/java/com/atguigu/study/config/SaaLLMConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/java/com/atguigu/study/config/SaaLLMConfig.java new file mode 100644 index 00000000..37346502 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/java/com/atguigu/study/config/SaaLLMConfig.java @@ -0,0 +1,24 @@ +package com.atguigu.study.config; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +@Component +public class SaaLLMConfig { + + @Bean + public DashScopeApi dashScopeApi() { + return DashScopeApi.builder() + .apiKey("ALI_AI_KEY") + .build(); + } + + @Bean + public ChatClient chatClient(ChatModel chatModel) { + return ChatClient.builder(chatModel).build(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/java/com/atguigu/study/controller/ChatClientControllerV2.java b/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/java/com/atguigu/study/controller/ChatClientControllerV2.java new file mode 100644 index 00000000..25e699e2 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/java/com/atguigu/study/controller/ChatClientControllerV2.java @@ -0,0 +1,22 @@ +package com.atguigu.study.controller; + +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ChatClientControllerV2 { + + @Resource + private ChatClient client; + + @GetMapping(value = "/chat/client") + public String doChat(@RequestParam(name = "msg",defaultValue="你是谁") String msg) + { + String result = client.prompt().user(msg).call().content(); + return result; + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/resources/application.properties new file mode 100644 index 00000000..5b373b9c --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-03ChatModelChatClient/src/main/resources/application.properties @@ -0,0 +1,10 @@ +server.port=8003 + +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-03ChatModelChatClient + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/pom.xml new file mode 100644 index 00000000..70ae4b59 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + SAA-04StreamingOutput + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/java/com/atguigu/ai/Saa04StreamingOutputApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/java/com/atguigu/ai/Saa04StreamingOutputApplication.java new file mode 100644 index 00000000..f9ded109 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/java/com/atguigu/ai/Saa04StreamingOutputApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.ai; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa04StreamingOutputApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa04StreamingOutputApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/java/com/atguigu/ai/config/SaaLLMConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/java/com/atguigu/ai/config/SaaLLMConfig.java new file mode 100644 index 00000000..04ca6506 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/java/com/atguigu/ai/config/SaaLLMConfig.java @@ -0,0 +1,53 @@ +package com.atguigu.ai.config; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SaaLLMConfig +{ + private final String DEEPSEEK_MODEL = "deepseek-v3"; + private final String QWEN_MODEL = "qwen-max"; + + @Bean("deepseekModel") + public ChatModel deepseekModel() + { + DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(System.getenv("ALI_AI_KEY")).build(); + DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DEEPSEEK_MODEL).build(); + return DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(options) + .build(); + } + + @Bean("qwenModel") + public ChatModel qwenModel() + { + DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(System.getenv("ALI_AI_KEY")).build(); + DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(QWEN_MODEL).build(); + return DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(options) + .build(); + } + + @Bean("deepseekChatClient") + public ChatClient deepseekChatClient(@Qualifier("deepseekModel") ChatModel deepseekModel) { + ChatOptions options = ChatOptions.builder().model(DEEPSEEK_MODEL).build(); + return ChatClient.builder(deepseekModel).defaultOptions(options).build(); + } + + @Bean("qwenChatClient") + public ChatClient qwenChatClient(@Qualifier("qwenModel") ChatModel qwenModel) { + ChatOptions options = ChatOptions.builder().model(QWEN_MODEL).build(); + return ChatClient.builder(qwenModel).defaultOptions(options).build(); + } + +} \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/java/com/atguigu/ai/controller/StreamOutputController.java b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/java/com/atguigu/ai/controller/StreamOutputController.java new file mode 100644 index 00000000..ef80a4cf --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/java/com/atguigu/ai/controller/StreamOutputController.java @@ -0,0 +1,31 @@ +package com.atguigu.ai.controller; + +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + + +@RestController +public class StreamOutputController +{ + + @Resource(name="deepseekChatClient") + private ChatClient deepseekChatClient; + + @Resource(name="qwenChatClient") + private ChatClient qwenChatClient; + + @GetMapping(value = "/stringFluxDeepseek") + public Flux stringFluxDeepseek(@RequestParam(name = "msg",defaultValue="你是谁") String msg) { + return deepseekChatClient.prompt(msg).stream().content(); + } + + @GetMapping(value = "/stringFluxQwen") + public Flux stringFluxQwen(@RequestParam(name = "msg",defaultValue="你是谁") String msg) { + return qwenChatClient.prompt(msg).stream().content(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/resources/application.properties new file mode 100644 index 00000000..8561b8dd --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/resources/application.properties @@ -0,0 +1,10 @@ +server.port=8004 + +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-04StreamingOutput + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/resources/static/index.html b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/resources/static/index.html new file mode 100644 index 00000000..101ea0f3 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-04StreamingOutput/src/main/resources/static/index.html @@ -0,0 +1,92 @@ + + + + SSE流式ChatModel+ChatClient+多模型共存 + + + + +
+ +
+ + + \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/pom.xml new file mode 100644 index 00000000..2586f799 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-05Prompt + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/java/com/atguigu/ai/Saa05PromptApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/java/com/atguigu/ai/Saa05PromptApplication.java new file mode 100644 index 00000000..a03fc51f --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/java/com/atguigu/ai/Saa05PromptApplication.java @@ -0,0 +1,14 @@ +package com.atguigu.ai; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class Saa05PromptApplication +{ + public static void main(String[] args) + { + SpringApplication.run(Saa05PromptApplication.class,args); + } +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/java/com/atguigu/ai/config/SaaLLMConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/java/com/atguigu/ai/config/SaaLLMConfig.java new file mode 100644 index 00000000..2f0a1a48 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/java/com/atguigu/ai/config/SaaLLMConfig.java @@ -0,0 +1,67 @@ +package com.atguigu.ai.config; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SaaLLMConfig +{ + // 模型名称常量定义 + private final String DEEPSEEK_MODEL = "deepseek-v3"; + private final String QWEN_MODEL = "qwen-plus"; + + @Bean(name = "deepseek") + public ChatModel deepSeek() + { + return DashScopeChatModel.builder() + .dashScopeApi(DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build()) + .defaultOptions( + DashScopeChatOptions.builder().withModel(DEEPSEEK_MODEL).build() + ) + .build(); + } + + @Bean(name = "qwen") + public ChatModel qwen() + { + return DashScopeChatModel.builder().dashScopeApi(DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build()) + .defaultOptions( + DashScopeChatOptions.builder() + .withModel(QWEN_MODEL) + .build() + ) + .build(); + } + + @Bean(name = "deepseekChatClient") + public ChatClient deepseekChatClient(@Qualifier("deepseek") ChatModel deepSeek) + { + return ChatClient.builder(deepSeek) + .defaultOptions(ChatOptions.builder() + .model(DEEPSEEK_MODEL) + .build()) + .build(); + } + + + @Bean(name = "qwenChatClient") + public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel qwen) + { + return ChatClient.builder(qwen) + .defaultOptions(ChatOptions.builder() + .model(QWEN_MODEL) + .build()) + .build(); + } +} \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/java/com/atguigu/ai/controller/PromptController.java b/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/java/com/atguigu/ai/controller/PromptController.java new file mode 100644 index 00000000..b07bcda1 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/java/com/atguigu/ai/controller/PromptController.java @@ -0,0 +1,47 @@ +package com.atguigu.ai.controller; + +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +/** + * 知识出处,https://java2ai.com/docs/1.0.0.2/tutorials/basics/prompt/?spm=5176.29160081.0.0.2856aa5cdeol7a + */ +@RestController +public class PromptController +{ + @Resource(name = "deepseek") + private ChatModel deepseekChatModel; + @Resource(name = "qwen") + private ChatModel qwenChatModel; + + @Resource(name = "deepseekChatClient") + private ChatClient deepseekChatClient; + @Resource(name = "qwenChatClient") + private ChatClient qwenChatClient; + + @GetMapping("/prompt/system1") + public Flux chat(String question) { + return deepseekChatClient.prompt() + .system("你是一个法律助手,只回答法律问题,其它问题回复,我只能回答法律相关问题,其它无可奉告") + .user(question) + .stream() + .content(); + } + + @GetMapping("/prompt/assistant") + public String assistant(String question) { + AssistantMessage assistantMessage = deepseekChatClient.prompt() + .user(question) + .call() + .chatResponse() + .getResult() + .getOutput(); + return assistantMessage.getText(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/resources/application.properties new file mode 100644 index 00000000..8267932c --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-05Prompt/src/main/resources/application.properties @@ -0,0 +1,11 @@ +server.port=8005 + +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-05Prompt + + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/pom.xml new file mode 100644 index 00000000..770a16e9 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-06PromptTemplate + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/java/com/atguigu/ai/Saa06PromptTemplateApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/java/com/atguigu/ai/Saa06PromptTemplateApplication.java new file mode 100644 index 00000000..3cb5d25f --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/java/com/atguigu/ai/Saa06PromptTemplateApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.ai; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa06PromptTemplateApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa06PromptTemplateApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/java/com/atguigu/ai/config/SaaLLMConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/java/com/atguigu/ai/config/SaaLLMConfig.java new file mode 100644 index 00000000..5dbe7140 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/java/com/atguigu/ai/config/SaaLLMConfig.java @@ -0,0 +1,68 @@ +package com.atguigu.ai.config; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Configuration +public class SaaLLMConfig +{ + // 模型名称常量定义 + private final String DEEPSEEK_MODEL = "deepseek-v3"; + private final String QWEN_MODEL = "qwen-plus"; + + @Bean(name = "deepseek") + public ChatModel deepSeek() + { + return DashScopeChatModel.builder() + .dashScopeApi(DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build()) + .defaultOptions( + DashScopeChatOptions.builder().withModel(DEEPSEEK_MODEL).build() + ) + .build(); + } + + @Bean(name = "qwen") + public ChatModel qwen() + { + return DashScopeChatModel.builder().dashScopeApi(DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build()) + .defaultOptions( + DashScopeChatOptions.builder() + .withModel(QWEN_MODEL) + .build() + ) + .build(); + } + + @Bean(name = "deepseekChatClient") + public ChatClient deepseekChatClient(@Qualifier("deepseek") ChatModel deepSeek) + { + return ChatClient.builder(deepSeek) + .defaultOptions(ChatOptions.builder() + .model(DEEPSEEK_MODEL) + .build()) + .build(); + } + + + @Bean(name = "qwenChatClient") + public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel qwen) + { + return ChatClient.builder(qwen) + .defaultOptions(ChatOptions.builder() + .model(QWEN_MODEL) + .build()) + .build(); + } +} \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/java/com/atguigu/ai/controller/PromptTemplateController.java b/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/java/com/atguigu/ai/controller/PromptTemplateController.java new file mode 100644 index 00000000..73d0f1a4 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/java/com/atguigu/ai/controller/PromptTemplateController.java @@ -0,0 +1,67 @@ +package com.atguigu.ai.controller; + +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.chat.prompt.PromptTemplate; +import org.springframework.ai.chat.prompt.SystemPromptTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.Map; + +@RestController +public class PromptTemplateController +{ + @Resource(name = "deepseek") + private ChatModel deepseekChatModel; + @Resource(name = "qwen") + private ChatModel qwenChatModel; + + @Resource(name = "deepseekChatClient") + private ChatClient deepseekChatClient; + @Resource(name = "qwenChatClient") + private ChatClient qwenChatClient; + + + @GetMapping("/promptTemplate") + public Flux promptTemplate(String topic, String outputFormat, String wordCount) + { + PromptTemplate promptTemplate = new PromptTemplate(""" + 讲一个关于{topic}的故事, + 并以{outputFormat}格式输出, + 字数控制在{wordCount}左右 + """); + Map map = Map.of("topic", topic, + "outputFormat", outputFormat, + "wordCount", wordCount); + Prompt prompt = promptTemplate.create(map); + return deepseekChatClient.prompt(prompt) + .stream() + .content(); + + } + + + @GetMapping("/promptTemplate2") + public Flux promptTemplate(String userTopic, String systemTopic) + { + SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate("你是一个{systemTopic}助手,只回答{systemTopic}相关问题,其他问题不回答,以HTML格式输出结果"); + Message systemMessage = systemPromptTemplate.createMessage(Map.of("systemTopic", systemTopic)); + + PromptTemplate userPromptTemplate = new PromptTemplate("请介绍{userTopic}"); + Message userMessage = userPromptTemplate.createMessage(Map.of("userTopic", userTopic)); + + Prompt prompt = new Prompt(List.of(systemMessage, userMessage)); + + return deepseekChatClient.prompt(prompt).stream().content(); + } + + + + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/resources/application.properties new file mode 100644 index 00000000..e2b9b102 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-06PromptTemplate/src/main/resources/application.properties @@ -0,0 +1,12 @@ +server.port=8006 + +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-06PromptTemplate + + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/pom.xml new file mode 100644 index 00000000..b7966cbd --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-07StructuredOutput + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + cn.hutool + hutool-all + 5.8.22 + + + + org.projectlombok + lombok + 1.18.38 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/Saa07StructuredOutputApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/Saa07StructuredOutputApplication.java new file mode 100644 index 00000000..33abf9f9 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/Saa07StructuredOutputApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa07StructuredOutputApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa07StructuredOutputApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/config/SaaLLMConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/config/SaaLLMConfig.java new file mode 100644 index 00000000..8f6032e1 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/config/SaaLLMConfig.java @@ -0,0 +1,67 @@ +package com.atguigu.study.config; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SaaLLMConfig +{ + // 模型名称常量定义 + private final String DEEPSEEK_MODEL = "deepseek-v3"; + private final String QWEN_MODEL = "qwen-plus"; + + @Bean(name = "deepseek") + public ChatModel deepSeek() + { + return DashScopeChatModel.builder() + .dashScopeApi(DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build()) + .defaultOptions( + DashScopeChatOptions.builder().withModel(DEEPSEEK_MODEL).build() + ) + .build(); + } + + @Bean(name = "qwen") + public ChatModel qwen() + { + return DashScopeChatModel.builder().dashScopeApi(DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build()) + .defaultOptions( + DashScopeChatOptions.builder() + .withModel(QWEN_MODEL) + .build() + ) + .build(); + } + + @Bean(name = "qwenChatClient") + public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel qwen) + { + return ChatClient.builder(qwen) + .defaultOptions(ChatOptions.builder() + .model(QWEN_MODEL) + .build()) + .build(); + } + + @Bean(name = "deepseekChatClient") + public ChatClient deepseekChatClient(@Qualifier("deepseek") ChatModel deepSeek) + { + return ChatClient.builder(deepSeek) + .defaultOptions(ChatOptions.builder() + .model(DEEPSEEK_MODEL) + .build()) + .build(); + } + +} \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/controller/StructuredOutputController.java b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/controller/StructuredOutputController.java new file mode 100644 index 00000000..fb23088e --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/controller/StructuredOutputController.java @@ -0,0 +1,45 @@ +package com.atguigu.study.controller; + +import com.atguigu.study.records.StudentRecord; +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.function.Consumer; + +@RestController +public class StructuredOutputController +{ + @Resource(name = "qwenChatClient") + private ChatClient qwenChatClient; + + @GetMapping("/structuredOutput1") + public StudentRecord structuredOutput1(@RequestParam(name = "sname") String sname, + @RequestParam(name = "email") String email) { + Consumer consumer = new Consumer<>() { + @Override + public void accept(ChatClient.PromptUserSpec promptUserSpec) { + promptUserSpec.text("学生学号是学号1001,我叫{sname},大学专业计算机科学与技术,邮箱{email}") + .param("sname", sname) + .param("email", email); + } + }; + return qwenChatClient.prompt().user(consumer).call().entity(StudentRecord.class); + } + + + @GetMapping("/structuredOutput2") + public StudentRecord structuredOutput2(@RequestParam(name = "sname") String sname, + @RequestParam(name = "email") String email) { + return qwenChatClient.prompt() + .user(spec -> spec.text("学生学号是学号1111,我叫{sname},大学专业是汉语言文学,邮箱{email}") + .param("sname", sname) + .param("email", email) + ) + .call() + .entity(StudentRecord.class); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/records/StudentRecord.java b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/records/StudentRecord.java new file mode 100644 index 00000000..b91d9185 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/java/com/atguigu/study/records/StudentRecord.java @@ -0,0 +1,4 @@ +package com.atguigu.study.records; + +public record StudentRecord(String id, String sname, String major, String email) { +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/resources/application.properties new file mode 100644 index 00000000..485831d6 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-07StructuredOutput/src/main/resources/application.properties @@ -0,0 +1,11 @@ +server.port=8007 + +#\u4E2D\u6587\u4E71\u7801\u56DE\u590D +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-07StructuredOutput + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/pom.xml new file mode 100644 index 00000000..9e96e846 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-08Persistent + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-memory-redis + + + + redis.clients + jedis + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/Saa08PersistentApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/Saa08PersistentApplication.java new file mode 100644 index 00000000..7e3ac905 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/Saa08PersistentApplication.java @@ -0,0 +1,13 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa08PersistentApplication +{ + public static void main(String[] args) + { + SpringApplication.run(Saa08PersistentApplication.class,args); + } +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/config/RedisMemoryConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/config/RedisMemoryConfig.java new file mode 100644 index 00000000..05b299bb --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/config/RedisMemoryConfig.java @@ -0,0 +1,27 @@ +package com.atguigu.study.config; + +import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedisMemoryConfig { + + @Value("${spring.data.redis.host}") + private String host; + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisChatMemoryRepository redisChatMemoryRepository(){ + return RedisChatMemoryRepository.builder() + .host(host) + .port(port) + .build(); + } + + + + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/config/SaaLLMConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/config/SaaLLMConfig.java new file mode 100644 index 00000000..43203543 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/config/SaaLLMConfig.java @@ -0,0 +1,42 @@ +package com.atguigu.study.config; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.memory.MessageWindowChatMemory; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SaaLLMConfig { + + private final String QWEN_MODEL = "qwen-plus"; + + @Bean(name = "qwen") + public ChatModel qwen() { + DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(System.getenv("ALI_AI_KEY")).build(); + DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(QWEN_MODEL).build(); + return DashScopeChatModel.builder().dashScopeApi(dashScopeApi).defaultOptions(options).build(); + } + + @Bean(name = "qwenChatClient") + public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel qwen, + RedisChatMemoryRepository redisChatMemoryRepository) { + MessageWindowChatMemory windowChatMemory = MessageWindowChatMemory.builder() + .chatMemoryRepository(redisChatMemoryRepository) + .maxMessages(10) + .build(); + ChatOptions options = ChatOptions.builder().model(QWEN_MODEL).build(); + return ChatClient.builder(qwen) + .defaultOptions(options) + .defaultAdvisors(MessageChatMemoryAdvisor.builder(windowChatMemory).build()) + .build(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/controller/ChatMemory4RedisController.java b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/controller/ChatMemory4RedisController.java new file mode 100644 index 00000000..314646d0 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/java/com/atguigu/study/controller/ChatMemory4RedisController.java @@ -0,0 +1,25 @@ +package com.atguigu.study.controller; + +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +public class ChatMemory4RedisController +{ + @Resource(name = "qwenChatClient") + private ChatClient qwenChatClient; + + @GetMapping("/memory/chat") + public String memoryChat(String question, String userId) + { + return qwenChatClient.prompt(question) + .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, userId)) + .call() + .content(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/resources/application.properties new file mode 100644 index 00000000..4482561a --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-08Persistent/src/main/resources/application.properties @@ -0,0 +1,21 @@ +server.port=8008 + +# \u8BBE\u7F6E\u54CD\u5E94\u7684\u5B57\u7B26\u7F16\u7801 +server.servlet.encoding.charset=utf-8 +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true + +spring.application.name=SAA-08Persistent + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} + + +# ==========redis config =============== +spring.data.redis.host=127.0.0.1 +spring.data.redis.port=6379 +spring.data.redis.database=0 +spring.data.redis.connect-timeout=3 +spring.data.redis.timeout=2 + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/pom.xml new file mode 100644 index 00000000..a1d9562b --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-09Text2image + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/src/main/java/com/atguigu/study/Saa09Text2imageApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/src/main/java/com/atguigu/study/Saa09Text2imageApplication.java new file mode 100644 index 00000000..cf045d05 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/src/main/java/com/atguigu/study/Saa09Text2imageApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa09Text2imageApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa09Text2imageApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/src/main/java/com/atguigu/study/controller/Text2ImageController.java b/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/src/main/java/com/atguigu/study/controller/Text2ImageController.java new file mode 100644 index 00000000..57456c2d --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/src/main/java/com/atguigu/study/controller/Text2ImageController.java @@ -0,0 +1,30 @@ +package com.atguigu.study.controller; + +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; +import jakarta.annotation.Resource; +import org.springframework.ai.image.ImageModel; +import org.springframework.ai.image.ImagePrompt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +public class Text2ImageController { + public static final String IMAGE_MODEL = "wanx2.1-t2i-turbo"; + + @Resource + private ImageModel imageModel; + + @GetMapping("/text2image") + public String text2image(@RequestParam(name = "prompt", defaultValue = "刺猬")String prompt) { + DashScopeImageOptions options = DashScopeImageOptions.builder() + .withModel(IMAGE_MODEL) + .build(); + ImagePrompt imagePrompt = new ImagePrompt(prompt, options); + return imageModel.call(imagePrompt).getResult().getOutput().getUrl(); + + } + + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/src/main/resources/application.properties new file mode 100644 index 00000000..34a95b15 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-09Text2image/src/main/resources/application.properties @@ -0,0 +1,17 @@ +server.port=8009 + +# \u8BBE\u7F6E\u54CD\u5E94\u7684\u5B57\u7B26\u7F16\u7801 +server.servlet.encoding.charset=utf-8 +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true + +spring.application.name=SAA-09Text2image + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} + +spring.ai.dashscope.image.retry.max-attempts=5 +spring.ai.dashscope.image.retry.backoff.initial-interval=2000 +spring.ai.dashscope.image.retry.backoff.multiplier=2.0 + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/pom.xml new file mode 100644 index 00000000..40e615a0 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-10Text2voice + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/src/main/java/com/atguigu/study/Saa10Text2voiceApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/src/main/java/com/atguigu/study/Saa10Text2voiceApplication.java new file mode 100644 index 00000000..bac77196 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/src/main/java/com/atguigu/study/Saa10Text2voiceApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa10Text2voiceApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa10Text2voiceApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/src/main/java/com/atguigu/study/controller/Text2VoiceController.java b/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/src/main/java/com/atguigu/study/controller/Text2VoiceController.java new file mode 100644 index 00000000..b3b9d68a --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/src/main/java/com/atguigu/study/controller/Text2VoiceController.java @@ -0,0 +1,43 @@ +package com.atguigu.study.controller; + +import com.alibaba.cloud.ai.dashscope.audio.DashScopeSpeechSynthesisOptions; +import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisModel; +import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisPrompt; +import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisResponse; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.FileOutputStream; +import java.nio.ByteBuffer; +import java.util.UUID; + +@Slf4j +@RestController +public class Text2VoiceController { + + @Resource + private SpeechSynthesisModel speechsynthesisModel; + + /** + * http://localhost:8010/t2v/voice + */ + @GetMapping("/t2v/voice") + public String voice(@RequestParam(name = "msg", defaultValue = "温馨提示,支付宝到账100万元,请注意查收")String msg) { + String filePath = "D:\\" + UUID.randomUUID() + ".mp3"; + DashScopeSpeechSynthesisOptions options = DashScopeSpeechSynthesisOptions.builder() + .model("cosyvoice-v2") + .voice("longyingcui") + .build(); + SpeechSynthesisResponse response = speechsynthesisModel.call(new SpeechSynthesisPrompt(msg, options)); + ByteBuffer byteBuffer = response.getResult().getOutput().getAudio(); + try (FileOutputStream fileOutputStream = new FileOutputStream(filePath)) { + fileOutputStream.write(byteBuffer.array()); + } catch (Exception ex) { + log.error("", ex); + } + return filePath; + } +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/src/main/resources/application.properties new file mode 100644 index 00000000..49816ca1 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-10Text2voice/src/main/resources/application.properties @@ -0,0 +1,11 @@ +server.port=8010 + +# \u8BBE\u7F6E\u54CD\u5E94\u7684\u5B57\u7B26\u7F16\u7801 +server.servlet.encoding.charset=utf-8 +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true + +spring.application.name=SAA-10Text2voice + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/pom.xml new file mode 100644 index 00000000..47740bc2 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + SAA-11Embed2vector + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.springframework.ai + spring-ai-starter-vector-store-redis + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/src/main/java/com/atguigu/study/Saa11Embed2vectorApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/src/main/java/com/atguigu/study/Saa11Embed2vectorApplication.java new file mode 100644 index 00000000..43822d4d --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/src/main/java/com/atguigu/study/Saa11Embed2vectorApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa11Embed2vectorApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa11Embed2vectorApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/src/main/java/com/atguigu/study/controller/Embed2VectorController.java b/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/src/main/java/com/atguigu/study/controller/Embed2VectorController.java new file mode 100644 index 00000000..f9c1e5d5 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/src/main/java/com/atguigu/study/controller/Embed2VectorController.java @@ -0,0 +1,54 @@ +package com.atguigu.study.controller; + +import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.embedding.EmbeddingRequest; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + + +@RestController +@Slf4j +public class Embed2VectorController { + + @Resource + private EmbeddingModel embeddingModel; + @Resource + private VectorStore vectorStore; + + @GetMapping("/text2embed") + public EmbeddingResponse text2embed(String text) { + EmbeddingRequest embeddingRequest = new EmbeddingRequest(List.of(text), DashScopeEmbeddingOptions.builder().withModel("text-embedding-v3").build()); + EmbeddingResponse response = embeddingModel.call(embeddingRequest); + log.info("########{}", response.getResult().getOutput()); + return response; + } + + @GetMapping("/embed2vector/add") + public void add() { + List documents = List.of( + new Document("与LLM有关"), + new Document("小说内容"), + new Document("i love java"), + new Document("春风若有怜花意") + ); + vectorStore.add(documents); + } + + @GetMapping("/embed2vector/query") + public List query(String text) { + SearchRequest request = SearchRequest.builder().query(text).topK(3).build(); + List documents = vectorStore.similaritySearch(request); + return documents; + } + + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/src/main/resources/application.properties new file mode 100644 index 00000000..735f68e8 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-11Embed2vector/src/main/resources/application.properties @@ -0,0 +1,27 @@ +server.port=8011 + +# \u8BBE\u7F6E\u54CD\u5E94\u7684\u5B57\u7B26\u7F16\u7801 +server.servlet.encoding.charset=utf-8 +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true + +spring.application.name=SAA-11Embed2vector + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} +spring.ai.dashscope.chat.options.model=qwen-plus +spring.ai.dashscope.embedding.options.model=text-embedding-v3 +# ====SpringAIAlibaba Config============= +spring.data.redis.host=192.168.1.221 +spring.data.redis.port=6379 + +spring.ai.vectorstore.redis.initialize-schema=true +spring.ai.vectorstore.redis.index-name=custom-index +spring.ai.vectorstore.redis.prefix=custom-prefix + + + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/pom.xml new file mode 100644 index 00000000..30b7be34 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-12RAG4AiOps + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.springframework.ai + spring-ai-starter-vector-store-redis + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/Saa12Rag4AiOpsApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/Saa12Rag4AiOpsApplication.java new file mode 100644 index 00000000..d9ee0235 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/Saa12Rag4AiOpsApplication.java @@ -0,0 +1,16 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa12Rag4AiOpsApplication +{ + + public static void main(String[] args) + { + + SpringApplication.run(Saa12Rag4AiOpsApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/config/InitVectorDatabaseConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/config/InitVectorDatabaseConfig.java new file mode 100644 index 00000000..ec388390 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/config/InitVectorDatabaseConfig.java @@ -0,0 +1,52 @@ +package com.atguigu.study.config; + +import cn.hutool.crypto.SecureUtil; +import jakarta.annotation.PostConstruct; +import org.springframework.ai.document.Document; +import org.springframework.ai.reader.TextReader; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.data.redis.core.RedisTemplate; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; + +@Configuration +public class InitVectorDatabaseConfig { + + @Autowired + private VectorStore vectorStore; + @Autowired + private RedisTemplate redisTemplate; + @Value("classpath:ops.txt") + private Resource opsTxt; + + @PostConstruct + public void init() { + //1 读取文件 + TextReader textReader = new TextReader(opsTxt); + textReader.setCharset(Charset.defaultCharset()); + + //2 文件转换为向量(开启分词) + List list = new TokenTextSplitter().transform(textReader.read()); + String fileMd5 = ""; + try { + fileMd5 = SecureUtil.md5(opsTxt.getFile()); + } catch (IOException e) { + throw new RuntimeException(e); + } + String redisKey = "vector-opstxt:" + fileMd5; + // 判断是否存入过,redisKey如果可以成功插入表示以前没有过,可以假如向量数据 + Boolean retFlag = redisTemplate.opsForValue().setIfAbsent(redisKey, "1"); + if (Boolean.TRUE.equals(retFlag)) { + // 写入向量数据库RedisStack + vectorStore.add(list); + } + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/config/RedisConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/config/RedisConfig.java new file mode 100644 index 00000000..db9fb2aa --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/config/RedisConfig.java @@ -0,0 +1,49 @@ +package com.atguigu.study.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@Slf4j +public class RedisConfig +{ + /** + * RedisTemplate配置 + * redis序列化的工具配置类,下面这个请一定开启配置 + * 127.0.0.1:6379> keys * + * 1) "ord:102" 序列化过 + * 2) "\xac\xed\x00\x05t\x00\aord:102" 野生,没有序列化过 + * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法 + * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法 + * this.redisTemplate.opsForSet(); //提供了操作set的所有方法 + * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法 + * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法 + * @param redisConnectionFactor + * @return + */ + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactor) + { + RedisTemplate redisTemplate = new RedisTemplate<>(); + + redisTemplate.setConnectionFactory(redisConnectionFactor); + //设置key序列化方式string + redisTemplate.setKeySerializer(new StringRedisSerializer()); + //设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化 + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + + redisTemplate.afterPropertiesSet(); + + return redisTemplate; + } +} + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/config/SaaLLMConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/config/SaaLLMConfig.java new file mode 100644 index 00000000..27b7f378 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/config/SaaLLMConfig.java @@ -0,0 +1,67 @@ +package com.atguigu.study.config; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SaaLLMConfig { + + private final String DEEPSEEK_MODEL = "deepseek-v3"; + private final String QWEN_MODEL = "qwen-plus"; + + + @Bean(name = "deepseek") + public ChatModel deepSeek() + { + return DashScopeChatModel.builder() + .dashScopeApi(DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build()) + .defaultOptions( + DashScopeChatOptions.builder().withModel(DEEPSEEK_MODEL).build() + ) + .build(); + } + + @Bean(name = "qwen") + public ChatModel qwen() + { + return DashScopeChatModel.builder().dashScopeApi(DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build()) + .defaultOptions( + DashScopeChatOptions.builder() + .withModel(QWEN_MODEL) + .build() + ) + .build(); + } + + @Bean(name = "deepseekChatClient") + public ChatClient deepseekChatClient(@Qualifier("deepseek") ChatModel deepSeek) + { + return ChatClient.builder(deepSeek) + .defaultOptions(ChatOptions.builder() + .model(DEEPSEEK_MODEL) + .build()) + .build(); + } + + + @Bean(name = "qwenChatClient") + public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel qwen) + { + return ChatClient.builder(qwen) + .defaultOptions(ChatOptions.builder() + .model(QWEN_MODEL) + .build()) + .build(); + } +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/controller/RagController.java b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/controller/RagController.java new file mode 100644 index 00000000..9f8eef41 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/java/com/atguigu/study/controller/RagController.java @@ -0,0 +1,43 @@ +package com.atguigu.study.controller; + +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor; +import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +@RestController +public class RagController +{ + + @Resource(name = "qwenChatClient") + private ChatClient chatClient; + @Resource + private VectorStore vectorStore; + + /** + * http://localhost:8012/rag1?msg=00000 + * http://localhost:8012/rag1?msg=C2222 + */ + @GetMapping("/rag1") + public Flux rag1(String msg) { + String systemInfo = """ + 你是一个运维工程师,按照给出的编码给出对应故障解释,否则回复找不到信息。 + """; + VectorStoreDocumentRetriever documentRetriever = VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).build(); + RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder() + .documentRetriever(documentRetriever) + .build(); + return chatClient + .prompt() + .system(systemInfo) + .user(msg) + .advisors(advisor) + .stream() + .content(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/resources/application.properties new file mode 100644 index 00000000..800f1e7b --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/resources/application.properties @@ -0,0 +1,21 @@ +server.port=8012 + +# \u8BBE\u7F6E\u5168\u5C40\u7F16\u7801\u683C\u5F0F +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-12RAG4AiDatabase + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} +spring.ai.dashscope.chat.options.model=deepseek-r1 +spring.ai.dashscope.embedding.options.model=text-embedding-v3 + + +# =======Redis Stack========== +spring.data.redis.host=192.168.1.221 +spring.data.redis.port=6379 +spring.ai.vectorstore.redis.initialize-schema=true +spring.ai.vectorstore.redis.index-name=custom-index +spring.ai.vectorstore.redis.prefix=custom-prefix diff --git a/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/resources/ops.txt b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/resources/ops.txt new file mode 100644 index 00000000..dd2e6259 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-12RAG4AiOps/src/main/resources/ops.txt @@ -0,0 +1,5 @@ +00000 系统OK正确执行后的返回 +A0001 用户端错误一级宏观错误码 +A0100 用户注册错误二级宏观错误码 +B1111 支付接口超时 +C2222 Kafka消息解压严重 \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/pom.xml new file mode 100644 index 00000000..5d574154 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-13ToolCalling + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/Saa13ToolCallingApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/Saa13ToolCallingApplication.java new file mode 100644 index 00000000..cf90b3b6 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/Saa13ToolCallingApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa13ToolCallingApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa13ToolCallingApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/config/SaaLLMConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/config/SaaLLMConfig.java new file mode 100644 index 00000000..10b429d4 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/config/SaaLLMConfig.java @@ -0,0 +1,40 @@ +package com.atguigu.study.config; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SaaLLMConfig +{ + @Bean + public ChatClient chatClient(ChatModel chatModel) + { + return ChatClient.builder(chatModel).build(); + } +} + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/controller/ToolCallingController.java b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/controller/ToolCallingController.java new file mode 100644 index 00000000..f4ced5fd --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/controller/ToolCallingController.java @@ -0,0 +1,36 @@ +package com.atguigu.study.controller; + +import com.atguigu.study.utils.DateTimeTools; +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +@RestController +public class ToolCallingController +{ + + @Resource + private ChatClient qwenChatClient; + + // http://localhost:8013/not/toolCalling?msg=你是谁,现在几点了 + @GetMapping("/not/toolCalling") + public Flux notToolCalling(String msg) + { + return qwenChatClient.prompt(msg) + .stream() + .content(); + } + + // http://localhost:8013/toolCalling?msg=你是谁,现在几点了 + @GetMapping("/toolCalling") + public Flux toolCalling(String msg) + { + return qwenChatClient.prompt(msg) + .tools(new DateTimeTools()) + .stream() + .content(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/utils/DateTimeTools.java b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/utils/DateTimeTools.java new file mode 100644 index 00000000..d53c9618 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/java/com/atguigu/study/utils/DateTimeTools.java @@ -0,0 +1,18 @@ +package com.atguigu.study.utils; + +import org.springframework.ai.tool.annotation.Tool; + +public class DateTimeTools { + + /** + * 1.定义 function call(tool call) + * 2. returnDirect + * true = tool直接返回不走大模型,直接给客户 + * false = 默认值,拿到tool返回的结果,给大模型,最后由大模型回复 + */ + @Tool(description = "获取当前时间", returnDirect = false) + public String getCurrentTime() { + return "现在时间是:" + java.time.LocalDateTime.now(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/resources/application.properties new file mode 100644 index 00000000..91c04b73 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-13ToolCalling/src/main/resources/application.properties @@ -0,0 +1,11 @@ +server.port=8013 + +# \u8BBE\u7F6E\u5168\u5C40\u7F16\u7801\u683C\u5F0F +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-13ToolCalling + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/pom.xml new file mode 100644 index 00000000..0f434be5 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-14LocalMcpServer + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.ai + spring-ai-starter-mcp-server-webflux + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/java/com/atguigu/study/Saa14LocalMcpServerApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/java/com/atguigu/study/Saa14LocalMcpServerApplication.java new file mode 100644 index 00000000..af6a5d83 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/java/com/atguigu/study/Saa14LocalMcpServerApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa14LocalMcpServerApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa14LocalMcpServerApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/java/com/atguigu/study/config/McpServerConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/java/com/atguigu/study/config/McpServerConfig.java new file mode 100644 index 00000000..41efeb38 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/java/com/atguigu/study/config/McpServerConfig.java @@ -0,0 +1,22 @@ +package com.atguigu.study.config; + +import com.atguigu.study.service.WeatherService; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.method.MethodToolCallbackProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class McpServerConfig { + + /** + * 将工具方法暴露给外部 mcp client 调用 + */ + @Bean + public ToolCallbackProvider weatherTools(WeatherService weatherService) { + return MethodToolCallbackProvider.builder() + .toolObjects(weatherService) + .build(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/java/com/atguigu/study/service/WeatherService.java b/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/java/com/atguigu/study/service/WeatherService.java new file mode 100644 index 00000000..d342009f --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/java/com/atguigu/study/service/WeatherService.java @@ -0,0 +1,22 @@ +package com.atguigu.study.service; + +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +public class WeatherService { + + @Tool(description = "根据城市名称获取天气预报") + public String getWeatherForecast(String city) { + Map map = Map.of( + "北京", "天气晴转多云,温度 18℃", + "上海", "天气阴转雷阵雨,温度 17℃", + "广州", "天气多云转阴,温度 19℃", + "深圳", "天气雷阵雨转小雨,温度 16℃" + ); + return map.getOrDefault(city, "没有找到该城市的天气信息"); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/resources/application.properties new file mode 100644 index 00000000..9c725be1 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-14LocalMcpServer/src/main/resources/application.properties @@ -0,0 +1,14 @@ +server.port=8014 + +# \u8BBE\u7F6E\u5168\u5C40\u7F16\u7801\u683C\u5F0F +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-14LocalMcpServer + + +# ====mcp-server Config============= +spring.ai.mcp.server.type=async +spring.ai.mcp.server.name=local-mcp-1 +spring.ai.mcp.server.version=1.0.0 \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/pom.xml new file mode 100644 index 00000000..8b983f27 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-15LocalMcpClient + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.springframework.ai + spring-ai-starter-mcp-client + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/java/com/atguigu/study/Saa15LocalMcpClientApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/java/com/atguigu/study/Saa15LocalMcpClientApplication.java new file mode 100644 index 00000000..1f253655 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/java/com/atguigu/study/Saa15LocalMcpClientApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa15LocalMcpClientApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa15LocalMcpClientApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/java/com/atguigu/study/config/SaaLLMConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/java/com/atguigu/study/config/SaaLLMConfig.java new file mode 100644 index 00000000..f97784c5 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/java/com/atguigu/study/config/SaaLLMConfig.java @@ -0,0 +1,17 @@ +package com.atguigu.study.config; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SaaLLMConfig { + + @Bean + public ChatClient chatClient(ChatModel chatModel, ToolCallbackProvider tools) { + return ChatClient.builder(chatModel).defaultToolCallbacks(tools.getToolCallbacks()).build(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/java/com/atguigu/study/controller/McpClientController.java b/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/java/com/atguigu/study/controller/McpClientController.java new file mode 100644 index 00000000..37c10c3c --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/java/com/atguigu/study/controller/McpClientController.java @@ -0,0 +1,22 @@ +package com.atguigu.study.controller; + +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +@RestController +public class McpClientController { + + @Resource + private ChatClient chatClient; + + // http://localhost:8015/mcpClient/chat?msg=上海 + @GetMapping("/mcpClient/chat") + public Flux chat(@RequestParam(name = "msg") String msg) { + return chatClient.prompt(msg).stream().content(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/resources/application.properties new file mode 100644 index 00000000..5f0f91ce --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-15LocalMcpClient/src/main/resources/application.properties @@ -0,0 +1,18 @@ +server.port=8015 + +# \u8BBE\u7F6E\u5168\u5C40\u7F16\u7801\u683C\u5F0F +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-15LocalMcpClient + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} + +# ====mcp-client Config============= +spring.ai.mcp.client.type=async +spring.ai.mcp.client.request-timeout=60s +spring.ai.mcp.client.toolcallback.enabled=true +spring.ai.mcp.client.sse.connections.mcp-server1.url=http://localhost:8014 + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/pom.xml new file mode 100644 index 00000000..081ab7ea --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + SAA-16ClientCallBaiduMcpServer + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.springframework.ai + spring-ai-starter-mcp-client + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/java/com/atguigu/study/Saa16ClientCallBaiduMcpServerApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/java/com/atguigu/study/Saa16ClientCallBaiduMcpServerApplication.java new file mode 100644 index 00000000..525fece3 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/java/com/atguigu/study/Saa16ClientCallBaiduMcpServerApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa16ClientCallBaiduMcpServerApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa16ClientCallBaiduMcpServerApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/java/com/atguigu/study/config/SaaLLMConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/java/com/atguigu/study/config/SaaLLMConfig.java new file mode 100644 index 00000000..d102c9e5 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/java/com/atguigu/study/config/SaaLLMConfig.java @@ -0,0 +1,20 @@ +package com.atguigu.study.config; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SaaLLMConfig +{ + @Bean + public ChatClient chatClient(ChatModel chatModel, ToolCallbackProvider tools) + { + return ChatClient.builder(chatModel) + //mcp协议,配置见yml文件,此处只赋能给ChatClient对象 + .defaultToolCallbacks(tools.getToolCallbacks()) + .build(); + } +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/java/com/atguigu/study/controller/McpClientCallBaiDuMcpController.java b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/java/com/atguigu/study/controller/McpClientCallBaiDuMcpController.java new file mode 100644 index 00000000..ddfd7321 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/java/com/atguigu/study/controller/McpClientCallBaiDuMcpController.java @@ -0,0 +1,31 @@ +package com.atguigu.study.controller; + +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + + +@RestController +public class McpClientCallBaiDuMcpController +{ + + @Resource + private ChatClient chatClient; //添加了MCP调用能力 + + /** + * 添加了MCP调用能力 + * http://localhost:8016/mcp/chat?msg=查询北纬39.9042东经116.4074天气 + * http://localhost:8016/mcp/chat?msg=查询61.149.121.66归属地 + * http://localhost:8016/mcp/chat?msg=查询昌平到天安门路线规划 + * @param msg + * @return + */ + @GetMapping("/mcp/chat") + public Flux mapChat(String msg) { + return chatClient.prompt(msg).stream().content(); + } + +} + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/resources/application.properties new file mode 100644 index 00000000..d4266ff0 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/resources/application.properties @@ -0,0 +1,16 @@ +server.port=8016 + +# \u8BBE\u7F6E\u5168\u5C40\u7F16\u7801\u683C\u5F0F +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-16ClientCallBaiduMcpServer + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} + +# ====mcp-client Config============= +spring.ai.mcp.client.request-timeout=60s +spring.ai.mcp.client.toolcallback.enabled=true +spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-server.json5 \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/resources/mcp-server.json5 b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/resources/mcp-server.json5 new file mode 100644 index 00000000..442837aa --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-16ClientCallBaiduMcpServer/src/main/resources/mcp-server.json5 @@ -0,0 +1,24 @@ +{ + "mcpServers": + { + "baidu-map": + { + + "command": "cmd", + "args": ["/c", "npx", "-y", "@baidumap/mcp-server-baidu-map"], + "env": {"BAIDU_MAP_API_KEY": "xx"} + } + } +} + +// mcp-server-baidu-map地址 https://mcp.so/zh/server/baidu-map/baidu-maps + +// 构建McpTransport协议 + +//cmd:启动 Windows 命令行解释器。 +///c:告诉 cmd 执行完后面的命令后关闭自身。 +//npx:npx = npm execute package,Node.js 的一个工具,用于执行 npm 包中的可执行文件。 +//-y 或 --yes:自动确认操作(类似于默认接受所有提示)。 +//@baidumap/mcp-server-baidu-map:要通过 npx 执行的 npm 包名 +//BAIDU_MAP_API_KEY 是访问百度地图开放平台API的AK + diff --git a/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/pom.xml new file mode 100644 index 00000000..0fcc394c --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-17BailianRAG + + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/java/com/atguigu/study/Saa17BailianRagApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/java/com/atguigu/study/Saa17BailianRagApplication.java new file mode 100644 index 00000000..113a1361 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/java/com/atguigu/study/Saa17BailianRagApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa17BailianRagApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa17BailianRagApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/java/com/atguigu/study/config/DashScopeConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/java/com/atguigu/study/config/DashScopeConfig.java new file mode 100644 index 00000000..e2066f85 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/java/com/atguigu/study/config/DashScopeConfig.java @@ -0,0 +1,32 @@ +package com.atguigu.study.config; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class DashScopeConfig { + + @Value("${spring.ai.dashscope.api-key}") + private String apiKey; + + @Bean(name = "dashScopeAPi") + public DashScopeApi dashScopeAPi() { + // workSpaceId是阿里云百炼平台的业务空间ID + // https://bailian.console.aliyun.com/?tab=app#/knowledge-base/detail/6aovdv07rm + return DashScopeApi.builder() + .apiKey(apiKey) + .workSpaceId("llm-p01mpsp4k19pjldd") + .build(); + } + + + @Bean(name = "chatClient") + public ChatClient chatClient(ChatModel chatModel) { + return ChatClient.builder(chatModel).build(); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/java/com/atguigu/study/controller/BailianRagController.java b/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/java/com/atguigu/study/controller/BailianRagController.java new file mode 100644 index 00000000..8b168793 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/java/com/atguigu/study/controller/BailianRagController.java @@ -0,0 +1,41 @@ +package com.atguigu.study.controller; + +import com.alibaba.cloud.ai.advisor.DocumentRetrievalAdvisor; +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.rag.DashScopeDocumentRetriever; +import com.alibaba.cloud.ai.dashscope.rag.DashScopeDocumentRetrieverOptions; +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + + +@RestController +public class BailianRagController +{ + + @Resource + private ChatClient chatClient; + @Resource + private DashScopeApi dashScopeApi; + + /** + * http://localhost:8017/bailian/rag/chat?msg=A0001 + */ + @GetMapping("/bailian/rag/chat") + public Flux chat(@RequestParam(name = "msg", defaultValue = "00000错误信息")String msg) { + // indexName在阿里云百炼平台 -》知识库的名称 + // https://bailian.console.aliyun.com/?tab=app#/knowledge-base/detail/6aovdv07rm + DashScopeDocumentRetrieverOptions options = DashScopeDocumentRetrieverOptions.builder().withIndexName("智能运维").build(); + DashScopeDocumentRetriever retriever = new DashScopeDocumentRetriever(dashScopeApi, options); + return chatClient.prompt() + .user(msg) + .advisors(new DocumentRetrievalAdvisor(retriever)) + .stream() + .content(); + } + + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/resources/application.properties new file mode 100644 index 00000000..b39fd00a --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-17BailianRAG/src/main/resources/application.properties @@ -0,0 +1,12 @@ +server.port=8017 + +# \u8BBE\u7F6E\u5168\u5C40\u7F16\u7801\u683C\u5F0F +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-17BailianRAG + + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/pom.xml b/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/pom.xml new file mode 100644 index 00000000..6238fa4b --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + + + org.cpq + + SAA-18TodayMenu + + + + org.springframework.boot + spring-boot-starter-web + + + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.projectlombok + lombok + 1.18.38 + + + + cn.hutool + hutool-all + 5.8.22 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + -parameters + + 21 + 21 + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/java/com/atguigu/study/Saa18TodayMenuApplication.java b/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/java/com/atguigu/study/Saa18TodayMenuApplication.java new file mode 100644 index 00000000..9c72dc9b --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/java/com/atguigu/study/Saa18TodayMenuApplication.java @@ -0,0 +1,15 @@ +package com.atguigu.study; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Saa18TodayMenuApplication +{ + + public static void main(String[] args) + { + SpringApplication.run(Saa18TodayMenuApplication.class, args); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/java/com/atguigu/study/config/DashScopeConfig.java b/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/java/com/atguigu/study/config/DashScopeConfig.java new file mode 100644 index 00000000..fbb8c652 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/java/com/atguigu/study/config/DashScopeConfig.java @@ -0,0 +1,40 @@ +package com.atguigu.study.config; + +import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgent; +import com.alibaba.cloud.ai.dashscope.api.DashScopeAgentApi; +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class DashScopeConfig +{ + + @Value("${spring.ai.dashscope.api-key}") + private String apiKey; + + @Bean + public DashScopeApi dashScopeApi() + { + return DashScopeApi.builder() + .apiKey(apiKey) + .workSpaceId("llm-p01mpsp4k19pjldd") + .build(); + } + + @Bean + public ChatClient chatClient(ChatModel chatModel) + { + return ChatClient.builder(chatModel).build(); + } + + @Bean + public DashScopeAgent dashScopeAgent(DashScopeAgentApi dashScopeAgentApi) { + return new DashScopeAgent(dashScopeAgentApi); + } + + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/java/com/atguigu/study/controller/MenuCallAgentController.java b/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/java/com/atguigu/study/controller/MenuCallAgentController.java new file mode 100644 index 00000000..2a516497 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/java/com/atguigu/study/controller/MenuCallAgentController.java @@ -0,0 +1,32 @@ +package com.atguigu.study.controller; + +import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgent; +import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgentOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + + +@RestController +public class MenuCallAgentController +{ + + @Value("${spring.ai.dashscope.agent.options.app-id}") + private String appId; + + @Autowired + private DashScopeAgent dashScopeAgent; + + @GetMapping(value = "/eatAgent", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux eatAgent(@RequestParam(name = "msg", defaultValue = "今天吃什么") String msg) { + DashScopeAgentOptions options = DashScopeAgentOptions.builder().withAppId(appId).build(); + Prompt prompt = new Prompt(msg, options); + return dashScopeAgent.stream(prompt).map(response -> response.getResult().getOutput().getText()); + } + +} diff --git a/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/resources/application.properties b/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/resources/application.properties new file mode 100644 index 00000000..ff00d42a --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/SAA-18TodayMenu/src/main/resources/application.properties @@ -0,0 +1,15 @@ +server.port=8018 + +# \u8BBE\u7F6E\u5168\u5C40\u7F16\u7801\u683C\u5F0F +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true +server.servlet.encoding.charset=UTF-8 + +spring.application.name=SAA-18TodayMenu + + +# ====SpringAIAlibaba Config============= +spring.ai.dashscope.api-key=${ALI_AI_KEY} + +# \u963F\u91CC\u4E91\u767E\u70BC\u5E73\u53F0\u5E94\u7528 -> \u5E94\u7528\u7BA1\u7406 -\u300B\u5E94\u7528id +spring.ai.dashscope.agent.options.app-id=6bad56de04904e768db17ae68cb078c5 \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-01/pom.xml b/project/spingai/spring-ai-alibaba-01/pom.xml new file mode 100644 index 00000000..b562b5bb --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/pom.xml @@ -0,0 +1,98 @@ + + 4.0.0 + + org.cpq + spring-ai-alibaba-01 + 1.0-SNAPSHOT + pom + + + UTF-8 + UTF-8 + 21 + 21 + 21 + + 3.5.5 + + 1.0.0 + + 1.0.0.2 + + + + SAA-01HelloWorld + SAA-02Ollama + SAA-03ChatModelChatClient + SAA-04StreamingOutput + SAA-05Prompt + SAA-06PromptTemplate + SAA-07StructuredOutput + SAA-08Persistent + SAA-09Text2image + SAA-10Text2voice + SAA-11Embed2vector + SAA-12RAG4AiOps + SAA-13ToolCalling + SAA-14LocalMcpServer + SAA-15LocalMcpClient + SAA-16ClientCallBaiduMcpServer + SAA-17BailianRAG + SAA-18TodayMenu + + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + com.alibaba.cloud.ai + spring-ai-alibaba-bom + ${SpringAIAlibaba.version} + pom + import + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + diff --git a/project/spingai/spring-ai-alibaba-01/readme.md b/project/spingai/spring-ai-alibaba-01/readme.md new file mode 100644 index 00000000..2a5fd50c --- /dev/null +++ b/project/spingai/spring-ai-alibaba-01/readme.md @@ -0,0 +1,4 @@ +阿里云百炼平台: + +https://bailian.console.aliyun.com/?tab=model#/api-key + diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/README.md b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/README.md new file mode 100644 index 00000000..e80d8785 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/README.md @@ -0,0 +1,39 @@ +# ReAct Agent Example + +This example showcases basic ReactAgent usage in Spring AI Alibaba. + +## Quick Start + +### Prerequisites + +* Requires JDK 17+. +* Choose your LLM provider and get the API-KEY. + +```shell +export AI_DASHSCOPE_API_KEY=your-api-key +``` + +### Run the ChatBot + +1. Download the code. + +```shell +git clone https://github.com/alibaba/spring-ai-alibaba.git +cd examples/chatbot +``` + +2. Start the ChatBot. + +```shell +mvn spring-boot:run +``` + +3. Chat with ChatBot. +Open the browser and visit [http://localhost:8080/chatui/index.html](http://localhost:8080/chatui/index.html) to chat with the ChatBot. + +

+ chatbot-ui +

+ +## More Examples +Check [spring-ai-alibaba-examples](https://github.com/spring-ai-alibaba/examples/tree/main/spring-ai-alibaba-agent-example) for more sophisticated examples. diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/pom.xml b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/pom.xml new file mode 100644 index 00000000..11c18f9f --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/pom.xml @@ -0,0 +1,140 @@ + + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + + com.alibaba.cloud.ai + a01-chatbot + 0.0.1-SNAPSHOT + Examples::ChatBot + ChatBot example using Spring AI Alibaba + + + 17 + 1.1.0 + 1.1.0.0-RC1 + 1.1.0.0-RC1 + + 24.2.1 + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + com.alibaba.cloud.ai + spring-ai-alibaba-extensions-bom + ${spring-ai-alibaba-extensions.version} + pom + import + + + com.alibaba.cloud.ai + spring-ai-alibaba-bom + ${spring-ai-alibaba.version} + pom + import + + + + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + com.alibaba.cloud.ai + spring-ai-alibaba-agent-framework + + + com.alibaba.cloud.ai + spring-ai-alibaba-studio + + + org.springframework.boot + spring-boot-starter + + + + + org.graalvm.polyglot + polyglot + ${graalvm.polyglot.version} + true + + + org.graalvm.polyglot + python-community + ${graalvm.polyglot.version} + pom + true + + + + org.projectlombok + lombok + 1.18.38 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + + false + + + true + always + + sonatype-snapshots + Sonatype Snapshot Repository + https://oss.sonatype.org/content/repositories/snapshots + + + + diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/AgentStaticLoader.java b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/AgentStaticLoader.java new file mode 100644 index 00000000..efbd1054 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/AgentStaticLoader.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.chatbot; + +import com.alibaba.cloud.ai.agent.studio.loader.AgentLoader; +import com.alibaba.cloud.ai.graph.GraphRepresentation; +import com.alibaba.cloud.ai.graph.agent.Agent; +import jakarta.annotation.Nonnull; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Static Agent Loader for programmatically provided agents. + * + *

This loader takes a static list of pre-created agent instances and makes them available + * through the AgentLoader interface. Perfect for cases where you already have agent instances and + * just need a convenient way to wrap them in an AgentLoader. + * + *

This class is not a Spring component by itself - instances are created programmatically and + * then registered as beans via factory methods. + */ +@Component +class AgentStaticLoader implements AgentLoader { + + private final Map agents = new ConcurrentHashMap<>(); + + public AgentStaticLoader(Agent agent) { + + GraphRepresentation representation = agent.getAndCompileGraph().stateGraph.getGraph(GraphRepresentation.Type.PLANTUML); + System.out.println(representation.content()); + + this.agents.put("research_agent", agent); + } + + @Override + @Nonnull + public List listAgents() { + return agents.keySet().stream().toList(); + } + + @Override + public Agent loadAgent(String name) { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Agent name cannot be null or empty"); + } + + Agent agent = agents.get(name); + if (agent == null) { + throw new NoSuchElementException("Agent not found: " + name); + } + + return agent; + } +} diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/ChatbotAgent.java b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/ChatbotAgent.java new file mode 100644 index 00000000..62ce173a --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/ChatbotAgent.java @@ -0,0 +1,126 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.chatbot; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import com.alibaba.cloud.ai.examples.chatbot.tool.UserLocationTool; +import com.alibaba.cloud.ai.examples.chatbot.tool.WeatherForLocationTool; +import com.alibaba.cloud.ai.examples.chatbot.tool.WeatherTool; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.hook.Hook; +import com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook; +import com.alibaba.cloud.ai.graph.agent.hook.hip.ToolConfig; +import com.alibaba.cloud.ai.graph.agent.hook.modelcalllimit.ModelCallLimitHook; +import com.alibaba.cloud.ai.graph.agent.hook.summarization.SummarizationHook; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +public class ChatbotAgent { + + private final String QWEN_MODEL = "qwen-plus"; + + String customSchema = """ + 请按照以下JSON格式输出: + { + "title": "标题", + "content": "内容", + "style": "风格" + } + """; + + + ToolCallback weatherTool = FunctionToolCallback.builder("get_weather", new WeatherTool()) + .description("Get weather for a given city") + .inputType(String.class) + .build(); + + ToolCallback getWeatherTool = FunctionToolCallback + .builder("getWeatherForLocation", new WeatherForLocationTool()) + .description("Get weather for a given city") + .inputType(String.class) + .build(); + + ToolCallback getUserLocationTool = FunctionToolCallback + .builder("getUserLocation", new UserLocationTool()) + .description("Retrieve user location based on user ID") + .inputType(String.class) + .build(); + + Hook humanInTheLoopHook = HumanInTheLoopHook.builder() + .approvalOn("getWeatherTool", + ToolConfig.builder().description("Please confirm tool execution.").build()) + .build(); + + @Bean("dashScopeApi") + public DashScopeApi dashScopeApi() { + return DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + } + + /** + * 自定义的ChatModel必须填写.model(模型名称)参数 + * @param dashScopeApi + * @return + */ + @Bean("chatModel") + public ChatModel chatModel(@Qualifier("dashScopeApi") DashScopeApi dashScopeApi) { + return DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(DashScopeChatOptions.builder() + .model(QWEN_MODEL) // 控制随机性 + .withTemperature(0.7) // 控制随机性 + .withMaxToken(2000) // 最大输出长度 + .withTopP(0.9) // 核采样参数 + .build()) + .build(); + } + + @Bean + public ReactAgent agent(@Qualifier("chatModel") ChatModel chatModel) { + SummarizationHook summarizationHook = SummarizationHook.builder() + .model(chatModel) + .maxTokensBeforeSummary(4000) + .messagesToKeep(20) + .build(); + ModelCallLimitHook modelCallLimitHook = ModelCallLimitHook.builder().runLimit(5).build(); + return ReactAgent.builder() + .name("schema_agent") + .model(chatModel) + .tools(getWeatherTool) + .saver(new MemorySaver()) + .hooks(modelCallLimitHook, summarizationHook) + .build(); + } + + @Bean + public MemorySaver memorySaver() { + return new MemorySaver(); + } + + +} + diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/ChatbotApplication.java b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/ChatbotApplication.java new file mode 100644 index 00000000..1e7b1737 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/ChatbotApplication.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.chatbot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; + +@SpringBootApplication +public class ChatbotApplication { + public static void main(String[] args) { + SpringApplication.run(ChatbotApplication.class, args); + } + + @Bean + public ApplicationListener applicationReadyEventListener(Environment environment) { + return event -> { + String port = environment.getProperty("server.port", "8080"); + String contextPath = environment.getProperty("server.servlet.context-path", ""); + String accessUrl = "http://localhost:" + port + contextPath + "/chatui/index.html"; + System.out.println("\n🎉========================================🎉"); + System.out.println("✅ Application is ready!"); + System.out.println("🚀 Chat with you agent: " + accessUrl); + System.out.println("🎉========================================🎉\n"); + }; + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/controller/ChatController.java b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/controller/ChatController.java new file mode 100644 index 00000000..7072325f --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/controller/ChatController.java @@ -0,0 +1,29 @@ +package com.alibaba.cloud.ai.examples.chatbot.controller; + +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import jakarta.annotation.Resource; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequestMapping("chat") +public class ChatController { + + + @Resource + private ReactAgent agent; + + /** + * @Description: 聊天 + SSE返回 + */ + @PostMapping("doChat") + public void doChat() throws Exception { + AssistantMessage response = agent.call("分析这段文本:春天来了,万物复苏。"); +// 输出会遵循 PoemOutput 的结构 + System.out.println(response.getText()); + } + +} diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/resp/PoemOutput.java b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/resp/PoemOutput.java new file mode 100644 index 00000000..e1f95d0b --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/resp/PoemOutput.java @@ -0,0 +1,17 @@ +package com.alibaba.cloud.ai.examples.chatbot.resp; + +public class PoemOutput { + private String title; + private String content; + private String style; + + // Getters and Setters + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + + public String getStyle() { return style; } + public void setStyle(String style) { this.style = style; } +} \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/resp/ResponseFormat.java b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/resp/ResponseFormat.java new file mode 100644 index 00000000..6e638053 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/resp/ResponseFormat.java @@ -0,0 +1,27 @@ +package com.alibaba.cloud.ai.examples.chatbot.resp; + +// 使用 Java 类定义响应格式 +public class ResponseFormat { + // 一个双关语响应(始终必需) + private String punnyResponse; + + // 如果可用的话,关于天气的任何有趣信息 + private String weatherConditions; + + // Getters and Setters + public String getPunnyResponse() { + return punnyResponse; + } + + public void setPunnyResponse(String punnyResponse) { + this.punnyResponse = punnyResponse; + } + + public String getWeatherConditions() { + return weatherConditions; + } + + public void setWeatherConditions(String weatherConditions) { + this.weatherConditions = weatherConditions; + } +} diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/resp/TextAnalysisResult.java b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/resp/TextAnalysisResult.java new file mode 100644 index 00000000..0ba4ed66 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/resp/TextAnalysisResult.java @@ -0,0 +1,21 @@ +package com.alibaba.cloud.ai.examples.chatbot.resp; + +import java.util.List; + +public class TextAnalysisResult { + private String summary; + private List keywords; + private String sentiment; + private Double confidence; + + // Getters and Setters + public String getSummary() { return summary; } + public void setSummary(String summary) { this.summary = summary; } + public List getKeywords() { return keywords; } + public void setKeywords(List keywords) { this.keywords = keywords; } + public String getSentiment() { return sentiment; } + public void setSentiment(String sentiment) { this.sentiment = sentiment; } + public Double getConfidence() { return confidence; } + public void setConfidence(Double confidence) { this.confidence = confidence; } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/tool/UserLocationTool.java b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/tool/UserLocationTool.java new file mode 100644 index 00000000..ad945339 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/tool/UserLocationTool.java @@ -0,0 +1,16 @@ +package com.alibaba.cloud.ai.examples.chatbot.tool; + +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.annotation.ToolParam; +import java.util.function.BiFunction; + +public class UserLocationTool implements BiFunction { + @Override + public String apply( + @ToolParam(description = "User query") String query, + ToolContext toolContext) { + // 从上下文中获取用户信息 + String userId = (String) toolContext.getContext().get("user_id"); + return "1".equals(userId) ? "Florida" : "San Francisco"; + } +} diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/tool/WeatherForLocationTool.java b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/tool/WeatherForLocationTool.java new file mode 100644 index 00000000..57ffd614 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/tool/WeatherForLocationTool.java @@ -0,0 +1,15 @@ +package com.alibaba.cloud.ai.examples.chatbot.tool; + +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.annotation.ToolParam; + +import java.util.function.BiFunction; + +public class WeatherForLocationTool implements BiFunction { + @Override + public String apply( + @ToolParam(description = "The city name") String city, + ToolContext toolContext) { + return "It's always sunny in " + city + "!"; + } +} diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/tool/WeatherTool.java b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/tool/WeatherTool.java new file mode 100644 index 00000000..7103f5c4 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/java/com/alibaba/cloud/ai/examples/chatbot/tool/WeatherTool.java @@ -0,0 +1,12 @@ +package com.alibaba.cloud.ai.examples.chatbot.tool; + +import org.springframework.ai.chat.model.ToolContext; + +import java.util.function.BiFunction; + +public class WeatherTool implements BiFunction { + @Override + public String apply(String city, ToolContext toolContext) { + return "It's always sunny in " + city + "!"; + } +} diff --git a/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/resources/application.yml b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/resources/application.yml new file mode 100644 index 00000000..151e2075 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/a01-chatbot/src/main/resources/application.yml @@ -0,0 +1,4 @@ +spring: + ai: + dashscope: + api-key: ${ALI_AI_KEY} \ No newline at end of file diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/README.md b/project/spingai/spring-ai-alibaba-examples/documentation/README.md new file mode 100644 index 00000000..582d5bf8 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/README.md @@ -0,0 +1 @@ +This directory contains the full code examples for documentations published on https://java2ai.com diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/pom.xml b/project/spingai/spring-ai-alibaba-examples/documentation/pom.xml new file mode 100644 index 00000000..8594f05e --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + com.alibaba.cloud.ai + documentation + 0.0.1-SNAPSHOT + Examples::Documentation + Examples for Website. + + + + + + + + + + + + + + + + 17 + 1.1.0 + 1.1.0.0-RC2 + 1.1.0.0-RC2 + + 3.22.0 + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + com.alibaba.cloud.ai + spring-ai-alibaba-extensions-bom + ${spring-ai-alibaba-extensions.version} + pom + import + + + com.alibaba.cloud.ai + spring-ai-alibaba-bom + ${spring-ai-alibaba.version} + pom + import + + + + + + + com.alibaba.cloud.ai + spring-ai-alibaba-studio + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + com.alibaba.cloud.ai + spring-ai-alibaba-agent-framework + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-a2a-nacos + + + + org.springframework.ai + spring-ai-starter-model-openai + + + + org.springframework.ai + spring-ai-starter-model-deepseek + + + + org.springframework.ai + spring-ai-starter-mcp-client + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.redisson + redisson + ${redission.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + + false + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + + + false + + + true + always + + snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + + + false + + + true + always + + sonatype-snapshots + Sonatype Snapshot Repository + https://oss.sonatype.org/content/repositories/snapshots + + + + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/AgentToolExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/AgentToolExample.java new file mode 100644 index 00000000..f377a345 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/AgentToolExample.java @@ -0,0 +1,513 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.agent.AgentTool; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; + +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.converter.BeanOutputConverter; + +import java.util.List; +import java.util.Optional; + +/** + * 智能体作为工具(Agent Tool)示例 + * + * 演示 Multi-agent 工具调用模式,包括: + * 1. 将子Agent作为工具使用 + * 2. 自定义输入和输出Schema + * 3. 类型化的Agent工具调用 + * 4. 完整的工具调用示例 + * + * 参考文档: advanced_doc/agent-tool.md + */ +public class AgentToolExample { + + private final ChatModel chatModel; + + public AgentToolExample(ChatModel chatModel) { + this.chatModel = chatModel; + } + + /** + * Main方法:运行所有示例 + * + * 注意:需要配置ChatModel实例才能运行 + */ + public static void main(String[] args) { + // 创建 DashScope API 实例 + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 创建 ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + if (chatModel == null) { + System.err.println("错误:请先配置ChatModel实例"); + System.err.println("请设置 AI_DASHSCOPE_API_KEY 环境变量"); + return; + } + + // 创建示例实例 + AgentToolExample example = new AgentToolExample(chatModel); + + // 运行所有示例 + example.runAllExamples(); + } + + /** + * 示例1:基础 Agent Tool 调用 + * + * 主Agent将子Agent作为工具调用,子Agent执行特定任务并返回结果 + */ + public void example1_basicAgentTool() throws GraphRunnerException { + // 创建子Agent - 作为工具使用 + ReactAgent writerAgent = ReactAgent.builder() + .name("writer_agent") + .model(chatModel) + .description("可以写文章") + .instruction("你是一个知名的作家,擅长写作和创作。请根据用户的提问进行回答。") + .build(); + + // 创建主Agent,将子Agent作为工具 + ReactAgent blogAgent = ReactAgent.builder() + .name("blog_agent") + .model(chatModel) + .instruction("根据用户给定的主题写一篇文章。使用写作工具来完成任务。") + .tools(AgentTool.getFunctionToolCallback(writerAgent)) + .build(); + + // 使用 + Optional result = blogAgent.invoke("帮我写一个100字左右的散文"); + + if (result.isPresent()) { + System.out.println("文章生成成功"); + // 处理结果 + } + } + + /** + * 示例2:使用 inputSchema 控制子Agent的输入 + * + * 通过定义输入Schema,使子Agent能够接收结构化的输入信息 + */ + public void example2_agentToolWithInputSchema() throws GraphRunnerException { + // 定义子Agent的输入Schema + String writerInputSchema = """ + { + "type": "object", + "properties": { + "topic": { + "type": "string" + }, + "wordCount": { + "type": "integer" + }, + "style": { + "type": "string" + } + }, + "required": ["topic", "wordCount", "style"] + } + """; + + ReactAgent writerAgent = ReactAgent.builder() + .name("structured_writer_agent") + .model(chatModel) + .description("根据结构化输入写文章") + .instruction("你是一个专业作家。请严格按照输入的主题、字数和风格要求创作文章。") + .inputSchema(writerInputSchema) + .build(); + + ReactAgent coordinatorAgent = ReactAgent.builder() + .name("coordinator_agent") + .model(chatModel) + .instruction("你需要调用写作工具来完成用户的写作请求。请根据用户需求,使用结构化的参数调用写作工具。") + .tools(AgentTool.getFunctionToolCallback(writerAgent)) + .build(); + + Optional result = coordinatorAgent.invoke("请写一篇关于春天的散文,大约150字"); + + if (result.isPresent()) { + System.out.println("结构化输入示例执行成功"); + } + } + + /** + * 示例3:使用 inputType 定义类型化输入 + * + * 使用 Java 类型定义输入,框架会自动生成 JSON Schema + */ + public void example3_agentToolWithInputType() throws GraphRunnerException { + // 定义输入类型 + record ArticleRequest(String topic, int wordCount, String style) { } + + ReactAgent writerAgent = ReactAgent.builder() + .name("typed_writer_agent") + .model(chatModel) + .description("根据类型化输入写文章") + .instruction("你是一个专业作家。请严格按照输入的 topic(主题)、wordCount(字数)和 style(风格)要求创作文章。") + .inputType(ArticleRequest.class) + .build(); + + ReactAgent coordinatorAgent = ReactAgent.builder() + .name("coordinator_with_type_agent") + .model(chatModel) + .instruction("你需要调用写作工具来完成用户的写作请求。工具接收 JSON 格式的参数。") + .tools(AgentTool.getFunctionToolCallback(writerAgent)) + .build(); + + Optional result = coordinatorAgent.invoke("请写一篇关于秋天的现代诗,大约100字"); + + if (result.isPresent()) { + System.out.println("类型化输入示例执行成功"); + } + } + + /** + * 示例4:使用 outputSchema 控制子Agent的输出 + * + * 定义输出Schema,使子Agent返回结构化的输出格式 + */ + public void example4_agentToolWithOutputSchema() throws GraphRunnerException { + // Use BeanOutputConverter to generate outputSchema + BeanOutputConverter outputConverter = new BeanOutputConverter<>(ArticleOutput.class); + String format = outputConverter.getFormat(); + + ReactAgent writerAgent = ReactAgent.builder() + .name("writer_with_output_schema") + .model(chatModel) + .description("写文章并返回结构化输出") + .instruction("你是一个专业作家。请创作文章并严格按照指定的JSON格式返回结果。") + .outputSchema(format) + .build(); + + ReactAgent coordinatorAgent = ReactAgent.builder() + .name("coordinator_output_schema") + .model(chatModel) + .instruction("调用写作工具完成用户请求,工具会返回结构化的文章数据。") + .tools(AgentTool.getFunctionToolCallback(writerAgent)) + .build(); + + Optional result = coordinatorAgent.invoke("写一篇关于冬天的短文"); + + if (result.isPresent()) { + System.out.println("结构化输出示例执行成功"); + } + } + + /** + * 示例5:使用 outputType 定义类型化输出 + * + * 使用 Java 类型定义输出,框架会自动生成输出 schema + */ + public void example5_agentToolWithOutputType() throws GraphRunnerException { + // 定义输出类型 + class ArticleOutput { + private String title; + private String content; + private int characterCount; + + // getters and setters + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + + public int getCharacterCount() { + return characterCount; + } + + public void setTitle(String title) { + this.title = title; + } + + + public void setContent(String content) { + this.content = content; + } + + + public void setCharacterCount(int characterCount) { + this.characterCount = characterCount; + } + } + + ReactAgent writerAgent = ReactAgent.builder() + .name("writer_with_output_type") + .model(chatModel) + .description("写文章并返回类型化输出") + .instruction("你是一个专业作家。请创作文章并返回包含 title、content 和 characterCount 的结构化结果。") + .outputType(ArticleOutput.class) + .build(); + + ReactAgent coordinatorAgent = ReactAgent.builder() + .name("coordinator_output_type") + .model(chatModel) + .instruction("调用写作工具完成用户请求。") + .tools(AgentTool.getFunctionToolCallback(writerAgent)) + .build(); + + Optional result = coordinatorAgent.invoke("写一篇关于夏天的小诗"); + + if (result.isPresent()) { + System.out.println("类型化输出示例执行成功"); + } + } + + /** + * 示例6:完整类型化示例 + * + * 同时使用 inputType 和 outputType 进行完整的类型化Agent工具调用 + */ + public void example6_fullTypedAgentTool() throws GraphRunnerException { + // 定义输入和输出类型 + record ArticleRequest(String topic, int wordCount, String style) { } + + class ArticleOutput { + private String title; + private String content; + private int characterCount; + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + + public int getCharacterCount() { + return characterCount; + } + + public void setTitle(String title) { + this.title = title; + } + + + public void setContent(String content) { + this.content = content; + } + + + public void setCharacterCount(int characterCount) { + this.characterCount = characterCount; + } + } + + class ReviewOutput { + private String comment; + private boolean approved; + private List suggestions; + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public boolean isApproved() { + return approved; + } + + public void setApproved(boolean approved) { + this.approved = approved; + } + + public List getSuggestions() { + return suggestions; + } + + public void setSuggestions(List suggestions) { + this.suggestions = suggestions; + } + } + + // 创建完整类型化的Agent + ReactAgent writerAgent = ReactAgent.builder() + .name("full_typed_writer") + .model(chatModel) + .description("完整类型化的写作工具") + .instruction("根据结构化输入(topic、wordCount、style)创作文章,并返回结构化输出(title、content、characterCount)。") + .inputType(ArticleRequest.class) + .outputType(ArticleOutput.class) + .build(); + + ReactAgent reviewerAgent = ReactAgent.builder() + .name("typed_reviewer") + .model(chatModel) + .description("完整类型化的评审工具") + .instruction("对文章进行评审,返回评审意见(comment、approved、suggestions)。") + .outputType(ReviewOutput.class) + .build(); + + ReactAgent orchestratorAgent = ReactAgent.builder() + .name("orchestrator") + .model(chatModel) + .instruction("协调写作和评审流程。先调用写作工具创作文章,然后调用评审工具进行评审。") + .tools( + AgentTool.getFunctionToolCallback(writerAgent), + AgentTool.getFunctionToolCallback(reviewerAgent) + ) + .build(); + + Optional result = orchestratorAgent.invoke("请写一篇关于友谊的散文,约200字,需要评审"); + + if (result.isPresent()) { + System.out.println("完整类型化示例执行成功"); + } + } + + /** + * 示例7:多个子Agent作为工具 + * + * 主Agent可以访问多个不同的子Agent工具,根据需要调用 + */ + public void example7_multipleAgentTools() throws GraphRunnerException { + // 创建写作Agent + ReactAgent writerAgent = ReactAgent.builder() + .name("writer_agent") + .model(chatModel) + .description("专门负责创作文章和内容生成") + .instruction("你是一个专业作家,擅长各类文章创作。") + .build(); + + // 创建翻译Agent + ReactAgent translatorAgent = ReactAgent.builder() + .name("translator_agent") + .model(chatModel) + .description("专门负责文本翻译工作") + .instruction("你是一个专业翻译,能够准确翻译多种语言。") + .build(); + + // 创建总结Agent + ReactAgent summarizerAgent = ReactAgent.builder() + .name("summarizer_agent") + .model(chatModel) + .description("专门负责内容总结和提炼") + .instruction("你是一个内容总结专家,擅长提炼关键信息。") + .build(); + + // 创建主Agent,集成多个工具 + ReactAgent multiToolAgent = ReactAgent.builder() + .name("multi_tool_coordinator") + .model(chatModel) + .instruction("你可以访问多个专业工具:写作、翻译和总结。" + + "根据用户需求选择合适的工具来完成任务。") + .tools( + AgentTool.getFunctionToolCallback(writerAgent), + AgentTool.getFunctionToolCallback(translatorAgent), + AgentTool.getFunctionToolCallback(summarizerAgent) + ) + .build(); + + // 测试不同的请求 + multiToolAgent.invoke("请写一篇关于AI的文章,然后翻译成英文,最后给出摘要"); + + System.out.println("多工具Agent示例执行成功"); + } + + /** + * 文章输出类 - 用于示例4和示例5 + */ + public static class ArticleOutput { + private String title; + private String content; + private int characterCount; + + // Getters and Setters + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public int getCharacterCount() { + return characterCount; + } + + public void setCharacterCount(int characterCount) { + this.characterCount = characterCount; + } + } + + /** + * 运行所有示例 + */ + public void runAllExamples() { + System.out.println("=== 智能体作为工具(Agent Tool)示例 ===\n"); + + try { + System.out.println("示例1: 基础 Agent Tool 调用"); + example1_basicAgentTool(); + System.out.println(); + + System.out.println("示例2: 使用 inputSchema 控制输入"); + example2_agentToolWithInputSchema(); + System.out.println(); + + System.out.println("示例3: 使用 inputType 定义类型化输入"); + example3_agentToolWithInputType(); + System.out.println(); + + System.out.println("示例4: 使用 outputSchema 控制输出"); + example4_agentToolWithOutputSchema(); + System.out.println(); + + System.out.println("示例5: 使用 outputType 定义类型化输出"); + example5_agentToolWithOutputType(); + System.out.println(); + + System.out.println("示例6: 完整类型化示例"); + example6_fullTypedAgentTool(); + System.out.println(); + + System.out.println("示例7: 多个子Agent作为工具"); + example7_multipleAgentTools(); + System.out.println(); + + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/ContextEngineeringExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/ContextEngineeringExample.java new file mode 100644 index 00000000..80d5e4ce --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/ContextEngineeringExample.java @@ -0,0 +1,562 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.hook.HookPosition; +import com.alibaba.cloud.ai.graph.agent.hook.HookPositions; +import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand; +import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook; +import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy; +import com.alibaba.cloud.ai.graph.agent.interceptor.ModelCallHandler; +import com.alibaba.cloud.ai.graph.agent.interceptor.ModelInterceptor; +import com.alibaba.cloud.ai.graph.agent.interceptor.ModelRequest; +import com.alibaba.cloud.ai.graph.agent.interceptor.ModelResponse; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.ToolCallback; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 上下文工程(Context Engineering)示例 + * + * 演示如何通过上下文工程提高Agent的可靠性,包括: + * 1. 模型上下文:系统提示、消息历史、工具、模型选择、响应格式 + * 2. 工具上下文:工具访问和修改状态 + * 3. 生命周期上下文:Hook机制 + * + * 参考文档: advanced_doc/context-engineering.md + */ +public class ContextEngineeringExample { + + private final ChatModel chatModel; + + public ContextEngineeringExample(ChatModel chatModel) { + this.chatModel = chatModel; + } + + /** + * Main方法:运行所有示例 + * + * 注意:需要配置ChatModel实例才能运行 + */ + public static void main(String[] args) { + // 创建 DashScope API 实例 + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 创建 ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + if (chatModel == null) { + System.err.println("错误:请先配置ChatModel实例"); + System.err.println("请设置 AI_DASHSCOPE_API_KEY 环境变量"); + return; + } + + // 创建示例实例 + ContextEngineeringExample example = new ContextEngineeringExample(chatModel); + + // 运行所有示例 + example.runAllExamples(); + } + + /** + * 示例1:基于状态的动态提示 + * + * 根据对话长度调整系统提示 + */ + public void example1_stateAwarePrompt() throws GraphRunnerException { + // 创建一个模型拦截器,根据对话长度调整系统提示 + class StateAwarePromptInterceptor extends ModelInterceptor { + @Override + public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) { + List messages = request.getMessages(); + int messageCount = messages.size(); + + // 基础提示 + String basePrompt = "你是一个有用的助手。"; + + // 根据消息数量调整提示 + if (messageCount > 10) { + basePrompt += "\n这是一个长对话 - 请尽量保持精准简捷。"; + } + + // 更新系统消息(参考 TodoListInterceptor 的实现方式) + SystemMessage enhancedSystemMessage; + if (request.getSystemMessage() == null) { + enhancedSystemMessage = new SystemMessage(basePrompt); + } + else { + enhancedSystemMessage = new SystemMessage( + request.getSystemMessage().getText() + "\n\n" + basePrompt + ); + } + + // 创建增强的请求 + ModelRequest enhancedRequest = ModelRequest.builder(request) + .systemMessage(enhancedSystemMessage) + .build(); + + // 调用处理器 + return handler.call(enhancedRequest); + } + + @Override + public String getName() { + return "StateAwarePromptInterceptor"; + } + } + + // 使用拦截器创建Agent + ReactAgent agent = ReactAgent.builder() + .name("context_aware_agent") + .model(chatModel) + .interceptors(new StateAwarePromptInterceptor()) + .build(); + + // 测试 + agent.invoke("你好"); + System.out.println("基于状态的动态提示示例执行完成"); + } + + /** + * 示例2:基于存储的个性化提示 + * + * 从长期记忆加载用户偏好并生成个性化提示 + */ + public void example2_personalizedPrompt() throws GraphRunnerException { + // 用户偏好类 + class UserPreferences { + private String communicationStyle; + private String language; + private List interests; + + public UserPreferences(String style, String lang, List interests) { + this.communicationStyle = style; + this.language = lang; + this.interests = interests; + } + + public String getCommunicationStyle() { + return communicationStyle; + } + + public String getLanguage() { + return language; + } + + public List getInterests() { + return interests; + } + } + + // 简单的用户偏好存储 + class UserPreferenceStore { + private Map store = new HashMap<>(); + + public UserPreferences getPreferences(String userId) { + return store.getOrDefault(userId, + new UserPreferences("专业", "中文", List.of())); + } + + public void savePreferences(String userId, UserPreferences prefs) { + store.put(userId, prefs); + } + } + + UserPreferenceStore store = new UserPreferenceStore(); + store.savePreferences("user_001", + new UserPreferences("友好轻松", "中文", List.of("技术", "阅读"))); + + // 从长期记忆加载用户偏好 + class PersonalizedPromptInterceptor extends ModelInterceptor { + private final UserPreferenceStore store; + + public PersonalizedPromptInterceptor(UserPreferenceStore store) { + this.store = store; + } + + @Override + public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) { + // 从运行时上下文获取用户ID + String userId = getUserIdFromContext(request); + + // 从存储加载用户偏好 + UserPreferences prefs = store.getPreferences(userId); + + // 构建个性化提示 + String personalizedPrompt = buildPersonalizedPrompt(prefs); + + // 更新系统消息(参考 TodoListInterceptor 的实现方式) + SystemMessage enhancedSystemMessage; + if (request.getSystemMessage() == null) { + enhancedSystemMessage = new SystemMessage(personalizedPrompt); + } + else { + enhancedSystemMessage = new SystemMessage( + request.getSystemMessage().getText() + "\n\n" + personalizedPrompt + ); + } + + // 创建增强的请求 + ModelRequest enhancedRequest = ModelRequest.builder(request) + .systemMessage(enhancedSystemMessage) + .build(); + + // 调用处理器 + return handler.call(enhancedRequest); + } + + private String getUserIdFromContext(ModelRequest request) { + // 从请求上下文提取用户ID + return "user_001"; // 简化示例 + } + + private String buildPersonalizedPrompt(UserPreferences prefs) { + StringBuilder prompt = new StringBuilder("你是一个有用的助手。"); + + if (prefs.getCommunicationStyle() != null) { + prompt.append("\n沟通风格:").append(prefs.getCommunicationStyle()); + } + + if (prefs.getLanguage() != null) { + prompt.append("\n使用语言:").append(prefs.getLanguage()); + } + + if (!prefs.getInterests().isEmpty()) { + prompt.append("\n用户兴趣:").append(String.join(", ", prefs.getInterests())); + } + + return prompt.toString(); + } + + @Override + public String getName() { + return "PersonalizedPromptInterceptor"; + } + } + + ReactAgent agent = ReactAgent.builder() + .name("personalized_agent") + .model(chatModel) + .interceptors(new PersonalizedPromptInterceptor(store)) + .build(); + + agent.invoke("介绍一下最新的AI技术"); + System.out.println("个性化提示示例执行完成"); + } + + /** + * 示例3:消息过滤 + * + * 只保留最近的N条消息,避免上下文过长 + */ + public void example3_messageFilter() { + class MessageFilterInterceptor extends ModelInterceptor { + private final int maxMessages; + + public MessageFilterInterceptor(int maxMessages) { + this.maxMessages = maxMessages; + } + + @Override + public ModelResponse interceptModel(ModelRequest request, ModelCallHandler next) { + List messages = request.getMessages(); + + // 只保留最近的N条消息 + if (messages.size() > maxMessages) { + List filtered = new ArrayList<>(); + + // 添加系统消息 + messages.stream() + .filter(m -> m instanceof SystemMessage) + .findFirst() + .ifPresent(filtered::add); + + // 添加最近的消息 + int startIndex = Math.max(0, messages.size() - maxMessages + 1); + filtered.addAll(messages.subList(startIndex, messages.size())); + + messages = filtered; + } + + ModelRequest updatedRequest = ModelRequest.builder(request) + .messages(messages) + .build(); + + return next.call(updatedRequest); + } + + @Override + public String getName() { + return "MessageFilterInterceptor"; + } + } + + ReactAgent agent = ReactAgent.builder() + .name("message_filter_agent") + .model(chatModel) + .interceptors(new MessageFilterInterceptor(10)) + .build(); + + System.out.println("消息过滤示例执行完成"); + } + + /** + * 示例4:基于上下文的工具选择 + * + * 根据用户角色动态选择可用工具 + */ + public void example4_contextualToolSelection() { + class ContextualToolInterceptor extends ModelInterceptor { + private final Map> roleBasedTools; + + public ContextualToolInterceptor(Map> roleBasedTools) { + this.roleBasedTools = roleBasedTools; + } + + @Override + public ModelResponse interceptModel(ModelRequest request, ModelCallHandler next) { + // 从上下文获取用户角色 + String userRole = getUserRole(request); + + // 根据角色选择工具 + List allowedTools = roleBasedTools.getOrDefault( + userRole, + Collections.emptyList() + ); + + // 更新工具选项(注:实际实现需要根据框架API调整) + // 这里展示概念性代码 + System.out.println("为角色 " + userRole + " 选择了 " + allowedTools.size() + " 个工具"); + + return next.call(request); + } + + private String getUserRole(ModelRequest request) { + // 从请求上下文提取用户角色 + return "user"; // 简化示例 + } + + @Override + public String getName() { + return "ContextualToolInterceptor"; + } + } + + // 配置基于角色的工具(示例) + Map> roleTools = Map.of( + "admin", List.of(/* readTool, writeTool, deleteTool */), + "user", List.of(/* readTool */), + "guest", List.of() + ); + + ReactAgent agent = ReactAgent.builder() + .name("role_based_agent") + .model(chatModel) + .interceptors(new ContextualToolInterceptor(roleTools)) + .build(); + + System.out.println("基于上下文的工具选择示例执行完成"); + } + + /** + * 示例5:日志记录 Hook + * + * 使用MessagesModelHook在模型调用前后记录日志 + */ + public void example5_loggingHook() throws GraphRunnerException { + @HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL}) + class LoggingHook extends MessagesModelHook { + @Override + public String getName() { + return "logging_hook"; + } + + @Override + public AgentCommand beforeModel(List previousMessages, RunnableConfig config) { + // 在模型调用前记录 + System.out.println("模型调用前 - 消息数: " + previousMessages.size()); + // 不修改消息,返回原始消息 + return new AgentCommand(previousMessages); + } + + @Override + public AgentCommand afterModel(List previousMessages, RunnableConfig config) { + // 在模型调用后记录 + System.out.println("模型调用后 - 响应已生成"); + // 不修改消息,返回原始消息 + return new AgentCommand(previousMessages); + } + } + + // 使用Hook + ReactAgent agent = ReactAgent.builder() + .name("logged_agent") + .model(chatModel) + .hooks(new LoggingHook()) + .build(); + + agent.invoke("测试日志记录"); + System.out.println("日志记录Hook示例执行完成"); + } + + /** + * 示例6:消息摘要 Hook + * + * 当对话过长时自动生成摘要 + * 使用MessagesModelHook实现 + */ + public void example6_summarizationHook() { + @HookPositions({HookPosition.BEFORE_MODEL}) + class SummarizationHook extends MessagesModelHook { + private final ChatModel summarizationModel; + private final int triggerLength; + + public SummarizationHook(ChatModel model, int triggerLength) { + this.summarizationModel = model; + this.triggerLength = triggerLength; + } + + @Override + public String getName() { + return "summarization_hook"; + } + + @Override + public AgentCommand beforeModel(List previousMessages, RunnableConfig config) { + if (previousMessages.size() <= triggerLength) { + // 如果消息数量未超过阈值,无需总结 + return new AgentCommand(previousMessages); + } + + // 生成对话摘要 + String summary = generateSummary(previousMessages); + + // 查找是否已存在 SystemMessage + SystemMessage existingSystemMessage = null; + for (Message msg : previousMessages) { + if (msg instanceof SystemMessage) { + existingSystemMessage = (SystemMessage) msg; + break; + } + } + + // 创建摘要 SystemMessage + String summaryText = "之前对话摘要:" + summary; + SystemMessage summarySystemMessage; + if (existingSystemMessage != null) { + // 如果存在 SystemMessage,追加摘要信息 + summarySystemMessage = new SystemMessage( + existingSystemMessage.getText() + "\n\n" + summaryText + ); + } + else { + // 如果不存在,创建新的 + summarySystemMessage = new SystemMessage(summaryText); + } + + // 保留最近的几条消息 + int recentCount = Math.min(5, previousMessages.size()); + List recentMessages = previousMessages.subList( + previousMessages.size() - recentCount, + previousMessages.size() + ); + + // 构建新的消息列表 + List newMessages = new ArrayList<>(); + newMessages.add(summarySystemMessage); + // 添加最近的消息,排除旧的 SystemMessage(如果存在) + for (Message msg : recentMessages) { + if (msg != existingSystemMessage) { + newMessages.add(msg); + } + } + + // 使用 REPLACE 策略替换所有消息 + return new AgentCommand(newMessages, UpdatePolicy.REPLACE); + } + + private String generateSummary(List messages) { + // 使用另一个模型生成摘要 + String conversation = messages.stream() + .map(Message::getText) + .collect(Collectors.joining("\n")); + + // 简化示例:返回固定摘要 + return "之前讨论了多个主题..."; + } + } + + ReactAgent agent = ReactAgent.builder() + .name("summarizing_agent") + .model(chatModel) + .hooks(new SummarizationHook(chatModel, 20)) + .build(); + + System.out.println("消息摘要Hook示例执行完成"); + } + + /** + * 运行所有示例 + */ + public void runAllExamples() { + System.out.println("=== 上下文工程(Context Engineering)示例 ===\n"); + + try { + // System.out.println("示例1: 基于状态的动态提示"); + // example1_stateAwarePrompt(); + // System.out.println(); + // + // System.out.println("示例2: 基于存储的个性化提示"); + // example2_personalizedPrompt(); + // System.out.println(); + // + // System.out.println("示例3: 消息过滤"); + // example3_messageFilter(); + // System.out.println(); + // + // System.out.println("示例4: 基于上下文的工具选择"); + // example4_contextualToolSelection(); + // System.out.println(); + // + // System.out.println("示例5: 日志记录Hook"); + example5_loggingHook(); + // System.out.println(); + // + // System.out.println("示例6: 消息摘要Hook"); + // example6_summarizationHook(); + // System.out.println(); + + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/HumanInTheLoopExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/HumanInTheLoopExample.java new file mode 100644 index 00000000..9efd0862 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/HumanInTheLoopExample.java @@ -0,0 +1,739 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.*; +import com.alibaba.cloud.ai.graph.action.InterruptionMetadata; +import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook; +import com.alibaba.cloud.ai.graph.agent.hook.hip.ToolConfig; +import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 人工介入(Human-in-the-Loop)示例 + * + * 演示如何使用人工介入Hook为Agent工具调用添加人工监督,包括: + * 1. 配置中断和审批 + * 2. 批准(approve)决策 + * 3. 编辑(edit)决策 + * 4. 拒绝(reject)决策 + * 5. 处理多个工具调用 + * 6. Workflow中嵌套ReactAgent的人工中断 + * 7. 实用工具方法 + * + * 参考文档: advanced_doc/human-in-the-loop.md + */ +public class HumanInTheLoopExample { + + private final ChatModel chatModel; + + public HumanInTheLoopExample(ChatModel chatModel) { + this.chatModel = chatModel; + } + + /** + * 实用工具方法:批准所有工具调用 + */ + public static InterruptionMetadata approveAll(InterruptionMetadata interruptionMetadata) { + InterruptionMetadata.Builder builder = InterruptionMetadata.builder() + .nodeId(interruptionMetadata.node()) + .state(interruptionMetadata.state()); + + interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { + builder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) + .build() + ); + }); + + return builder.build(); + } + + /** + * 实用工具方法:拒绝所有工具调用 + */ + public static InterruptionMetadata rejectAll(InterruptionMetadata interruptionMetadata, String reason) { + InterruptionMetadata.Builder builder = InterruptionMetadata.builder() + .nodeId(interruptionMetadata.node()) + .state(interruptionMetadata.state()); + + interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { + builder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.REJECTED) + .description(reason) + .build() + ); + }); + + return builder.build(); + } + + /** + * 实用工具方法:编辑特定工具的参数 + */ + public static InterruptionMetadata editTool( + InterruptionMetadata interruptionMetadata, + String toolName, + String newArguments) { + InterruptionMetadata.Builder builder = InterruptionMetadata.builder() + .nodeId(interruptionMetadata.node()) + .state(interruptionMetadata.state()); + + interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { + if (toolFeedback.getName().equals(toolName)) { + builder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .arguments(newArguments) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.EDITED) + .build() + ); + } + else { + builder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) + .build() + ); + } + }); + + return builder.build(); + } + + /** + * Main方法:运行所有示例 + * + * 注意:需要配置ChatModel实例才能运行 + */ + public static void main(String[] args) { + // 创建 DashScope API 实例 + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 创建 ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + if (chatModel == null) { + System.err.println("错误:请先配置ChatModel实例"); + System.err.println("请设置 AI_DASHSCOPE_API_KEY 环境变量"); + return; + } + + // 创建示例实例 + HumanInTheLoopExample example = new HumanInTheLoopExample(chatModel); + + // 运行所有示例 + example.runAllExamples(); + } + + /** + * 示例1:配置中断和基本使用 + * + * 为特定工具配置人工审批 + */ + public void example1_basicConfiguration() throws Exception { + // 配置检查点保存器(人工介入需要检查点来处理中断) + MemorySaver memorySaver = new MemorySaver(); + + // 创建工具回调(示例) + ToolCallback writeFileTool = FunctionToolCallback.builder("write_file", (args) -> "文件已写入") + .description("写入文件") + .inputType(String.class) + .build(); + + ToolCallback executeSqlTool = FunctionToolCallback.builder("execute_sql", (args) -> "SQL已执行") + .description("执行SQL语句") + .inputType(String.class) + .build(); + + ToolCallback readDataTool = FunctionToolCallback.builder("read_data", (args) -> "数据已读取") + .description("读取数据") + .inputType(String.class) + .build(); + + // 创建人工介入Hook + HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder() + .approvalOn("write_file", ToolConfig.builder() + .description("文件写入操作需要审批") + .build()) + .approvalOn("execute_sql", ToolConfig.builder() + .description("SQL执行操作需要审批") + .build()) + .build(); + + // 创建Agent + ReactAgent agent = ReactAgent.builder() + .name("approval_agent") + .model(chatModel) + .tools(writeFileTool, executeSqlTool, readDataTool) + .hooks(List.of(humanInTheLoopHook)) + .saver(memorySaver) + .build(); + + AssistantMessage message = agent.call("执行SQL语句:select * from user limit 1"); + System.out.println(message.getText()); + + System.out.println("人工介入Hook配置示例完成"); + } + + /** + * 示例2:批准(approve)决策 + * + * 人工批准工具调用并继续执行 + */ + public void example2_approveDecision() throws Exception { + MemorySaver memorySaver = new MemorySaver(); + + ToolCallback poetTool = FunctionToolCallback.builder("poem", (args) -> "春江潮水连海平,海上明月共潮生...") + .description("写诗工具") + .inputType(String.class) + .build(); + + HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder() + .approvalOn("poem", ToolConfig.builder() + .description("请确认诗歌创作操作") + .build()) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("poet_agent") + .model(chatModel) + .tools(List.of(poetTool)) + .hooks(List.of(humanInTheLoopHook)) + .saver(memorySaver) + .build(); + + String threadId = "user-session-001"; + RunnableConfig config = RunnableConfig.builder() + .threadId(threadId) + .build(); + + // 第一次调用 - 触发中断 + System.out.println("=== 第一次调用:期望中断 ==="); + Optional result = agent.invokeAndGetOutput( + "帮我写一首100字左右的诗", + config + ); + + // 检查中断并处理 + if (result.isPresent() && result.get() instanceof InterruptionMetadata) { + InterruptionMetadata interruptionMetadata = (InterruptionMetadata) result.get(); + + System.out.println("检测到中断,需要人工审批"); + List toolFeedbacks = + interruptionMetadata.toolFeedbacks(); + + for (InterruptionMetadata.ToolFeedback feedback : toolFeedbacks) { + System.out.println("工具: " + feedback.getName()); + System.out.println("参数: " + feedback.getArguments()); + System.out.println("描述: " + feedback.getDescription()); + } + + // 构建批准反馈 + InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder() + .nodeId(interruptionMetadata.node()) + .state(interruptionMetadata.state()); + + // 对每个工具调用设置批准决策 + interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { + InterruptionMetadata.ToolFeedback approvedFeedback = + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) + .build(); + feedbackBuilder.addToolFeedback(approvedFeedback); + }); + + InterruptionMetadata approvalMetadata = feedbackBuilder.build(); + + // 使用批准决策恢复执行 + RunnableConfig resumeConfig = RunnableConfig.builder() + .threadId(threadId) // 相同的线程ID以恢复暂停的对话 + .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, approvalMetadata) + .build(); + + // 第二次调用以恢复执行 + System.out.println("\n=== 第二次调用:使用批准决策恢复 ==="); + Optional finalResult = agent.invokeAndGetOutput("", resumeConfig); + + if (finalResult.isPresent()) { + System.out.println("执行完成"); + } + } + + System.out.println("批准决策示例执行完成"); + } + + /** + * 示例3:编辑(edit)决策 + * + * 人工编辑工具参数后继续执行 + */ + public void example3_editDecision() throws Exception { + MemorySaver memorySaver = new MemorySaver(); + + ToolCallback executeSqlTool = FunctionToolCallback.builder("execute_sql", (args) -> "SQL执行结果") + .description("执行SQL语句") + .inputType(String.class) + .build(); + + HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder() + .approvalOn("execute_sql", ToolConfig.builder() + .description("SQL执行操作需要审批") + .build()) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("sql_agent") + .model(chatModel) + .tools(executeSqlTool) + .hooks(List.of(humanInTheLoopHook)) + .saver(memorySaver) + .build(); + + String threadId = "sql-session-001"; + RunnableConfig config = RunnableConfig.builder() + .threadId(threadId) + .build(); + + // 第一次调用 - 触发中断 + Optional result = agent.invokeAndGetOutput( + "删除数据库中的旧记录", + config + ); + + if (result.isPresent() && result.get() instanceof InterruptionMetadata) { + InterruptionMetadata interruptionMetadata = (InterruptionMetadata) result.get(); + + // 构建编辑反馈 + InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder() + .nodeId(interruptionMetadata.node()) + .state(interruptionMetadata.state()); + + interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { + // 修改工具参数 + String editedArguments = toolFeedback.getArguments() + .replace("DELETE FROM records", "DELETE FROM old_records"); + + InterruptionMetadata.ToolFeedback editedFeedback = + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .arguments(editedArguments) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.EDITED) + .build(); + feedbackBuilder.addToolFeedback(editedFeedback); + }); + + InterruptionMetadata editMetadata = feedbackBuilder.build(); + + // 使用编辑决策恢复执行 + RunnableConfig resumeConfig = RunnableConfig.builder() + .threadId(threadId) + .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, editMetadata) + .build(); + + Optional finalResult = agent.invokeAndGetOutput("", resumeConfig); + + System.out.println("编辑决策示例执行完成"); + } + } + + /** + * 示例4:拒绝(reject)决策 + * + * 人工拒绝工具调用并终止当前流程 + */ + public void example4_rejectDecision() throws Exception { + MemorySaver memorySaver = new MemorySaver(); + + ToolCallback deleteTool = FunctionToolCallback.builder("delete_data", (args) -> "数据已删除") + .description("删除数据") + .inputType(String.class) + .build(); + + HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder() + .approvalOn("delete_data", ToolConfig.builder() + .description("删除操作需要审批") + .build()) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("delete_agent") + .model(chatModel) + .tools(deleteTool) + .hooks(List.of(humanInTheLoopHook)) + .saver(memorySaver) + .build(); + + String threadId = "delete-session-001"; + RunnableConfig config = RunnableConfig.builder() + .threadId(threadId) + .build(); + + // 第一次调用 - 触发中断 + Optional result = agent.invokeAndGetOutput( + "删除所有用户数据", + config + ); + + if (result.isPresent() && result.get() instanceof InterruptionMetadata) { + InterruptionMetadata interruptionMetadata = (InterruptionMetadata) result.get(); + + // 构建拒绝反馈 + InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder() + .nodeId(interruptionMetadata.node()) + .state(interruptionMetadata.state()); + + interruptionMetadata.toolFeedbacks().forEach(toolFeedback -> { + InterruptionMetadata.ToolFeedback rejectedFeedback = + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.REJECTED) + .description("不允许删除操作,请使用归档功能代替。") + .build(); + feedbackBuilder.addToolFeedback(rejectedFeedback); + }); + + InterruptionMetadata rejectMetadata = feedbackBuilder.build(); + + // 使用拒绝决策恢复执行 + RunnableConfig resumeConfig = RunnableConfig.builder() + .threadId(threadId) + .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, rejectMetadata) + .build(); + + Optional finalResult = agent.invokeAndGetOutput("", resumeConfig); + + System.out.println("拒绝决策示例执行完成"); + } + } + + /** + * 示例5:处理多个工具调用 + * + * 一次性处理多个需要审批的工具调用 + */ + public void example5_multipleTools() throws Exception { + MemorySaver memorySaver = new MemorySaver(); + + ToolCallback tool1 = FunctionToolCallback.builder("tool1", (args) -> "工具1结果") + .description("工具1") + .inputType(String.class) + .build(); + + ToolCallback tool2 = FunctionToolCallback.builder("tool2", (args) -> "工具2结果") + .description("工具2") + .inputType(String.class) + .build(); + + ToolCallback tool3 = FunctionToolCallback.builder("tool3", (args) -> "工具3结果") + .description("工具3") + .inputType(String.class) + .build(); + + HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder() + .approvalOn("tool1", ToolConfig.builder().description("工具1需要审批").build()) + .approvalOn("tool2", ToolConfig.builder().description("工具2需要审批").build()) + .approvalOn("tool3", ToolConfig.builder().description("工具3需要审批").build()) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("multi_tool_agent") + .model(chatModel) + .tools(tool1, tool2, tool3) + .hooks(List.of(humanInTheLoopHook)) + .saver(memorySaver) + .build(); + + String threadId = "multi-session-001"; + RunnableConfig config = RunnableConfig.builder() + .threadId(threadId) + .build(); + + Optional result = agent.invokeAndGetOutput("执行所有工具", config); + + if (result.isPresent() && result.get() instanceof InterruptionMetadata) { + InterruptionMetadata interruptionMetadata = (InterruptionMetadata) result.get(); + + InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder() + .nodeId(interruptionMetadata.node()) + .state(interruptionMetadata.state()); + + List feedbacks = interruptionMetadata.toolFeedbacks(); + + // 第一个工具:批准 + if (feedbacks.size() > 0) { + feedbackBuilder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(feedbacks.get(0)) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) + .build() + ); + } + + // 第二个工具:编辑 + if (feedbacks.size() > 1) { + feedbackBuilder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(feedbacks.get(1)) + .arguments("{\"param\": \"new_value\"}") + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.EDITED) + .build() + ); + } + + // 第三个工具:拒绝 + if (feedbacks.size() > 2) { + feedbackBuilder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(feedbacks.get(2)) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.REJECTED) + .description("不允许此操作") + .build() + ); + } + + InterruptionMetadata decisionsMetadata = feedbackBuilder.build(); + + RunnableConfig resumeConfig = RunnableConfig.builder() + .threadId(threadId) + .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, decisionsMetadata) + .build(); + + Optional outputOptional = agent.invokeAndGetOutput("", resumeConfig); + + System.out.println("多个决策示例执行完成,最终状态:\n\n" + outputOptional.get().state()); + } + } + + /** + * 示例6:Workflow中嵌套ReactAgent的人工中断 + * + * 演示如何在StateGraph工作流中嵌套带有HumanInTheLoopHook的ReactAgent, + * 并处理工作流执行过程中的中断和恢复 + */ + public void example6_workflowWithHumanInTheLoop() throws Exception { + // 创建工具回调 + ToolCallback searchTool = FunctionToolCallback + .builder("search", (args) -> "搜索结果:AI Agent是能够感知环境、自主决策并采取行动的智能系统。") + .description("搜索工具,用于查找相关信息") + .inputType(String.class) + .build(); + + // 配置检查点保存器(人工介入需要检查点来处理中断) + MemorySaver saver = new MemorySaver(); + + // 创建带有人工介入Hook的ReactAgent + ReactAgent qaAgent = ReactAgent.builder() + .name("qa_agent") + .model(chatModel) + .instruction("你是一个问答专家,负责回答用户的问题。如果需要搜索信息,请使用search工具。\n用户问题:{cleaned_input}") + .outputKey("qa_result") + .saver(saver) + .hooks(HumanInTheLoopHook.builder() + .approvalOn("search", ToolConfig.builder() + .description("搜索操作需要人工审批,请确认是否执行搜索") + .build()) + .build()) + .tools(searchTool) + .enableLogging(true) + .build(); + + // 创建预处理Node:清理输入 + class PreprocessorNode implements NodeAction { + @Override + public Map apply(OverAllState state) throws Exception { + String input = state.value("input", "").toString(); + String cleaned = input.trim(); + System.out.println("预处理节点:清理输入 -> " + cleaned); + return Map.of("cleaned_input", cleaned); + } + } + + // 创建验证Node:验证结果质量 + class ValidatorNode implements NodeAction { + @Override + public Map apply(OverAllState state) throws Exception { + Optional qaResultOpt = state.value("qa_result"); + if (qaResultOpt.isPresent() && qaResultOpt.get() instanceof Message message) { + boolean isValid = message.getText().length() > 30; // 简单验证:答案长度需大于30 + System.out.println("验证节点:结果验证 -> " + (isValid ? "通过" : "不通过")); + return Map.of("is_valid", isValid); + } + return Map.of("is_valid", false); + } + } + + // 定义状态管理策略 + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("input", new ReplaceStrategy()); + strategies.put("cleaned_input", new ReplaceStrategy()); + strategies.put("qa_result", new ReplaceStrategy()); + strategies.put("is_valid", new ReplaceStrategy()); + return strategies; + }; + + // 构建工作流 + StateGraph workflow = new StateGraph(keyStrategyFactory); + + // 添加普通Node + workflow.addNode("preprocess", node_async(new PreprocessorNode())); + workflow.addNode("validate", node_async(new ValidatorNode())); + + // 添加Agent Node(嵌套的ReactAgent) + workflow.addNode(qaAgent.name(), qaAgent.asNode( + true, // includeContents: 传递父图的消息历史 + false // includeReasoning: 不返回推理过程 + )); + + // 定义流程:预处理 -> Agent处理 -> 验证 + workflow.addEdge(StateGraph.START, "preprocess"); + workflow.addEdge("preprocess", qaAgent.name()); + workflow.addEdge(qaAgent.name(), "validate"); + + // 条件边:验证通过则结束,否则重新处理 + workflow.addConditionalEdges( + "validate", + edge_async(state -> { + Boolean isValid = (Boolean) state.value("is_valid", false); + return isValid ? "end" : qaAgent.name(); + }), + Map.of( + "end", StateGraph.END, + qaAgent.name(), qaAgent.name() + ) + ); + + // 编译工作流 + CompiledGraph compiledGraph = workflow.compile( + CompileConfig.builder() + .saverConfig(SaverConfig.builder().register(saver).build()) + .build() + ); + + String threadId = "workflow-hilt-001"; + Map input = Map.of("input", "请解释量子计算的基本原理"); + + // 第一次调用 - 可能触发中断 + System.out.println("=== 第一次调用工作流:可能触发中断 ==="); + Optional nodeOutputOptional = compiledGraph.invokeAndGetOutput( + input, + RunnableConfig.builder().threadId(threadId).build() + ); + + // 检查是否发生中断 + if (nodeOutputOptional.isPresent() && nodeOutputOptional.get() instanceof InterruptionMetadata interruptionMetadata) { + System.out.println("\n工作流被中断,等待人工审核。"); + System.out.println("中断节点: " + interruptionMetadata.node()); + System.out.println("中断状态: " + interruptionMetadata.state()); + + List feedbacks = interruptionMetadata.toolFeedbacks(); + System.out.println("需要审批的工具调用数量: " + feedbacks.size()); + + // 显示所有需要审批的工具调用 + for (InterruptionMetadata.ToolFeedback feedback : feedbacks) { + System.out.println("\n工具名称: " + feedback.getName()); + System.out.println("工具参数: " + feedback.getArguments()); + System.out.println("工具描述: " + feedback.getDescription()); + } + + // 构建人工反馈(批准所有工具调用) + InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder() + .nodeId(interruptionMetadata.node()) + .state(interruptionMetadata.state()); + + // 对每个工具调用设置批准决策 + feedbacks.forEach(toolFeedback -> { + feedbackBuilder.addToolFeedback( + InterruptionMetadata.ToolFeedback.builder(toolFeedback) + .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED) + .build() + ); + }); + + InterruptionMetadata approvalMetadata = feedbackBuilder.build(); + + // 使用批准决策恢复执行 + System.out.println("\n=== 第二次调用:使用批准决策恢复工作流 ==="); + RunnableConfig resumableConfig = RunnableConfig.builder() + .threadId(threadId) + .addHumanFeedback(approvalMetadata) + .build(); + + nodeOutputOptional = compiledGraph.invokeAndGetOutput(Map.of(), resumableConfig); + System.out.println("\n工作流中嵌套ReactAgent的人工中断示例执行完成"); + + } + + } + + /** + * 运行所有示例 + */ + public void runAllExamples() { + System.out.println("=== 人工介入(Human-in-the-Loop)示例 ===\n"); + + try { +// System.out.println("示例1: 配置中断和基本使用"); +// example1_basicConfiguration(); +// System.out.println(); +// +// System.out.println("示例2: 批准(approve)决策"); +// example2_approveDecision(); +// System.out.println(); +// +// System.out.println("示例3: 编辑(edit)决策"); +// example3_editDecision(); +// System.out.println(); +// +// System.out.println("示例4: 拒绝(reject)决策"); +// example4_rejectDecision(); +// System.out.println(); +// +// System.out.println("示例5: 处理多个工具调用决策"); +// example5_multipleTools(); +// System.out.println(); + + // System.out.println("示例6: Workflow中嵌套ReactAgent的人工中断"); + example6_workflowWithHumanInTheLoop(); + // System.out.println(); + + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/MemoryExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/MemoryExample.java new file mode 100644 index 00000000..737cb8f0 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/MemoryExample.java @@ -0,0 +1,661 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.hook.HookPosition; +import com.alibaba.cloud.ai.graph.agent.hook.HookPositions; +import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand; +import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook; +import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; +import com.alibaba.cloud.ai.graph.store.Store; +import com.alibaba.cloud.ai.graph.store.StoreItem; +import com.alibaba.cloud.ai.graph.store.stores.MemoryStore; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; + +import java.util.*; +import java.util.function.BiFunction; + +/** + * 记忆管理(Memory)示例 + * + * 演示如何在Agent中使用记忆管理功能,包括: + * 1. 在工具中读取长期记忆 + * 2. 在工具中写入长期记忆 + * 3. 使用ModelHook管理长期记忆 + * 4. 结合短期和长期记忆 + * 5. 跨会话记忆 + * 6. 用户偏好学习 + * + * 参考文档: advanced_doc/memory.md + */ +public class MemoryExample { + + private final ChatModel chatModel; + + public MemoryExample(ChatModel chatModel) { + this.chatModel = chatModel; + } + + /** + * Main方法:运行所有示例 + * + * 注意:需要配置ChatModel实例才能运行 + */ + public static void main(String[] args) { + // 创建 DashScope API 实例 + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 创建 ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + if (chatModel == null) { + System.err.println("错误:请先配置ChatModel实例"); + System.err.println("请设置 AI_DASHSCOPE_API_KEY 环境变量"); + return; + } + + // 创建示例实例 + MemoryExample example = new MemoryExample(chatModel); + + // 运行所有示例 + example.runAllExamples(); + } + + private static void mockInsertToStore(MemoryStore store) { + // 向存储中写入示例数据 + Map userData = new HashMap<>(); + userData.put("name", "张三"); + userData.put("language", "中文"); + + StoreItem userItem = StoreItem.of(List.of("users"), "user_123", userData); + store.putItem(userItem); + } + + /** + * 示例1:在工具中读取长期记忆 + * + * 创建一个工具,让Agent能够查询用户信息 + */ + public void example1_readMemoryInTool() throws GraphRunnerException { + // 定义请求和响应记录 + record GetMemoryRequest(List namespace, String key) { } + record MemoryResponse(String message, Map value) { } + + // 创建获取用户信息的工具 + BiFunction getUserInfoFunction = + (request, context) -> { + RunnableConfig runnableConfig = (RunnableConfig) context.getContext().get("config"); + Store store = runnableConfig.store(); + Optional itemOpt = store.getItem(request.namespace(), request.key()); + if (itemOpt.isPresent()) { + Map value = itemOpt.get().getValue(); + return new MemoryResponse("找到用户信息", value); + } + return new MemoryResponse("未找到用户", Map.of()); + }; + + ToolCallback getUserInfoTool = FunctionToolCallback.builder("getUserInfo", getUserInfoFunction) + .description("查询用户信息") + .inputType(GetMemoryRequest.class) + .build(); + + // 创建Agent + ReactAgent agent = ReactAgent.builder() + .name("memory_agent") + .model(chatModel) + .tools(getUserInfoTool) + .saver(new MemorySaver()) + .build(); + + + // 创建内存存储 + MemoryStore store = new MemoryStore(); + // 在Store中放入模拟数据,实际应用中,存储可能是其他流程中生成 + mockInsertToStore(store); + // 运行Agent + RunnableConfig config = RunnableConfig.builder() + .threadId("session_001") + .addMetadata("user_id", "user_123") + .store(store) + .build(); + + agent.invoke("查询用户信息,namespace=['users'], key='user_123'", config); + + System.out.println("工具读取长期记忆示例执行完成"); + } + + /** + * 示例2:在工具中写入长期记忆 + * + * 创建一个更新用户信息的工具 + */ + public void example2_writeMemoryInTool() throws GraphRunnerException { + // 定义请求记录 + record SaveMemoryRequest(List namespace, String key, Map value) { } + record MemoryResponse(String message, Map value) { } + + // 创建保存用户信息的工具 + BiFunction saveUserInfoFunction = + (request, context) -> { + RunnableConfig runnableConfig = (RunnableConfig) context.getContext().get("config"); + Store store = runnableConfig.store(); + StoreItem item = StoreItem.of(request.namespace(), request.key(), request.value()); + store.putItem(item); + return new MemoryResponse("成功保存用户信息", request.value()); + }; + + ToolCallback saveUserInfoTool = FunctionToolCallback.builder("saveUserInfo", saveUserInfoFunction) + .description("保存用户信息") + .inputType(SaveMemoryRequest.class) + .build(); + + // 创建Agent + ReactAgent agent = ReactAgent.builder() + .name("save_memory_agent") + .model(chatModel) + .tools(saveUserInfoTool) + .saver(new MemorySaver()) + .build(); + + // 创建内存存储 + MemoryStore store = new MemoryStore(); + RunnableConfig config = RunnableConfig.builder() + .threadId("session_001") + .addMetadata("user_id", "user_123") + .store(store) + .build(); + // 运行Agent + agent.invoke( + "我叫张三,请保存我的信息。使用 saveUserInfo 工具,namespace=['users'], key='user_123', value={'name': '张三'}", + config + ); + + // 可以直接访问存储获取值 + Optional savedItem = store.getItem(List.of("users"), "user_123"); + if (savedItem.isPresent()) { + Map savedValue = savedItem.get().getValue(); + System.out.println("保存的数据: " + savedValue); + } + + System.out.println("工具写入长期记忆示例执行完成"); + } + + /** + * 示例3:使用MessagesModelHook管理长期记忆 + * + * 在模型调用前后自动加载和保存长期记忆 + */ + public void example3_memoryWithModelHook() throws GraphRunnerException { + // 创建记忆拦截器 + @HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL}) + class MemoryInterceptor extends MessagesModelHook { + @Override + public String getName() { + return "memory_interceptor"; + } + + @Override + public AgentCommand beforeModel(List previousMessages, RunnableConfig config) { + // 从配置中获取用户ID + String userId = (String) config.metadata("user_id").orElse(null); + if (userId == null) { + return new AgentCommand(previousMessages); + } + + Store store = config.store(); + // 从记忆存储中加载用户画像 + Optional itemOpt = store.getItem(List.of("user_profiles"), userId); + if (itemOpt.isPresent()) { + Map profile = itemOpt.get().getValue(); + + // 将用户上下文注入系统消息 + String userContext = String.format( + "用户信息:姓名=%s, 年龄=%s, 邮箱=%s, 偏好=%s", + profile.get("name"), + profile.get("age"), + profile.get("email"), + profile.get("preferences") + ); + + // 查找是否已存在 SystemMessage + SystemMessage existingSystemMessage = null; + int systemMessageIndex = -1; + for (int i = 0; i < previousMessages.size(); i++) { + Message msg = previousMessages.get(i); + if (msg instanceof SystemMessage) { + existingSystemMessage = (SystemMessage) msg; + systemMessageIndex = i; + break; + } + } + + // 如果找到 SystemMessage,更新它;否则创建新的 + SystemMessage enhancedSystemMessage; + if (existingSystemMessage != null) { + // 更新现有的 SystemMessage + enhancedSystemMessage = new SystemMessage( + existingSystemMessage.getText() + "\n\n" + userContext + ); + } + else { + // 创建新的 SystemMessage + enhancedSystemMessage = new SystemMessage(userContext); + } + + // 构建新的消息列表 + List newMessages = new ArrayList<>(); + if (systemMessageIndex >= 0) { + // 如果找到了 SystemMessage,替换它 + for (int i = 0; i < previousMessages.size(); i++) { + if (i == systemMessageIndex) { + newMessages.add(enhancedSystemMessage); + } + else { + newMessages.add(previousMessages.get(i)); + } + } + } + else { + // 如果没有找到 SystemMessage,在开头添加新的 + newMessages.add(enhancedSystemMessage); + newMessages.addAll(previousMessages); + } + + // 使用 REPLACE 策略替换所有消息 + return new AgentCommand(newMessages, UpdatePolicy.REPLACE); + } + + return new AgentCommand(previousMessages); + } + + @Override + public AgentCommand afterModel(List previousMessages, RunnableConfig config) { + // 可以在这里实现对话后的记忆保存逻辑 + // 不修改消息,返回原始消息 + return new AgentCommand(previousMessages); + } + } + + MessagesModelHook memoryInterceptor = new MemoryInterceptor(); + + // 创建带有记忆拦截器的Agent + ReactAgent agent = ReactAgent.builder() + .name("memory_agent") + .model(chatModel) + .hooks(memoryInterceptor) + .saver(new MemorySaver()) + .build(); + + + // 创建内存存储 + MemoryStore memoryStore = new MemoryStore(); + + // 模拟数据,预先填充用户画像 + Map profileData = new HashMap<>(); + profileData.put("name", "王小明"); + profileData.put("age", 28); + profileData.put("email", "wang@example.com"); + profileData.put("preferences", List.of("喜欢咖啡", "喜欢阅读")); + + StoreItem profileItem = StoreItem.of(List.of("user_profiles"), "user_001", profileData); + memoryStore.putItem(profileItem); + RunnableConfig config = RunnableConfig.builder() + .threadId("session_001") + .addMetadata("user_id", "user_001") + .store(memoryStore) + .build(); + + // Agent会自动加载用户画像信息 + agent.invoke("请介绍一下我的信息。", config); + + System.out.println("ModelHook管理长期记忆示例执行完成"); + } + + /** + * 示例4:结合短期和长期记忆 + * + * 短期记忆用于存储对话上下文,长期记忆用于存储持久化数据 + * 使用MessagesModelHook实现 + */ + public void example4_combinedMemory() throws GraphRunnerException { + // 创建组合记忆Hook + @HookPositions({HookPosition.BEFORE_MODEL}) + class CombinedMemoryHook extends MessagesModelHook { + @Override + public String getName() { + return "combined_memory"; + } + + @Override + public AgentCommand beforeModel(List previousMessages, RunnableConfig config) { + Optional userIdOpt = config.metadata("user_id"); + if (userIdOpt.isEmpty()) { + return new AgentCommand(previousMessages); + } + String userId = (String) userIdOpt.get(); + + Store memoryStore = config.store(); + // 从长期记忆加载 + Optional profileOpt = memoryStore.getItem(List.of("profiles"), userId); + if (profileOpt.isEmpty()) { + return new AgentCommand(previousMessages); + } + + Map profile = profileOpt.get().getValue(); + String contextInfo = String.format("长期记忆:用户 %s, 职业: %s", + profile.get("name"), profile.get("occupation")); + + // 查找是否已存在 SystemMessage + SystemMessage existingSystemMessage = null; + int systemMessageIndex = -1; + for (int i = 0; i < previousMessages.size(); i++) { + Message msg = previousMessages.get(i); + if (msg instanceof SystemMessage) { + existingSystemMessage = (SystemMessage) msg; + systemMessageIndex = i; + break; + } + } + + // 如果找到 SystemMessage,更新它;否则创建新的 + SystemMessage enhancedSystemMessage; + if (existingSystemMessage != null) { + // 更新现有的 SystemMessage + enhancedSystemMessage = new SystemMessage( + existingSystemMessage.getText() + "\n\n" + contextInfo + ); + } + else { + // 创建新的 SystemMessage + enhancedSystemMessage = new SystemMessage(contextInfo); + } + + // 构建新的消息列表 + List newMessages = new ArrayList<>(); + if (systemMessageIndex >= 0) { + // 如果找到了 SystemMessage,替换它 + for (int i = 0; i < previousMessages.size(); i++) { + if (i == systemMessageIndex) { + newMessages.add(enhancedSystemMessage); + } + else { + newMessages.add(previousMessages.get(i)); + } + } + } + else { + // 如果没有找到 SystemMessage,在开头添加新的 + newMessages.add(enhancedSystemMessage); + newMessages.addAll(previousMessages); + } + + // 使用 REPLACE 策略替换所有消息 + return new AgentCommand(newMessages, UpdatePolicy.REPLACE); + } + } + + MessagesModelHook combinedMemoryHook = new CombinedMemoryHook(); + + // 创建Agent + ReactAgent agent = ReactAgent.builder() + .name("combined_memory_agent") + .model(chatModel) + .hooks(combinedMemoryHook) + .saver(new MemorySaver()) // 短期记忆 + .build(); + + // 创建记忆存储 + MemoryStore memoryStore = new MemoryStore(); + // 设置长期记忆 + Map userProfile = new HashMap<>(); + userProfile.put("name", "李工程师"); + userProfile.put("occupation", "软件工程师"); + StoreItem profileItem = StoreItem.of(List.of("profiles"), "user_002", userProfile); + memoryStore.putItem(profileItem); + + RunnableConfig config = RunnableConfig.builder() + .threadId("combined_thread") + .addMetadata("user_id", "user_002") + .store(memoryStore) + .build(); + + // 短期记忆:在对话中记住 + agent.invoke("我今天在做一个 Spring 项目。", config); + + // 提出需要同时使用两种记忆的问题 + agent.invoke("根据我的职业和今天的工作,给我一些建议。", config); + // 响应会同时使用长期记忆(职业)和短期记忆(Spring项目) + + System.out.println("结合短期和长期记忆示例执行完成"); + } + + /** + * 示例5:跨会话记忆 + * + * 同一用户在不同会话中应该能够访问相同的长期记忆 + */ + public void example5_crossSessionMemory() throws GraphRunnerException { + record SaveMemoryRequest(List namespace, String key, Map value) { } + record GetMemoryRequest(List namespace, String key) { } + record MemoryResponse(String message, Map value) { } + + + ToolCallback saveMemoryTool = FunctionToolCallback.builder("saveMemory", + (BiFunction) (request, context) -> { + StoreItem item = StoreItem.of(request.namespace(), request.key(), request.value()); + RunnableConfig runnableConfig = (RunnableConfig) context.getContext().get("config"); + Store memoryStore = runnableConfig.store(); + memoryStore.putItem(item); + return new MemoryResponse("已保存", request.value()); + }) + .description("保存到长期记忆") + .inputType(SaveMemoryRequest.class) + .build(); + + ToolCallback getMemoryTool = FunctionToolCallback.builder("getMemory", + (BiFunction) (request, context) -> { + RunnableConfig runnableConfig = (RunnableConfig) context.getContext().get("config"); + Store memoryStore = runnableConfig.store(); + Optional itemOpt = memoryStore.getItem(request.namespace(), request.key()); + return new MemoryResponse( + itemOpt.isPresent() ? "找到" : "未找到", + itemOpt.map(StoreItem::getValue).orElse(Map.of()) + ); + }) + .description("从长期记忆获取") + .inputType(GetMemoryRequest.class) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("session_agent") + .model(chatModel) + .tools(saveMemoryTool, getMemoryTool) + .saver(new MemorySaver()) + .build(); + + // 创建记忆存储和工具 + MemoryStore memoryStore = new MemoryStore(); + // 会话1:保存信息 + RunnableConfig session1 = RunnableConfig.builder() + .threadId("session_morning") + .addMetadata("user_id", "user_003") + .store(memoryStore) + .build(); + + agent.invoke( + "记住我的密码是 secret123。用 saveMemory 保存,namespace=['credentials'], key='user_003_password', value={'password': 'secret123'}。", + session1 + ); + + // 会话2:检索信息(不同的线程,同一用户) + RunnableConfig session2 = RunnableConfig.builder() + .threadId("session_afternoon") + .addMetadata("user_id", "user_003") + .store(memoryStore) + .build(); + + agent.invoke( + "我的密码是什么?用 getMemory 获取,namespace=['credentials'], key='user_003_password'。", + session2 + ); + // 长期记忆在不同会话间持久化 + + System.out.println("跨会话记忆示例执行完成"); + } + + /** + * 示例6:用户偏好学习 + * + * Agent可以随着时间的推移学习并存储用户偏好 + * 使用MessagesModelHook实现 + */ + public void example6_preferLearning() throws GraphRunnerException { + MemoryStore memoryStore = new MemoryStore(); + + @HookPositions({HookPosition.AFTER_MODEL}) + class PreferenceLearningHook extends MessagesModelHook { + private final MemoryStore store; + + public PreferenceLearningHook(MemoryStore store) { + this.store = store; + } + + @Override + public String getName() { + return "preference_learning"; + } + + @Override + public AgentCommand afterModel(List previousMessages, RunnableConfig config) { + String userId = (String) config.metadata("user_id").orElse(null); + if (userId == null) { + return new AgentCommand(previousMessages); + } + + // 提取用户输入 + if (previousMessages.isEmpty()) { + return new AgentCommand(previousMessages); + } + + // 加载现有偏好 + Optional prefsOpt = store.getItem(List.of("user_data"), userId + "_preferences"); + List prefs = new ArrayList<>(); + if (prefsOpt.isPresent()) { + Map prefsData = prefsOpt.get().getValue(); + prefs = (List) prefsData.getOrDefault("items", new ArrayList<>()); + } + + // 简单的偏好提取(实际应用中使用NLP) + for (Message msg : previousMessages) { + String content = msg.getText().toLowerCase(); + if (content.contains("喜欢") || content.contains("偏好")) { + prefs.add(msg.getText()); + + Map prefsData = new HashMap<>(); + prefsData.put("items", prefs); + StoreItem item = StoreItem.of(List.of("user_data"), userId + "_preferences", prefsData); + store.putItem(item); + + System.out.println("学习到用户偏好 " + userId + ": " + msg.getText()); + } + } + + // 不修改消息,返回原始消息 + return new AgentCommand(previousMessages); + } + } + + MessagesModelHook preferenceLearningHook = new PreferenceLearningHook(memoryStore); + + ReactAgent agent = ReactAgent.builder() + .name("learning_agent") + .model(chatModel) + .hooks(preferenceLearningHook) + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("learning_thread") + .addMetadata("user_id", "user_004") + .build(); + + // 用户表达偏好 + agent.invoke("我喜欢喝绿茶。", config); + agent.invoke("我偏好早上运动。", config); + + // 验证偏好已被存储 + Optional savedPrefs = memoryStore.getItem(List.of("user_data"), "user_004_preferences"); + if (savedPrefs.isPresent()) { + System.out.println("已保存的偏好: " + savedPrefs.get().getValue()); + } + + System.out.println("用户偏好学习示例执行完成"); + } + + /** + * 运行所有示例 + */ + public void runAllExamples() { + System.out.println("=== 记忆管理(Memory)示例 ===\n"); + + try { + System.out.println("示例1: 在工具中读取长期记忆"); + example1_readMemoryInTool(); + System.out.println(); + // + // System.out.println("示例2: 在工具中写入长期记忆"); + // example2_writeMemoryInTool(); + // System.out.println(); + // + // System.out.println("示例3: 使用ModelHook管理长期记忆"); + // example3_memoryWithModelHook(); + // System.out.println(); + // + // System.out.println("示例4: 结合短期和长期记忆"); + // example4_combinedMemory(); + // System.out.println(); + // + // System.out.println("示例5: 跨会话记忆"); + // example5_crossSessionMemory(); + // System.out.println(); + // + // System.out.println("示例6: 用户偏好学习"); + // example6_preferLearning(); + // System.out.println(); + + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/MultiAgentExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/MultiAgentExample.java new file mode 100644 index 00000000..7e741aa9 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/MultiAgentExample.java @@ -0,0 +1,936 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.GraphResponse; +import com.alibaba.cloud.ai.graph.GraphRepresentation; +import com.alibaba.cloud.ai.graph.NodeOutput; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.flow.agent.LlmRoutingAgent; +import com.alibaba.cloud.ai.graph.agent.flow.agent.ParallelAgent; +import com.alibaba.cloud.ai.graph.agent.flow.agent.SequentialAgent; +import com.alibaba.cloud.ai.graph.agent.flow.agent.SupervisorAgent; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; + +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.model.ChatModel; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; + +import reactor.core.publisher.Flux; + +/** + * 多智能体(Multi-agent)示例 + * + * 演示不同的 Multi-agent 协作模式,包括: + * 1. 顺序执行(Sequential Agent) + * 2. 并行执行(Parallel Agent) + * 3. LLM路由(LlmRoutingAgent) + * 4. 自定义合并策略 + * 5. 监督者模式(SupervisorAgent) + * + * 参考文档: advanced_doc/multi-agent.md + */ +public class MultiAgentExample { + + private final ChatModel chatModel; + + public MultiAgentExample(ChatModel chatModel) { + this.chatModel = chatModel; + } + + /** + * Main方法:运行所有示例 + * + * 注意:需要配置ChatModel实例才能运行 + */ + public static void main(String[] args) { + // 创建 DashScope API 实例 + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 创建 ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + if (chatModel == null) { + System.err.println("错误:请先配置ChatModel实例"); + System.err.println("请设置 AI_DASHSCOPE_API_KEY 环境变量"); + return; + } + + // 创建示例实例 + MultiAgentExample example = new MultiAgentExample(chatModel); + + // 运行所有示例 + example.runAllExamples(); + } + + /** + * 示例1:顺序执行(Sequential Agent) + * + * 多个Agent按预定义的顺序依次执行,每个Agent的输出成为下一个Agent的输入 + */ + public void example1_sequentialAgent() throws Exception { + // 创建专业化的子Agent + ReactAgent writerAgent = ReactAgent.builder() + .name("writer_agent") + .model(chatModel) + .description("专业写作Agent") + .instruction("你是一个知名的作家,擅长写作和创作。请根据用户的提问进行回答:{input}。") + .outputKey("article") + .build(); + + ReactAgent reviewerAgent = ReactAgent.builder() + .name("reviewer_agent") + .model(chatModel) + .description("专业评审Agent") + .instruction("你是一个知名的评论家,擅长对文章进行评论和修改。" + + "对于散文类文章,请确保文章中必须包含对于西湖风景的描述。待评论文章:\n\n {article}" + + "最终只返回修改后的文章,不要包含任何评论信息。") + .outputKey("reviewed_article") + .build(); + + // 创建顺序Agent + SequentialAgent blogAgent = SequentialAgent.builder() + .name("blog_agent") + .description("根据用户给定的主题写一篇文章,然后将文章交给评论员进行评论") + .subAgents(List.of(writerAgent, reviewerAgent)) + .build(); + + // 使用 + Optional result = blogAgent.invoke("帮我写一个100字左右的散文"); + + if (result.isPresent()) { + OverAllState state = result.get(); + + // 访问第一个Agent的输出 + state.value("article").ifPresent(article -> { + if (article instanceof AssistantMessage) { + System.out.println("原始文章: " + ((AssistantMessage) article).getText()); + } + }); + + // 访问第二个Agent的输出 + state.value("reviewed_article").ifPresent(reviewedArticle -> { + if (reviewedArticle instanceof AssistantMessage) { + System.out.println("评审后文章: " + ((AssistantMessage) reviewedArticle).getText()); + } + }); + } + } + + /** + * 示例2:控制推理内容 + * + * 使用 returnReasoningContents 控制是否在消息历史中包含中间推理 + */ + public void example2_controlReasoningContents() throws Exception { + ReactAgent writerAgent = ReactAgent.builder() + .name("writer_agent") + .model(chatModel) + .returnReasoningContents(true) // 返回推理过程 + .outputKey("article") + .build(); + + ReactAgent reviewerAgent = ReactAgent.builder() + .name("reviewer_agent") + .model(chatModel) + .instruction("请对文章进行评审修正:\n{article},最终返回评审修正后的文章内容") + .includeContents(true) // 包含上一个Agent的推理内容 + .returnReasoningContents(true) // 返回推理过程 + .outputKey("reviewed_article") + .build(); + + + // 每个子agent的推理内容,下一个执行的子agent会看到上一个子agent的推理内容 + SequentialAgent blogAgent = SequentialAgent.builder() + .name("blog_agent") + .subAgents(List.of(writerAgent, reviewerAgent)) + .build(); + + Optional result = blogAgent.invoke("帮我写一个100字左右的散文"); + + if (result.isPresent()) { + // 消息历史将包含所有工具调用和推理过程 + List messages = (List) result.get().value("messages").orElse(List.of()); + System.out.println("消息数量: " + messages.size()); // 包含所有中间步骤 + } + } + + /** + * 示例3:并行执行(Parallel Agent) + * + * 多个Agent同时处理相同的输入,它们的结果被收集并合并 + */ + public void example3_parallelAgent() throws Exception { + // 创建多个专业化Agent + ReactAgent proseWriterAgent = ReactAgent.builder() + .name("prose_writer_agent") + .model(chatModel) + .description("专门写散文的AI助手") + .instruction("你是一个知名的散文作家,擅长写优美的散文。" + + "用户会给你一个主题:{input},你只需要创作一篇100字左右的散文。") + .outputKey("prose_result") + .enableLogging(true) + .build(); + + ReactAgent poemWriterAgent = ReactAgent.builder() + .name("poem_writer_agent") + .model(chatModel) + .description("专门写现代诗的AI助手") + .instruction("你是一个知名的现代诗人,擅长写现代诗。" + + "用户会给你的主题是:{input},你只需要创作一首现代诗。") + .outputKey("poem_result") + .enableLogging(true) + .build(); + + ReactAgent summaryAgent = ReactAgent.builder() + .name("summary_agent") + .model(chatModel) + .description("专门做内容总结的AI助手") + .instruction("你是一个专业的内容分析师,擅长对主题进行总结和提炼。" + + "用户会给你一个主题:{input},你只需要对这个主题进行简要总结。") + .outputKey("summary_result") + .enableLogging(true) + .build(); + + // 创建并行Agent + ParallelAgent parallelAgent = ParallelAgent.builder() + .name("parallel_creative_agent") + .description("并行执行多个创作任务,包括写散文、写诗和做总结") + .mergeOutputKey("merged_results") + .subAgents(List.of(proseWriterAgent, poemWriterAgent, summaryAgent)) + .mergeStrategy(new ParallelAgent.DefaultMergeStrategy()) + .build(); + + ExecutorService executorService = Executors.newFixedThreadPool(3); + // 使用 + Flux flux = parallelAgent.stream("以'西湖'为主题", RunnableConfig.builder().addParallelNodeExecutor("parallel_creative_agent", executorService).build()); + + AtomicReference lastOutput = new AtomicReference<>(); + flux.doOnNext(nodeOutput -> { + System.out.println("节点输出: " + nodeOutput); + lastOutput.set(nodeOutput); + }).doOnError(error -> { + System.err.println("执行出错: " + error.getMessage()); + }).doOnComplete(() -> { + System.out.println("并行Agent流式执行完成\n\n"); + + NodeOutput output = lastOutput.get(); + if (output == null) { + System.out.println("未收到任何输出,无法展示结果。"); + return; + } + + OverAllState state = output.state(); + // 访问各个Agent的输出 + state.value("prose_result").ifPresent(r -> + System.out.println("散文: " + r)); + state.value("poem_result").ifPresent(r -> + System.out.println("诗歌: " + r)); + state.value("summary_result").ifPresent(r -> + System.out.println("总结: " + r)); + + // 访问合并后的结果 + state.value("merged_results").ifPresent(r -> + System.out.println("合并结果: " + r)); + }).blockLast(); + + } + + /** + * 示例4:自定义合并策略 + * + * 实现自定义的合并策略来控制如何组合多个Agent的输出 + */ + public void example4_customMergeStrategy() throws Exception { + // 自定义合并策略 + class CustomMergeStrategy implements ParallelAgent.MergeStrategy { + @Override + public Map merge(Map mergedState, OverAllState state) { + // 从每个Agent的状态中提取输出 + state.data().forEach((key, value) -> { + // 检查key不为null且以"_result"结尾 + if (key != null && key.endsWith("_result")) { + String resultText = ""; + if (value instanceof GraphResponse graphResponse) { + if (graphResponse.resultValue().isPresent()) { + resultText = graphResponse.resultValue().get().toString(); + } + } else if (value != null) { + resultText = value.toString(); + } + Object existing = mergedState.get("all_results"); + if (existing == null) { + mergedState.put("all_results", resultText); + } + else { + mergedState.put("all_results", existing + "\n\n---\n\n" + resultText); + } + } + }); + return mergedState; + } + } + + // 创建Agent + ReactAgent agent1 = ReactAgent.builder() + .name("agent1") + .model(chatModel) + .outputKey("agent1_result") + .build(); + + ReactAgent agent2 = ReactAgent.builder() + .name("agent2") + .model(chatModel) + .outputKey("agent2_result") + .build(); + + ReactAgent agent3 = ReactAgent.builder() + .name("agent3") + .model(chatModel) + .outputKey("agent3_result") + .build(); + + // 使用自定义合并策略 + ParallelAgent parallelAgent = ParallelAgent.builder() + .name("parallel_agent") + .subAgents(List.of(agent1, agent2, agent3)) + .mergeStrategy(new CustomMergeStrategy()) + .mergeOutputKey("all_results") + .build(); + + Optional result = parallelAgent.invoke("分析这个主题"); + + if (result.isPresent()) { + OverAllState state = result.get(); + state.value("all_results").ifPresent(mergeResult -> { + System.out.println("合并结果: " + mergeResult); + }); + System.out.println("自定义合并策略示例执行成功"); + } + } + + /** + * 示例5:LLM路由(LlmRoutingAgent) + * + * 使用大语言模型动态决定将请求路由到哪个子Agent + */ + public void example5_llmRoutingAgent() throws Exception { + // 创建专业化的子Agent + ReactAgent writerAgent = ReactAgent.builder() + .name("writer_agent") + .model(chatModel) + .description("擅长创作各类文章,包括散文、诗歌等文学作品") + .instruction("你是一个知名的作家,擅长写作和创作。请根据用户的提问进行回答。") + .outputKey("writer_output") + .build(); + + ReactAgent reviewerAgent = ReactAgent.builder() + .name("reviewer_agent") + .model(chatModel) + .description("擅长对文章进行评论、修改和润色") + .instruction("你是一个知名的评论家,擅长对文章进行评论和修改。" + + "对于散文类文章,请确保文章中必须包含对于西湖风景的描述。") + .outputKey("reviewer_output") + .build(); + + ReactAgent translatorAgent = ReactAgent.builder() + .name("translator_agent") + .model(chatModel) + .description("擅长将文章翻译成各种语言") + .instruction("你是一个专业的翻译家,能够准确地将文章翻译成目标语言。") + .outputKey("translator_output") + .build(); + + // 创建路由Agent + LlmRoutingAgent routingAgent = LlmRoutingAgent.builder() + .name("content_routing_agent") + .description("根据用户需求智能路由到合适的专家Agent") + .model(chatModel) + .subAgents(List.of(writerAgent, reviewerAgent, translatorAgent)) + .build(); + + // 使用 - LLM会自动选择最合适的Agent + System.out.println("路由测试1: 写作请求"); + Optional result1 = routingAgent.invoke("帮我写一篇关于春天的散文"); + // LLM会路由到 writerAgent + + System.out.println("路由测试2: 修改请求"); + Optional result2 = routingAgent.invoke("请帮我修改这篇文章:春天来了,花开了。"); + // LLM会路由到 reviewerAgent + + System.out.println("路由测试3: 翻译请求"); + Optional result3 = routingAgent.invoke("请将以下内容翻译成英文:春暖花开"); + // LLM会路由到 translatorAgent + + System.out.println("LLM路由示例执行完成"); + } + + /** + * 示例6:优化路由准确性 + * + * 通过提供清晰明确的Agent描述来提高路由的准确性 + */ + public void example6_optimizedRouting() throws Exception { + // 1. 提供清晰明确的Agent描述 + ReactAgent codeAgent = ReactAgent.builder() + .name("code_agent") + .model(chatModel) + .description("专门处理编程相关问题,包括代码编写、调试、重构和优化。" + + "擅长Java、Python、JavaScript等主流编程语言。") + .instruction("你是一个资深的软件工程师...") + .build(); + + // 2. 明确Agent的职责边界 + ReactAgent businessAgent = ReactAgent.builder() + .name("business_agent") + .model(chatModel) + .description("专门处理商业分析、市场研究和战略规划问题。" + + "不处理技术实现细节。") + .instruction("你是一个资深的商业分析师...") + .build(); + + ReactAgent writerAgent = ReactAgent.builder() + .name("writer_agent") + .model(chatModel) + .description("专门处理内容创作,包括文章、报告、文案等写作任务。") + .instruction("你是一个专业作家...") + .build(); + + // 3. 使用不同领域的Agent避免重叠 + LlmRoutingAgent routingAgent = LlmRoutingAgent.builder() + .name("multi_domain_router") + .model(chatModel) + .subAgents(List.of(codeAgent, businessAgent, writerAgent)) + .build(); + + // 测试路由 + routingAgent.invoke("如何用Java实现单例模式?"); + routingAgent.invoke("分析一下这个市场的竞争态势"); + routingAgent.invoke("写一篇产品介绍文案"); + + System.out.println("优化路由示例执行完成"); + } + + /** + * 示例7:混合模式 - 结合顺序、并行和路由 + * + * 组合不同的模式创建复杂的工作流 + */ + public void example7_hybridPattern() throws Exception { + // 创建研究Agent(并行执行) + ReactAgent webResearchAgent = ReactAgent.builder() + .name("web_research") + .model(chatModel) + .description("从互联网搜索信息") + .instruction("请搜索并收集关于以下主题的信息:{input}") + .outputKey("web_data") + .build(); + + ReactAgent dbResearchAgent = ReactAgent.builder() + .name("db_research") + .model(chatModel) + .description("从数据库查询信息") + .instruction("请从数据库中查询并收集关于以下主题的信息:{input}") + .outputKey("db_data") + .build(); + + ParallelAgent researchAgent = ParallelAgent.builder() + .name("parallel_research") + .description("并行收集多个数据源的信息") + .subAgents(List.of(webResearchAgent, dbResearchAgent)) + .mergeOutputKey("research_data") + .build(); + + // 创建分析Agent + ReactAgent analysisAgent = ReactAgent.builder() + .name("analysis_agent") + .model(chatModel) + .description("分析研究数据") + .instruction("请分析以下收集到的数据并提供见解:{research_data}") + .outputKey("analysis_result") + .build(); + + // 创建报告Agent(路由选择格式) + ReactAgent pdfReportAgent = ReactAgent.builder() + .name("pdf_report") + .model(chatModel) + .description("生成PDF格式报告") + .instruction(""" + 请根据研究结果和分析结果生成一份PDF格式的报告。 + + 研究结果:{research_data} + 分析结果:{analysis_result} + """) + .outputKey("pdf_report") + .build(); + + ReactAgent htmlReportAgent = ReactAgent.builder() + .name("html_report") + .model(chatModel) + .description("生成HTML格式报告") + .instruction(""" + 请根据研究结果和分析结果生成一份HTML格式的报告。 + + 研究结果:{research_data} + 分析结果:{analysis_result} + """) + .outputKey("html_report") + .build(); + + LlmRoutingAgent reportAgent = LlmRoutingAgent.builder() + .name("report_router") + .description("根据需求选择报告格式") + .model(chatModel) + .subAgents(List.of(pdfReportAgent, htmlReportAgent)) + .build(); + + // 组合成顺序工作流 + SequentialAgent hybridWorkflow = SequentialAgent.builder() + .name("research_workflow") + .description("完整的研究工作流:并行收集 -> 分析 -> 路由生成报告") + .subAgents(List.of(researchAgent, analysisAgent, reportAgent)) + .build(); + + + // 打印工作流图表 + System.out.println("\n=== 混合模式工作流图表 ==="); + printGraphRepresentation(hybridWorkflow); + System.out.println("=========================\n"); + + Optional result = hybridWorkflow.invoke("研究AI技术趋势并生成HTML报告"); + + if (result.isPresent()) { + System.out.println("混合模式示例执行成功"); + } + } + + /** + * 示例8:监督者模式(SupervisorAgent) + * + * SupervisorAgent 与 LlmRoutingAgent 类似,但有以下关键区别: + * 1. 子Agent处理完成后会返回到Supervisor,而不是直接结束 + * 2. Supervisor可以决定继续路由到其他子Agent,或者标记任务完成(FINISH) + * 3. 支持嵌套Agent(如SequentialAgent、ParallelAgent)作为子Agent + * + * 这个示例展示了如何使用SupervisorAgent管理包含普通ReactAgent和嵌套SequentialAgent的复杂工作流 + */ + public void example8_supervisorAgent() throws Exception { + // 定义专业的监督者指令(如果不定义,则使用系统默认的提示词) + final String SUPERVISOR_INSTRUCTION = """ + 你是一个智能的内容管理监督者,负责协调和管理多个专业Agent来完成用户的内容处理需求。 + + ## 你的职责 + 1. 分析用户需求,将其分解为合适的子任务 + 2. 根据任务特性,选择合适的Agent进行处理 + 3. 监控任务执行状态,决定是否需要继续处理或完成任务 + 4. 当所有任务完成时,返回FINISH结束流程 + + ## 可用的子Agent及其职责 + + ### writer_agent + - **功能**: 擅长创作各类文章,包括散文、诗歌等文学作品 + - **适用场景**: + * 用户需要创作新文章、散文、诗歌等原创内容 + * 简单的写作任务,不需要后续评审或修改 + - **输出**: writer_output + + ### translator_agent + - **功能**: 擅长将文章翻译成各种语言 + - **适用场景**: + * 用户需要将内容翻译成其他语言 + * 翻译任务通常是单一操作,不需要多步骤处理 + - **输出**: translator_output + + ### writing_workflow_agent + - **功能**: 完整的写作工作流,包含两个步骤:先写文章,然后进行评审和修改 + - **适用场景**: + * 用户需要高质量的文章,要求经过评审和修改 + * 任务明确要求"确保质量"、"需要评审"、"需要修改"等 + * 需要多步骤处理的复杂写作任务 + - **工作流程**: + 1. article_writer: 根据用户需求创作文章 + 2. reviewer: 对文章进行评审和修改,确保质量 + - **输出**: reviewed_article + + ## 决策规则 + + 1. **单一任务判断**: + - 如果用户只需要翻译,选择 translator_agent + - 如果用户只需要简单写作,选择 writer_agent + - 如果用户需要高质量文章或明确要求评审,选择 writing_workflow_agent + + 2. **多步骤任务处理**: + - 如果用户需求包含多个步骤(如"先写文章,然后翻译"),需要分步处理 + - 先路由到第一个合适的Agent,等待其完成 + - 完成后,根据剩余需求继续路由到下一个Agent + - 直到所有步骤完成,返回FINISH + + 3. **任务完成判断**: + - 当用户的所有需求都已满足时,返回FINISH + - 如果还有未完成的任务,继续路由到相应的Agent + + ## 响应格式 + 只返回Agent名称(writer_agent、translator_agent、writing_workflow_agent)或FINISH,不要包含其他解释。 + """; + // 1. 创建普通的ReactAgent子Agent + ReactAgent writerAgent = ReactAgent.builder() + .name("writer_agent") + .model(chatModel) + .description("擅长创作各类文章,包括散文、诗歌等文学作品") + .instruction("你是一个知名的作家,擅长写作和创作。请根据用户的提问进行回答:\n\n {input}。") + .outputKey("writer_output") + .build(); + + ReactAgent translatorAgent = ReactAgent.builder() + .name("translator_agent") + .model(chatModel) + .description("擅长将文章翻译成各种语言") + .instruction("你是一个专业的翻译家,能够准确地将文章翻译成目标语言。" + + "如果待翻译的内容已存在于状态中,请使用:\n\n {writer_output}。") + .outputKey("translator_output") + .build(); + + // 2. 创建嵌套的SequentialAgent作为子Agent + // 这个SequentialAgent包含多个步骤:先写文章,再评审 + ReactAgent articleWriterAgent = ReactAgent.builder() + .name("article_writer") + .model(chatModel) + .description("专业写作Agent") + .instruction("你是一个知名的作家,擅长写作和创作。请根据用户的提问进行回答:{input}。") + .outputKey("article") + .build(); + + ReactAgent reviewerAgent = ReactAgent.builder() + .name("reviewer") + .model(chatModel) + .description("专业评审Agent") + .instruction("你是一个知名的评论家,擅长对文章进行评论和修改。" + + "对于散文类文章,请确保文章中必须包含对于西湖风景的描述。待评论文章:\n\n {article}" + + "最终只返回修改后的文章,不要包含任何评论信息。") + .outputKey("reviewed_article") + .build(); + + // 创建嵌套的SequentialAgent + SequentialAgent writingWorkflowAgent = SequentialAgent.builder() + .name("writing_workflow_agent") + .description("完整的写作工作流:先写文章,然后进行评审和修改") + .subAgents(List.of(articleWriterAgent, reviewerAgent)) + .build(); + + // 3. 创建SupervisorAgent,包含普通Agent和嵌套Agent + SupervisorAgent supervisorAgent = SupervisorAgent.builder() + .name("content_supervisor") + .description("内容管理监督者,负责协调写作、翻译和完整写作工作流等任务") + .model(chatModel) + .systemPrompt(SUPERVISOR_INSTRUCTION) + .subAgents(List.of(writerAgent, translatorAgent, writingWorkflowAgent)) + .build(); + + // 使用示例 + System.out.println("监督者测试1: 简单写作任务"); + Optional result1 = supervisorAgent.invoke("帮我写一篇关于春天的短文"); + // Supervisor会路由到writer_agent,处理完成后返回Supervisor,Supervisor判断完成返回FINISH + if (result1.isPresent()) { + result1.get().value("writer_output").ifPresent(output -> + System.out.println("写作结果: " + output)); + } + + System.out.println("\n监督者测试2: 需要完整工作流的任务"); + Optional result2 = supervisorAgent.invoke("帮我写一篇关于西湖的散文,并确保质量"); + // Supervisor会路由到writing_workflow_agent(嵌套SequentialAgent), + // 该Agent会先写文章,然后评审,完成后返回Supervisor,Supervisor判断完成返回FINISH + if (result2.isPresent()) { + result2.get().value("reviewed_article").ifPresent(output -> + System.out.println("评审后文章: " + output)); + } + + System.out.println("\n监督者测试3: 翻译任务"); + Optional result3 = supervisorAgent.invoke("请将以下内容翻译成英文:春暖花开"); + // Supervisor会路由到translator_agent,处理完成后返回Supervisor,Supervisor判断完成返回FINISH + if (result3.isPresent()) { + result3.get().value("translator_output").ifPresent(output -> + System.out.println("翻译结果: " + output)); + } + + System.out.println("\n监督者测试4: 多步骤任务(可能需要多次路由)"); + Optional result4 = supervisorAgent.invoke("先帮我写一篇关于春天的文章,然后翻译成英文"); + // Supervisor可能会: + // 1. 先路由到writer_agent写文章,完成后返回Supervisor + // 2. Supervisor判断还需要翻译,路由到translator_agent + // 3. 翻译完成后返回Supervisor,Supervisor判断所有任务完成,返回FINISH + if (result4.isPresent()) { + result4.get().value("writer_output").ifPresent(output -> + System.out.println("写作结果: " + output)); + result4.get().value("translator_output").ifPresent(output -> + System.out.println("翻译结果: " + output)); + } + + // 打印工作流图表 + System.out.println("\n=== SupervisorAgent 工作流图表 ==="); + printGraphRepresentation(supervisorAgent); + System.out.println("==================================\n"); + + // 示例5:SupervisorAgent作为SequentialAgent的子Agent,使用占位符 + System.out.println("\n监督者测试5: SupervisorAgent作为SequentialAgent的子Agent(使用占位符)"); + example8_supervisorAgentAsSequentialSubAgent(); + System.out.println(); + + System.out.println("SupervisorAgent示例执行完成"); + } + + /** + * 示例8.1:SupervisorAgent作为SequentialAgent的子Agent,使用占位符 + * + * 这个示例展示了: + * 1. SupervisorAgent可以作为SequentialAgent的子Agent + * 2. SupervisorAgent的instruction可以使用占位符引用前序Agent的输出 + * 3. SupervisorAgent的子Agent的instruction也可以使用占位符引用前序Agent的输出 + */ + private void example8_supervisorAgentAsSequentialSubAgent() throws Exception { + // 1. 创建第一个Agent,用于生成文章内容 + ReactAgent articleWriterAgent = ReactAgent.builder() + .name("article_writer") + .model(chatModel) + .description("专业写作Agent,负责创作文章") + .instruction("你是一个知名的作家,擅长写作和创作。请根据用户的提问进行回答:{input}。") + .outputKey("article_content") + .build(); + + // 2. 创建SupervisorAgent的子Agent + ReactAgent translatorAgent = ReactAgent.builder() + .name("translator_agent") + .model(chatModel) + .description("擅长将文章翻译成各种语言") + .instruction("你是一个专业的翻译家,能够准确地将文章翻译成目标语言。待翻译文章:\n\n {article_content}。") + .outputKey("translator_output") + .build(); + + ReactAgent reviewerAgent = ReactAgent.builder() + .name("reviewer_agent") + .model(chatModel) + .description("擅长对文章进行评审和修改") + .instruction("你是一个知名的评论家,擅长对文章进行评论和修改。待评审文章:\n\n {article_content}。" + + "请对文章进行评审,指出优点和需要改进的地方,并返回评审后的改进版本。") + .outputKey("reviewer_output") + .build(); + + // 3. 定义SupervisorAgent的instruction,使用占位符引用前序Agent的输出 + // 这个instruction包含 {article_content} 占位符,会被替换为第一个Agent的输出 + final String SUPERVISOR_INSTRUCTION = """ + 你是一个智能的内容处理监督者,你可以看到前序Agent的聊天历史与任务处理记录。当前,你收到了以下文章内容: + + {article_content} + + 请根据文章内容的特点和用户需求,决定是进行翻译还是评审: + - 如果用户要求翻译或文章需要翻译成其他语言,选择 translator_agent + - 如果用户要求评审、改进或优化文章,选择 reviewer_agent + - 如果任务完成,返回 FINISH + """; + + final String SUPERVISOR_SYSTEM_PROMPT = """ + 你是一个智能的内容处理监督者,负责协调翻译和评审任务。 + + ## 可用的子Agent及其职责 + + ### translator_agent + - **功能**: 擅长将文章翻译成各种语言 + - **适用场景**: 当文章需要翻译成其他语言时 + - **输出**: translator_output + + ### reviewer_agent + - **功能**: 擅长对文章进行评审和修改 + - **适用场景**: 当文章需要评审、改进或优化时 + - **输出**: reviewer_output + + ## 决策规则 + + 1. **根据文章内容和用户需求判断**: + - 如果用户要求翻译或文章需要翻译成其他语言,选择 translator_agent + - 如果用户要求评审、改进或优化文章,选择 reviewer_agent + + 2. **任务完成判断**: + - 当所有任务完成时,返回 FINISH + + ## 响应格式 + 只返回Agent名称(translator_agent、reviewer_agent)或FINISH,不要包含其他解释。 + """; + + // 4. 创建SupervisorAgent,其instruction使用占位符 + SupervisorAgent supervisorAgent = SupervisorAgent.builder() + .name("content_supervisor") + .description("内容处理监督者,根据前序Agent的输出决定翻译或评审") + .model(chatModel) + .systemPrompt(SUPERVISOR_SYSTEM_PROMPT) + .instruction(SUPERVISOR_INSTRUCTION) // 这个instruction包含 {article_content} 占位符 + .subAgents(List.of(translatorAgent, reviewerAgent)) + .build(); + + // 5. 创建SequentialAgent,先执行articleWriterAgent,然后执行supervisorAgent + SequentialAgent sequentialAgent = SequentialAgent.builder() + .name("content_processing_workflow") + .description("内容处理工作流:先写文章,然后根据文章内容决定翻译或评审") + .subAgents(List.of(articleWriterAgent, supervisorAgent)) + .build(); + + // 测试场景1:写文章后翻译 + System.out.println("场景1: 写文章后翻译"); + Optional result1 = sequentialAgent.invoke("帮我写一篇关于春天的短文,然后翻译成英文"); + if (result1.isPresent()) { + OverAllState state = result1.get(); + state.value("article_content").ifPresent(output -> { + if (output instanceof AssistantMessage) { + System.out.println("文章内容: " + ((AssistantMessage) output).getText()); + } + }); + state.value("translator_output").ifPresent(output -> { + if (output instanceof AssistantMessage) { + System.out.println("翻译结果: " + ((AssistantMessage) output).getText()); + } + }); + } + + // 测试场景2:写文章后评审 + System.out.println("\n场景2: 写文章后评审"); + Optional result2 = sequentialAgent.invoke("帮我写一篇关于春天的短文,然后进行评审和改进"); + if (result2.isPresent()) { + OverAllState state = result2.get(); + state.value("article_content").ifPresent(output -> { + if (output instanceof AssistantMessage) { + System.out.println("文章内容: " + ((AssistantMessage) output).getText()); + } + }); + state.value("reviewer_output").ifPresent(output -> { + if (output instanceof AssistantMessage) { + System.out.println("评审结果: " + ((AssistantMessage) output).getText()); + } + }); + } + } + + /** + * 打印工作流图表(支持SupervisorAgent) + */ + private void printGraphRepresentation(SupervisorAgent agent) { + GraphRepresentation representation = agent.getAndCompileGraph().getGraph(GraphRepresentation.Type.PLANTUML); + System.out.println(representation.content()); + } + + private void testRoutingSequentialEmbedding() throws GraphRunnerException { + ReactAgent reactAgent = ReactAgent.builder() + .name("weather_agent") + .description("根据用户的问题和提炼的位置信息查询天气。\n\n 用户问题:{input} \n\n 位置信息:{location}") + .model(chatModel) + .outputKey("weather") + .systemPrompt("你是一个天气查询专家").build(); + + ReactAgent locationAgent = ReactAgent.builder() + .name("location_agent") + .description("根据用户的问题,进行位置查询。\n 用户问题:{input}") + .model(chatModel) + .outputKey("location") + .systemPrompt("你是一个位置查询专家").build(); + + SequentialAgent sequentialAgent = SequentialAgent.builder() + .name("天气小助手") + .description("天气小助手") + .subAgents(List.of(locationAgent, reactAgent)) + .build(); + + LlmRoutingAgent routingAgent = LlmRoutingAgent.builder() + .name("用户小助手") + .description("帮助用户完成各种需求") +// .routingInstruction(""); // 可以提供详尽的说明,告知routing路由职责,如何选择子Agent等,用于替代系统默认的prompt。 + .model(chatModel) + .subAgents(List.of(sequentialAgent)).build(); + + Optional invoke = routingAgent.invoke("天气怎么样"); + System.out.println(invoke); + } + + /** + * 打印工作流图表 + * + * 使用PlantUML格式展示Agent工作流的结构 + */ + private void printGraphRepresentation(SequentialAgent agent) { + GraphRepresentation representation = agent.getAndCompileGraph().getGraph(GraphRepresentation.Type.PLANTUML); + System.out.println(representation.content()); + } + + /** + * 运行所有示例 + */ + public void runAllExamples() { + System.out.println("=== 多智能体(Multi-agent)示例 ===\n"); + + try { + System.out.println("示例1: 顺序执行(Sequential Agent)"); + example1_sequentialAgent(); + System.out.println(); + + System.out.println("示例2: 控制推理内容"); + example2_controlReasoningContents(); + System.out.println(); + + System.out.println("示例3: 并行执行(Parallel Agent)"); + example3_parallelAgent(); + System.out.println(); + + System.out.println("示例4: 自定义合并策略"); + example4_customMergeStrategy(); + System.out.println(); +// + System.out.println("示例5: LLM路由(LlmRoutingAgent)"); + example5_llmRoutingAgent(); + System.out.println(); + + System.out.println("示例6: 优化路由准确性"); + example6_optimizedRouting(); + System.out.println(); + + System.out.println("示例7: 混合模式"); + example7_hybridPattern(); + System.out.println(); + + System.out.println("示例8: 监督者模式(SupervisorAgent)"); + example8_supervisorAgent(); + System.out.println(); + + testRoutingSequentialEmbedding(); + + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/RAGExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/RAGExample.java new file mode 100644 index 00000000..0ef59784 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/RAGExample.java @@ -0,0 +1,430 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.document.Document; +import org.springframework.ai.reader.TextReader; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; +import org.springframework.ai.vectorstore.VectorStore; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 检索增强生成(RAG)示例 + * + * 演示如何使用RAG技术为LLM提供外部知识,包括: + * 1. 构建知识库 + * 2. 两步RAG + * 3. Agentic RAG + * 4. 混合RAG + * + * 参考文档: advanced_doc/rag.md + */ +public class RAGExample { + + private final ChatModel chatModel; + private final VectorStore vectorStore; + + public RAGExample(ChatModel chatModel, VectorStore vectorStore) { + this.chatModel = chatModel; + this.vectorStore = vectorStore; + } + + /** + * Main方法:运行所有示例 + * + * 注意:需要配置ChatModel和VectorStore实例才能运行 + */ + public static void main(String[] args) { + // 创建 DashScope API 实例 + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 创建 ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // TODO: 请配置您的VectorStore实例 + // 例如:VectorStore vectorStore = new YourVectorStoreImplementation(); + VectorStore vectorStore = null; // 请替换为实际的VectorStore实例 + + if (chatModel == null || vectorStore == null) { + System.err.println("错误:请先配置ChatModel和VectorStore实例"); + System.err.println("请设置 AI_DASHSCOPE_API_KEY 环境变量,并配置VectorStore实例"); + return; + } + + // 创建示例实例 + RAGExample example = new RAGExample(chatModel, vectorStore); + + // 运行所有示例 + example.runAllExamples(); + } + + /** + * 示例1:构建知识库 + * + * 从文档加载、分割、嵌入并存储到向量数据库 + */ + public void example1_buildKnowledgeBase() { + // 1. 加载文档 + Resource resource = new FileSystemResource("path/to/document.txt"); + TextReader textReader = new TextReader(resource); + List documents = textReader.get(); + + // 2. 分割文档为块 + TokenTextSplitter splitter = new TokenTextSplitter(); + List chunks = splitter.apply(documents); + + // 3. 将块添加到向量存储 + vectorStore.add(chunks); + + // 现在可以使用向量存储进行检索 + List results = vectorStore.similaritySearch("查询文本"); + + System.out.println("知识库构建完成,检索到 " + results.size() + " 个相关文档"); + } + + /** + * 示例2:两步RAG + * + * 检索步骤总是在生成步骤之前执行 + */ + public void example2_twoStepRAG() { + // 两步RAG:检索 -> 生成 + String userQuestion = "Spring AI Alibaba支持哪些模型?"; + + // Step 1: Retrieve relevant documents + List relevantDocs = vectorStore.similaritySearch(userQuestion); + + // Step 2: Build context from documents + String context = relevantDocs.stream() + .map(Document::getText) + .collect(Collectors.joining("\n\n")); + + // Step 3: Generate answer with context + ChatClient chatClient = ChatClient.builder(chatModel).build(); + String answer = chatClient.prompt() + .user(u -> u.text("基于以下上下文回答问题:\n\n上下文:\n" + context + "\n\n问题:" + userQuestion)) + .call() + .content(); + + System.out.println("答案: " + answer); + + // 检索到的文档作为上下文添加到提示中 + // ChatModel 使用增强的上下文生成答案 + + System.out.println("两步RAG示例执行完成"); + } + + /** + * 示例3:Agentic RAG + * + * Agent决定何时以及如何检索信息 + */ + public void example3_agenticRAG() throws Exception { + // 创建文档检索工具 + class DocumentSearchTool { + public Response search(Request request) { + // 从向量存储检索相关文档 + List docs = vectorStore.similaritySearch(request.query()); + + // 合并文档内容 + String combinedContent = docs.stream() + .map(Document::getText) + .collect(Collectors.joining("\n\n")); + + return new Response(combinedContent); + } + + public record Request(String query) { } + + public record Response(String content) { } + } + + DocumentSearchTool searchTool = new DocumentSearchTool(); + + // 创建工具回调 + ToolCallback searchCallback = FunctionToolCallback.builder("search_documents", + (Function) + request -> searchTool.search(request)) + .description("搜索文档以查找相关信息") + .inputType(DocumentSearchTool.Request.class) + .build(); + + // 创建带有检索工具的Agent + ReactAgent ragAgent = ReactAgent.builder() + .name("rag_agent") + .model(chatModel) + .instruction("你是一个智能助手。当需要查找信息时,使用search_documents工具。" + + "基于检索到的信息回答用户的问题,并引用相关片段。") + .tools(searchCallback) + .build(); + + // Agent会自动决定何时调用检索工具 + ragAgent.invoke("Spring AI Alibaba支持哪些向量数据库?"); + + System.out.println("Agentic RAG示例执行完成"); + } + + /** + * 示例4:多源RAG + * + * Agent可以从多个来源检索信息 + */ + public void example4_multiSourceRAG() throws Exception { + // 创建多个检索工具 + class WebSearchTool { + public Response search(Request request) { + return new Response("从网络搜索到的信息: " + request.query()); + } + + public record Request(String query) { } + + public record Response(String content) { } + } + + class DatabaseQueryTool { + public Response query(Request request) { + return new Response("从数据库查询到的信息: " + request.query()); + } + + public record Request(String query) { } + + public record Response(String content) { } + } + + class DocumentSearchTool { + public Response search(Request request) { + List docs = vectorStore.similaritySearch(request.query()); + String content = docs.stream() + .map(Document::getText) + .collect(Collectors.joining("\n\n")); + return new Response(content); + } + + public record Request(String query) { } + + public record Response(String content) { } + } + + WebSearchTool webSearchTool = new WebSearchTool(); + DatabaseQueryTool dbQueryTool = new DatabaseQueryTool(); + DocumentSearchTool docSearchTool = new DocumentSearchTool(); + + ToolCallback webSearchCallback = FunctionToolCallback.builder("web_search", + (Function) + req -> webSearchTool.search(req)) + .description("搜索互联网以获取最新信息") + .inputType(WebSearchTool.Request.class) + .build(); + + ToolCallback databaseQueryCallback = FunctionToolCallback.builder("database_query", + (Function) + req -> dbQueryTool.query(req)) + .description("查询内部数据库") + .inputType(DatabaseQueryTool.Request.class) + .build(); + + ToolCallback documentSearchCallback = FunctionToolCallback.builder("document_search", + (Function) + req -> docSearchTool.search(req)) + .description("搜索文档库") + .inputType(DocumentSearchTool.Request.class) + .build(); + + // Agent可以访问多个检索源 + ReactAgent multiSourceAgent = ReactAgent.builder() + .name("multi_source_rag_agent") + .model(chatModel) + .instruction("你可以访问多个信息源:" + + "1. web_search - 用于最新的互联网信息\n" + + "2. database_query - 用于内部数据\n" + + "3. document_search - 用于文档库\n" + + "根据问题选择最合适的工具。") + .tools(webSearchCallback, databaseQueryCallback, documentSearchCallback) + .build(); + + multiSourceAgent.invoke("比较我们的产品文档中的功能和最新的市场趋势"); + + System.out.println("多工具Agentic RAG示例执行完成"); + } + + /** + * 示例5:混合RAG + * + * 结合查询增强、检索验证和答案验证 + */ + public void example5_hybridRAG() { + class HybridRAGSystem { + private final ChatModel chatModel; + private final VectorStore vectorStore; + + public HybridRAGSystem(ChatModel chatModel, VectorStore vectorStore) { + this.chatModel = chatModel; + this.vectorStore = vectorStore; + } + + public String answer(String userQuestion) { + // 1. 查询增强 + String enhancedQuery = enhanceQuery(userQuestion); + + int maxAttempts = 3; + for (int attempt = 0; attempt < maxAttempts; attempt++) { + // 2. 检索文档 + List docs = vectorStore.similaritySearch(enhancedQuery); + + // 3. 检索验证 + if (!isRetrievalSufficient(docs)) { + enhancedQuery = refineQuery(enhancedQuery, docs); + continue; + } + + // 4. 生成答案 + String answer = generateAnswer(userQuestion, docs); + + // 5. 答案验证 + ValidationResult validation = validateAnswer(answer, docs); + if (validation.isValid()) { + return answer; + } + + // 6. 根据验证结果决定下一步 + if (validation.shouldRetry()) { + enhancedQuery = refineBasedOnValidation(enhancedQuery, validation); + } + else { + return answer; // 返回当前最佳答案 + } + } + + return "无法生成满意的答案"; + } + + private String enhanceQuery(String query) { + return query; // 实现查询增强逻辑 + } + + private boolean isRetrievalSufficient(List docs) { + return !docs.isEmpty() && calculateRelevanceScore(docs) > 0.7; + } + + private double calculateRelevanceScore(List docs) { + return 0.8; // 实现相关性评分逻辑 + } + + private String refineQuery(String query, List docs) { + return query; // 实现查询优化逻辑 + } + + private String generateAnswer(String question, List docs) { + String context = docs.stream() + .map(Document::getText) + .collect(Collectors.joining("\n\n")); + + ChatClient client = ChatClient.builder(chatModel).build(); + return client.prompt() + .system("基于以下上下文回答问题:\n" + context) + .user(question) + .call() + .content(); + } + + private ValidationResult validateAnswer(String answer, List docs) { + // 实现答案验证逻辑 + return new ValidationResult(true, false); + } + + private String refineBasedOnValidation(String query, ValidationResult validation) { + return query; // 基于验证结果优化查询 + } + + class ValidationResult { + private boolean valid; + private boolean shouldRetry; + + public ValidationResult(boolean valid, boolean shouldRetry) { + this.valid = valid; + this.shouldRetry = shouldRetry; + } + + public boolean isValid() { + return valid; + } + + public boolean shouldRetry() { + return shouldRetry; + } + } + } + + HybridRAGSystem hybridRAG = new HybridRAGSystem(chatModel, vectorStore); + String answer = hybridRAG.answer("解释一下Spring AI Alibaba的核心功能"); + + System.out.println("混合RAG答案: " + answer); + System.out.println("混合RAG示例执行完成"); + } + + /** + * 运行所有示例 + */ + public void runAllExamples() { + System.out.println("=== 检索增强生成(RAG)示例 ===\n"); + + try { + System.out.println("示例1: 构建知识库"); + // example1_buildKnowledgeBase(); // 需要实际文件路径 + System.out.println(); + + System.out.println("示例2: 两步RAG"); + example2_twoStepRAG(); + System.out.println(); + + System.out.println("示例3: Agentic RAG"); + example3_agenticRAG(); + System.out.println(); + + System.out.println("示例4: 多数据源RAG"); + example4_multiSourceRAG(); + System.out.println(); + + System.out.println("示例5: 混合RAG"); + example5_hybridRAG(); + System.out.println(); + + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/WorkflowExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/WorkflowExample.java new file mode 100644 index 00000000..ab37ef83 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/WorkflowExample.java @@ -0,0 +1,667 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.NodeOutput; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.action.NodeActionWithConfig; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; +import com.alibaba.cloud.ai.graph.streaming.StreamingOutput; +import com.alibaba.cloud.ai.graph.utils.TypeRef; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.PromptTemplate; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 工作流(Workflow)示例 + * + * 演示如何使用StateGraph构建智能工作流,包括: + * 1. 定义自定义Node + * 2. Agent作为Node + * 3. 混合使用Agent Node和普通Node + * 4. 执行工作流 + * + * 参考文档: advanced_doc/workflow.md + */ +public class WorkflowExample { + + private final ChatModel chatModel; + + public WorkflowExample(ChatModel chatModel) { + this.chatModel = chatModel; + } + + /** + * Main方法:运行所有示例 + * + * 注意:需要配置ChatModel实例才能运行 + */ + public static void main(String[] args) { + // 创建 DashScope API 实例 + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 创建 ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + if (chatModel == null) { + System.err.println("错误:请先配置ChatModel实例"); + System.err.println("请设置 AI_DASHSCOPE_API_KEY 环境变量"); + return; + } + + // 创建示例实例 + WorkflowExample example = new WorkflowExample(chatModel); + + // 运行所有示例 + example.runAllExamples(); + } + + /** + * 示例1:基础Node定义 + * + * 创建简单的文本处理Node + */ + public void example1_basicNode() { + class TextProcessorNode implements NodeAction { + @Override + public Map apply(OverAllState state) throws Exception { + // 1. 从状态中获取输入 + String input = state.value("query", "").toString(); + + // 2. 执行业务逻辑 + String processedText = input.toUpperCase().trim(); + + // 3. 返回更新后的状态 + Map result = new HashMap<>(); + result.put("processed_text", processedText); + return result; + } + } + + TextProcessorNode processor = new TextProcessorNode(); + System.out.println("基础Node定义示例完成"); + } + + /** + * 示例2:带配置的AI Node + * + * 创建调用LLM的Node + */ + public void example2_aiNode() { + class QueryExpanderNode implements NodeActionWithConfig { + private final ChatClient chatClient; + private final PromptTemplate promptTemplate; + + public QueryExpanderNode(ChatClient.Builder chatClientBuilder) { + this.chatClient = chatClientBuilder.build(); + this.promptTemplate = new PromptTemplate( + "你是一个搜索优化专家。请为以下查询生成 {number} 个不同的变体。\n" + + "原始查询:{query}\n\n" + + "查询变体:\n" + ); + } + + @Override + public Map apply(OverAllState state, RunnableConfig config) throws Exception { + // 获取输入参数 + String query = state.value("query", "").toString(); + Integer number = (Integer) state.value("expanderNumber", 3); + + // 调用 LLM + String result = chatClient.prompt() + .user(user -> user + .text(promptTemplate.getTemplate()) + .param("query", query) + .param("number", number)) + .call() + .content(); + + // 处理结果 + String[] variants = result.split("\n"); + + // 返回更新的状态 + Map output = new HashMap<>(); + output.put("queryVariants", Arrays.asList(variants)); + return output; + } + } + + QueryExpanderNode expander = new QueryExpanderNode(ChatClient.builder(chatModel)); + System.out.println("AI Node示例完成"); + } + + /** + * 示例3:条件评估Node + * + * 用于工作流中的条件分支判断 + */ + public void example3_conditionNode() { + class ConditionEvaluatorNode implements NodeAction { + @Override + public Map apply(OverAllState state) throws Exception { + String input = state.value("input", "").toString().toLowerCase(); + + // 根据输入内容决定路由 + String route; + if (input.contains("错误") || input.contains("异常")) { + route = "error_handling"; + } + else if (input.contains("数据") || input.contains("分析")) { + route = "data_processing"; + } + else if (input.contains("报告") || input.contains("总结")) { + route = "report_generation"; + } + else { + route = "default"; + } + + Map result = new HashMap<>(); + result.put("_condition_result", route); + return result; + } + } + + ConditionEvaluatorNode evaluator = new ConditionEvaluatorNode(); + System.out.println("条件评估Node示例完成"); + } + + /** + * 示例4:并行结果聚合Node + * + * 用于收集和聚合并行执行的多个Node的结果 + */ + public void example4_aggregatorNode() { + class ParallelResultAggregatorNode implements NodeAction { + private final String outputKey; + + public ParallelResultAggregatorNode(String outputKey) { + this.outputKey = outputKey; + } + + @Override + public Map apply(OverAllState state) throws Exception { + // 收集所有并行任务的结果 + List results = new ArrayList<>(); + + // 假设并行任务将结果存储在不同的键中 + state.value("result_1").ifPresent(r -> results.add(r.toString())); + state.value("result_2").ifPresent(r -> results.add(r.toString())); + state.value("result_3").ifPresent(r -> results.add(r.toString())); + + // 聚合结果 + String aggregatedResult = String.join("\n---\n", results); + + Map output = new HashMap<>(); + output.put(outputKey, aggregatedResult); + return output; + } + } + + ParallelResultAggregatorNode aggregator = new ParallelResultAggregatorNode("merged_results"); + System.out.println("聚合Node示例完成"); + } + + /** + * 示例5:集成自定义Node到StateGraph + * + * 构建包含自定义Node的工作流 + */ + public void example5_buildWorkflowWithCustomNodes() throws Exception { + // 定义状态管理策略 + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("query", new ReplaceStrategy()); + strategies.put("processed_text", new ReplaceStrategy()); + strategies.put("queryVariants", new ReplaceStrategy()); + strategies.put("final_result", new ReplaceStrategy()); + return strategies; + }; + + // 创建Node实例 + class TextProcessorNode implements NodeAction { + @Override + public Map apply(OverAllState state) throws Exception { + String input = state.value("query", "").toString(); + String processed = input.toUpperCase().trim(); + return Map.of("processed_text", processed); + } + } + + class ConditionNode implements NodeAction { + @Override + public Map apply(OverAllState state) throws Exception { + String input = state.value("processed_text", "").toString(); + String route = input.length() > 10 ? "long" : "short"; + return Map.of("_condition_result", route); + } + } + + // 构建 StateGraph + StateGraph graph = new StateGraph(keyStrategyFactory); + + // 添加自定义 Node + graph.addNode("processor", node_async(new TextProcessorNode())); + graph.addNode("condition", node_async(new ConditionNode())); + + // 定义边(流程连接) + graph.addEdge(StateGraph.START, "processor"); + graph.addEdge("processor", "condition"); + + // 条件边:根据 condition node 的结果路由 + graph.addConditionalEdges( + "condition", + edge_async(state -> state.value("_condition_result", "short").toString()), + Map.of( + "long", "processor", // 长文本重新处理 + "short", StateGraph.END // 短文本结束 + ) + ); + + System.out.println("自定义Node工作流构建完成"); + } + + /** + * 示例6:Agent作为SubGraph Node + * + * 将ReactAgent嵌入到工作流中 + */ + public void example6_agentAsNode() throws Exception { + // 创建专门的数据分析 Agent + ReactAgent analysisAgent = ReactAgent.builder() + .name("data_analyzer") + .model(chatModel) + .instruction("你是一个数据分析专家,负责分析数据并提供洞察,请分析以下输入数据:\n {input}") + .outputKey("analysis_result") + .build(); + + // 创建报告生成 Agent + ReactAgent reportAgent = ReactAgent.builder() + .name("report_generator") + .model(chatModel) + .instruction("你是一个报告生成专家,负责将分析结果 “{analysis_result}” 转化为专业报告") + .outputKey("final_report") + .build(); + + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("input", new ReplaceStrategy()); + return strategies; + }; + + // 构建包含 Agent 的工作流 + StateGraph workflow = new StateGraph(keyStrategyFactory); + + // 将 Agent 作为 SubGraph Node 添加 + workflow.addNode(analysisAgent.name(), analysisAgent.asNode( + true, // includeContents: 是否传递父图的消息历史 + false)); + + workflow.addNode(reportAgent.name(), reportAgent.asNode( + true, + false)); + + // 定义流程 + workflow.addEdge(StateGraph.START, analysisAgent.name()); + workflow.addEdge(analysisAgent.name(), reportAgent.name()); + workflow.addEdge(reportAgent.name(), StateGraph.END); + + CompiledGraph compiledGraph = workflow.compile(CompileConfig.builder().build()); + NodeOutput lastOutput = compiledGraph.stream(Map.of("input", "2025年全年销量100亿,毛利率 23%,净利率 13%。2024年全年销量80亿,毛利率 20%,净利率 8%。")).doOnNext(output -> { + if (output instanceof StreamingOutput streamingOutput) { + System.out.println("Output from node " + streamingOutput.node() + ": " + streamingOutput.message().getText()); + } + }).blockLast(); + + System.out.println("\n\n最终结果,包含所有节点状态:\n" + lastOutput.state().data()); + } + + /** + * 示例7:混合使用Agent Node和普通Node + * + * 在工作流中结合Agent和自定义Node + */ + public void example7_hybridWorkflow() throws Exception { + // 创建 Agent + ReactAgent qaAgent = ReactAgent.builder() + .name("qa_agent") + .model(chatModel) + .instruction("你是一个问答专家,负责回答用户的问题:\n {cleaned_input}") + .outputKey("qa_result") + .enableLogging(true) + .build(); + + // 创建自定义 Node + class PreprocessorNode implements NodeAction { + @Override + public Map apply(OverAllState state) throws Exception { + String input = state.value("input", "").toString(); + String cleaned = input.trim().toLowerCase(); + return Map.of("cleaned_input", cleaned); + } + } + + class ValidatorNode implements NodeAction { + @Override + public Map apply(OverAllState state) throws Exception { + Message message = (Message)state.value("qa_result").get(); + boolean isValid = message.getText().length() > 50; // 简单验证 + return Map.of("is_valid", isValid); + } + } + + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("input", new ReplaceStrategy()); + strategies.put("cleaned_input", new ReplaceStrategy()); + strategies.put("qa_result", new ReplaceStrategy()); + strategies.put("is_valid", new ReplaceStrategy()); + return strategies; + }; + + // 构建混合工作流 + StateGraph workflow = new StateGraph(keyStrategyFactory); + + // 添加普通 Node + workflow.addNode("preprocess", node_async(new PreprocessorNode())); + workflow.addNode("validate", node_async(new ValidatorNode())); + + // 添加 Agent Node + workflow.addNode(qaAgent.name(), qaAgent.asNode( + true, + false)); + + // 定义流程:预处理 -> Agent处理 -> 验证 + workflow.addEdge(StateGraph.START, "preprocess"); + workflow.addEdge("preprocess", qaAgent.name()); + workflow.addEdge(qaAgent.name(), "validate"); + + // 条件边:验证通过则结束,否则重新处理 + workflow.addConditionalEdges( + "validate", + edge_async(state -> (Boolean) state.value("is_valid", false) ? "end" : qaAgent.name()), + Map.of("end", StateGraph.END, qaAgent.name(), qaAgent.name()) + ); + + CompiledGraph compiledGraph = workflow.compile(CompileConfig.builder().build()); + NodeOutput lastOutput = compiledGraph.stream(Map.of("input", "请解释量子计算的基本原理")).doOnNext(output -> { + if (output instanceof StreamingOutput streamingOutput) { + if (streamingOutput.message() != null) { + // steaming output from streaming llm node + System.out.println("Streaming output from node " + streamingOutput.node() + ": " + streamingOutput.message().getText()); + } else { + // output from normal node, investigate the state to get the node data + System.out.println("Output from node " + streamingOutput.node() + ": " + streamingOutput.state().data()); + } + } + }).blockLast(); + + System.out.println("\n\n\n最终结果,包含所有节点状态:\n" + lastOutput.state().data()); + } + + /** + * 示例8:执行工作流 + * + * 编译并执行StateGraph工作流 + */ + public void example8_executeWorkflow() throws Exception { + // 创建简单的工作流 + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("input", new ReplaceStrategy()); + strategies.put("output", new ReplaceStrategy()); + return strategies; + }; + + StateGraph workflow = new StateGraph(keyStrategyFactory); + + class SimpleNode implements NodeAction { + @Override + public Map apply(OverAllState state) throws Exception { + String input = state.value("input", "").toString(); + return Map.of("output", "Processed: " + input); + } + } + + workflow.addNode("process", node_async(new SimpleNode())); + workflow.addEdge(StateGraph.START, "process"); + workflow.addEdge("process", StateGraph.END); + + // 编译工作流 + CompileConfig compileConfig = CompileConfig.builder().build(); + CompiledGraph compiledGraph = workflow.compile(compileConfig); + + // 准备输入 + Map input = Map.of( + "input", "请分析2024年AI行业发展趋势" + ); + + // 配置运行参数 + RunnableConfig runnableConfig = RunnableConfig.builder() + .threadId("workflow-001") + .build(); + + // 执行工作流 + Optional result = compiledGraph.invoke(input, runnableConfig); + + // 处理结果 + result.ifPresent(state -> { + System.out.println("输入: " + state.value("input").orElse("无")); + System.out.println("输出: " + state.value("output").orElse("无")); + }); + + System.out.println("工作流执行完成"); + } + + /** + * 示例9:多Agent协作工作流 + * + * 构建完整的研究工作流 + */ + private static final String RESEARCH_RESULT = """ + #### 1. 引言 + AI Agent(人工智能代理)是近年来人工智能领域的重要研究方向之一。它指的是一种能够感知环境、自主决策并采取行动以实现特定目标的智能系统。随着深度学习、强化学习和自然语言处理等技术的发展,AI Agent 在多个领域展现出巨大的潜力。 + + 本报告旨在全面梳理 AI Agent 的技术发展、应用场景、典型案例以及未来趋势,为相关研究和应用提供参考。 + + --- + + #### 2. 技术发展 + + ##### 2.1 核心技术 + - **感知能力**:通过计算机视觉、语音识别和传感器数据处理,AI Agent 能够理解外部环境。 + - **决策能力**:基于强化学习、规则引擎或大模型推理,AI Agent 可以在复杂环境中做出最优决策。 + - **执行能力**:通过与物理设备(如机器人)或软件系统(如自动化工具)集成,AI Agent 实现任务执行。 + - **学习与适应**:利用在线学习和迁移学习技术,AI Agent 能够不断优化自身行为。 + + ##### 2.2 关键进展 + - **大模型驱动的 Agent**:以 LLM(大语言模型)为基础的 AI Agent 成为研究热点,例如 AutoGPT、BabyAGI 等项目展示了自主任务分解与执行的能力。 + - **多模态融合**:结合文本、图像、音频等多种输入形式,提升 Agent 的环境理解能力。 + - **人机协作**:设计更自然的人机交互机制,使 AI Agent 更好地融入人类工作流程。 + + """; + + public void example9_multiAgentResearchWorkflow() throws Exception { + // 创建工具(示例) + ToolCallback searchTool = FunctionToolCallback + .builder("search", (args) -> RESEARCH_RESULT) + .description("搜索工具") + .inputType(String.class) + .build(); + + ToolCallback analysisTool = FunctionToolCallback + .builder("analysis", (args) -> "分析结果") + .description("分析工具") + .inputType(String.class) + .build(); + + ToolCallback summaryTool = FunctionToolCallback + .builder("summary", (args) -> "总结结果") + .description("总结结果。") + .inputType(String.class) + .build(); + + // 1. 创建信息收集 Agent + ReactAgent researchAgent = ReactAgent.builder() + .name("researcher") + .model(chatModel) + .instruction("你是一个研究专家,负责收集和整理相关信息,请研究主题: {input}") + .tools(searchTool) + .outputKey("research_data") + .enableLogging(true) + .build(); + + // 2. 创建数据分析 Agent + ReactAgent analysisAgent = ReactAgent.builder() + .name("analyst") + .model(chatModel) + .instruction("你是一个分析专家,负责深入分析关于主题 “{input}” 的研究数据。数据如下: \n\n {research_data}") + .tools(analysisTool) + .outputKey("analysis_result") + .enableLogging(true) + .build(); + + // 3. 创建总结 Agent + ReactAgent summaryAgent = ReactAgent.builder() + .name("summarizer") + .model(chatModel) + .instruction("你是一个总结专家,负责将分析结果提炼为简洁的结论,结果:\n\n {analysis_result}") + .tools(summaryTool) + .outputKey("final_summary") + .enableLogging(true) + .build(); + + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("input", new ReplaceStrategy()); + return strategies; + }; + + // 4. 构建工作流 + StateGraph workflow = new StateGraph(keyStrategyFactory); + + // 添加 Agent 节点 + workflow.addNode(researchAgent.name(), researchAgent.asNode( + true, // 包含历史消息 + false // 不返回推理过程 + )); + + workflow.addNode(analysisAgent.name(), analysisAgent.asNode( + true, + false)); + + workflow.addNode(summaryAgent.name(), summaryAgent.asNode( + true, + true // 返回完整推理过程 + )); + + // 定义顺序执行流程 + workflow.addEdge(StateGraph.START, researchAgent.name()); + workflow.addEdge(researchAgent.name(), analysisAgent.name()); + workflow.addEdge(analysisAgent.name(), summaryAgent.name()); + workflow.addEdge(summaryAgent.name(), StateGraph.END); + + + CompiledGraph compiledGraph = workflow.compile(CompileConfig.builder().build()); + NodeOutput finaOutput = compiledGraph.stream(Map.of("input", "帮我做一份关于AI Agent的研究报告")).doOnNext(output -> { + if (output instanceof StreamingOutput streamingOutput) { + System.out.println("Output from node " + streamingOutput.node() + ": " + streamingOutput.message().getText()); + } + }).blockLast(); + + System.out.println("多Agent研究工作流构建完成"); + System.out.println("最终输出: " + finaOutput.state().value("final_summary").orElse("无")); + } + + /** + * 运行所有示例 + */ + public void runAllExamples() { + System.out.println("=== 工作流(Workflow)示例 ===\n"); + + try { + System.out.println("示例1: 基础Node定义"); + example1_basicNode(); + System.out.println(); + + System.out.println("示例2: 带配置的AI Node"); + example2_aiNode(); + System.out.println(); + + System.out.println("示例3: 条件评估Node"); + example3_conditionNode(); + System.out.println(); + + System.out.println("示例4: 并行结果聚合Node"); + example4_aggregatorNode(); + System.out.println(); + + System.out.println("示例5: 集成自定义Node到StateGraph"); + example5_buildWorkflowWithCustomNodes(); + System.out.println(); + + System.out.println("示例6: Agent作为SubGraph Node"); + example6_agentAsNode(); + System.out.println(); +// + System.out.println("示例7: 混合使用Agent Node和普通Node"); + example7_hybridWorkflow(); + System.out.println(); + + System.out.println("示例8: 执行工作流"); + example8_executeWorkflow(); + System.out.println(); + + System.out.println("示例9: 多Agent协作工作流"); + example9_multiAgentResearchWorkflow(); + System.out.println(); + + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/A2AAgentConfig.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/A2AAgentConfig.java new file mode 100644 index 00000000..723b8703 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/A2AAgentConfig.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced.a2a; + +import com.alibaba.cloud.ai.graph.agent.ReactAgent; + +import org.springframework.ai.chat.model.ChatModel; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 定义并暴露本地 ReactAgent + */ +@Configuration +public class A2AAgentConfig { + + @Bean(name = "dataAnalysisAgent") + public ReactAgent dataAnalysisAgent(@Qualifier("dashscopeChatModel") ChatModel chatModel) { + return ReactAgent.builder() + .name("data_analysis_agent") + .model(chatModel) + .description("专门用于数据分析和统计计算的本地智能体") + .instruction("你是一个专业的数据分析专家,擅长处理各类数据统计和分析任务。" + + "你能够理解用户的数据分析需求,提供准确的统计计算结果和专业的分析建议。") + .outputKey("messages") + .build(); + } +} diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/A2AExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/A2AExample.java new file mode 100644 index 00000000..3396fa75 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/A2AExample.java @@ -0,0 +1,138 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced.a2a; + +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.a2a.A2aRemoteAgent; +import com.alibaba.cloud.ai.graph.agent.a2a.AgentCardProvider; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; + +import org.springframework.ai.chat.model.ChatModel; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * A2A (Agent-to-Agent) 一体化示例:注册 -> 发现 -> 调用 + * + * - 启动本应用后,data_analysis_agent 将作为本地 ReactAgent 自动注册到 A2A(并根据配置注册到 Nacos) + * - 通过 AgentCardProvider 从注册中心发现该 Agent + * - 构造 A2aRemoteAgent 远程代理并完成调用 + */ +@Component +public class A2AExample { + + private final ChatModel chatModel; + private final AgentCardProvider agentCardProvider; + private final ReactAgent localDataAnalysisAgent; + + @Autowired + public A2AExample(@Qualifier("dashscopeChatModel") ChatModel chatModel, + AgentCardProvider agentCardProvider, + @Qualifier("dataAnalysisAgent") ReactAgent localDataAnalysisAgent) { + this.chatModel = chatModel; + this.agentCardProvider = agentCardProvider; + this.localDataAnalysisAgent = localDataAnalysisAgent; + } + + /** + * 运行一体化演示 + * 1) 本地 Agent 已由 Spring 容器创建并通过 A2A Server 自动暴露 + * 2) 使用 AgentCardProvider 从注册中心发现该 Agent + * 3) 构建 A2aRemoteAgent 并完成一次远程调用 + */ + public void runDemo() throws GraphRunnerException { + System.out.println("=== A2A 一体化演示:注册 -> 发现 -> 调用 ===\n"); + + // 阶段说明 + System.out.println("【架构说明】"); + System.out.println("1. Registry(注册):本地 Agent 注册到 Nacos,供其他服务发现"); + System.out.println("2. Discovery(发现):通过 AgentCardProvider 从 Nacos 查询 Agent"); + System.out.println("3. Invocation(调用):构造 A2aRemoteAgent 完成远程调用"); + System.out.println(); + + // 1) 本地直连:验证本地注册的 ReactAgent 可用 + System.out.println("【阶段1:本地直调】验证 ReactAgent Bean 功能"); + System.out.println("- Agent 名称: data_analysis_agent"); + System.out.println("- 调用方式: 直接调用 Bean"); + System.out.println("- 注册状态: 已通过 A2A Server AutoConfiguration 注册到 Nacos"); + System.out.println(); + + System.out.println("执行本地调用..."); + Optional localResult = localDataAnalysisAgent.invoke("请对上月销售数据进行趋势分析,并给出关键结论。"); + localResult.flatMap(s -> s.value("messages")).ifPresent(r -> + System.out.println("✓ 本地调用成功,结果: " + (r.toString().length() > 100 ? r.toString() + .substring(0, 100) + "..." : r))); + System.out.println(); + + // 2) 发现:通过 AgentCardProvider 从注册中心获取该 Agent 的 AgentCard + System.out.println("【阶段2:服务发现】使用 AgentCardProvider 从 Nacos 发现 Agent"); + System.out.println("- 发现机制: Nacos Discovery (spring.ai.alibaba.a2a.nacos.discovery.enabled=true)"); + System.out.println("- AgentCardProvider 类型: " + agentCardProvider.getClass().getSimpleName()); + System.out.println("- 查询 Agent: data_analysis_agent"); + System.out.println(); + + System.out.println("构建 A2aRemoteAgent..."); + A2aRemoteAgent remote = A2aRemoteAgent.builder() + .name("data_analysis_agent") + .agentCardProvider(agentCardProvider) // 从 Nacos 自动获取 AgentCard + .description("数据分析远程代理") + .build(); + System.out.println("✓ A2aRemoteAgent 构建成功,AgentCard 已从 Nacos 获取"); + System.out.println(); + + // 3) 远程调用:通过 A2aRemoteAgent 调用(即便是同进程,也模拟远程化调用路径) + System.out.println("【阶段3:远程调用】通过 A2aRemoteAgent 执行远程调用"); + System.out.println("- 调用路径: A2aRemoteAgent -> REST API (/a2a/message) -> 本地 ReactAgent"); + System.out.println("- 传输协议: JSON-RPC over HTTP"); + System.out.println(); + + System.out.println("执行远程调用..."); + Optional remoteResult = remote.invoke("请根据季度数据给出同比与环比分析概要。"); + remoteResult.flatMap(s -> s.value("output")).ifPresent(r -> + System.out.println("✓ 远程调用成功,结果: " + (r.toString().length() > 100 ? r.toString() + .substring(0, 100) + "..." : r))); + System.out.println(); + + // 验证要点 + System.out.println("【验证要点】"); + System.out.println("1. 本地 AgentCard:"); + System.out.println(" → curl http://localhost:8080/.well-known/agent.json"); + System.out.println(); + System.out.println("2. Nacos 控制台(验证注册):"); + System.out.println(" → http://localhost:8848/nacos"); + System.out.println(" → 登录 (nacos/nacos)"); + System.out.println(" → 查看 A2A 服务注册维度"); + System.out.println(); + System.out.println("3. 配置说明:"); + System.out.println(" → registry.enabled=true : 将本地 Agent 注册到 Nacos(服务提供者)"); + System.out.println(" → discovery.enabled=true : 从 Nacos 发现其他 Agent(服务消费者)"); + System.out.println(); + System.out.println("4. 其他服务调用:"); + System.out.println(" 其他服务可使用相同的方式发现并调用 data_analysis_agent:"); + System.out.println(" ```"); + System.out.println(" A2aRemoteAgent remote = A2aRemoteAgent.builder()"); + System.out.println(" .name(\"data_analysis_agent\")"); + System.out.println(" .agentCardProvider(agentCardProvider)"); + System.out.println(" .build();"); + System.out.println(" remote.invoke(\"分析请求...\");"); + System.out.println(" ```"); + } +} diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/A2AExampleController.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/A2AExampleController.java new file mode 100644 index 00000000..c67981a1 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/A2AExampleController.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced.a2a; + +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.RestController; + +import java.util.HashMap; +import java.util.Map; + +/** + * 提供 HTTP 接口来调用 A2A 示例 + */ +@RestController +@RequestMapping("/api/a2a") +public class A2AExampleController { + + private final A2AExample a2aExample; + + @Autowired + public A2AExampleController(A2AExample a2aExample) { + this.a2aExample = a2aExample; + } + + /** + * 运行统一的 A2A 演示 + * + * @return 执行结果 + */ + @GetMapping("/demo") + public Map runDemo() { + Map response = new HashMap<>(); + try { + a2aExample.runDemo(); + response.put("status", "success"); + response.put("message", "A2A 一体化演示执行完成"); + } + catch (Exception e) { + response.put("status", "error"); + response.put("message", "执行演示时出错: " + e.getMessage()); + response.put("error", e.getClass().getSimpleName()); + } + return response; + } +} diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/DocumentationApplication.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/DocumentationApplication.java new file mode 100644 index 00000000..d2296fb8 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/DocumentationApplication.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced.a2a; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Spring AI Alibaba Documentation Examples Application + * + * 本应用演示了 Spring AI Alibaba 的各种功能,包括: + * - Agent Framework 示例 + * - A2A (Agent-to-Agent) 分布式智能体示例 + * - Graph 工作流示例 + */ +@SpringBootApplication +public class DocumentationApplication { + + private static final Logger logger = LoggerFactory.getLogger(DocumentationApplication.class); + + public static void main(String[] args) { + SpringApplication.run(DocumentationApplication.class, args); + } + + // @Bean + public CommandLineRunner demoRunner(A2AExample a2aExample) { + return args -> { + logger.info("================================================="); + logger.info("Spring AI Alibaba Documentation Examples Started"); + logger.info("================================================="); + // a2aExample.runDemo(); + logger.info("Application is ready. Hit /api/a2a/demo to run the A2A demo."); + }; + } +} diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/README.md b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/README.md new file mode 100644 index 00000000..80ee3f54 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/a2a/README.md @@ -0,0 +1,194 @@ +# Spring AI Alibaba A2A 一体化演示(注册 -> 发现 -> 调用) + +本示例演示如何在一个应用中完成: +- 本地 ReactAgent 的创建与对外暴露(A2A Server 自动导出) +- **使用 Nacos Registry 进行 Agent 注册** +- 使用 Nacos Discovery 进行 Agent 发现 +- 通过 AgentCardProvider 发现并以 A2aRemoteAgent 方式调用 + +## 架构说明 + +### A2A 注册与发现流程 + +1. **本地 Agent 创建**:`A2AAgentConfig` 创建 ReactAgent Bean +2. **A2A Server 自动暴露**:Spring Boot 启动时,A2A Server AutoConfiguration 自动: + - 根据 ReactAgent Bean 生成 AgentCard + - 暴露 REST API 端点(`/.well-known/agent.json` 和 `/a2a/message`) +3. **Nacos Registry 注册**:配置 `registry.enabled: true` 后: + - 自动将 AgentCard 注册到 Nacos A2A 服务注册表 + - 其他服务可通过 Nacos 发现此 Agent +4. **Nacos Discovery 发现**:配置 `discovery.enabled: true` 后: + - AgentCardProvider 可从 Nacos 查询已注册的 Agent + - 构造 A2aRemoteAgent 进行远程调用 + +## 目录 +- `A2AAgentConfig.java`:定义本地 ReactAgent(data_analysis_agent) +- `A2AExample.java`:一体化演示入口,完成本地调用、发现、远程调用 +- `A2AExampleController.java`:提供 `/api/a2a/demo` HTTP 入口 +- `application.yml`:**A2A server、Nacos Registry 和 Nacos Discovery 配置** + +## 配置说明 + +### 关键配置项(application.yml) + +```yaml +spring: + ai: + alibaba: + a2a: + nacos: + server-addr: 127.0.0.1:8848 + username: nacos + password: nacos + discovery: + enabled: true # 启用服务发现(查询其他 Agent) + registry: + enabled: true # 启用服务注册(注册本地 Agent) + server: + version: 1.0.0 + card: + name: data_analysis_agent + description: 专门用于数据分析和统计计算的本地智能体 + provider: + name: Spring AI Alibaba Documentation + organization: Spring AI Alibaba +``` + +**重要**: +- `registry.enabled: true` - 必须启用才能将 Agent 注册到 Nacos +- `discovery.enabled: true` - 启用后才能通过 AgentCardProvider 发现其他 Agent +- `server.card` - 定义注册到 Nacos 的 AgentCard 元数据 + +## 运行 + +### 1) 准备环境 + +```bash +export DASHSCOPE_API_KEY=your-api-key +export NACOS_SERVER_ADDR=127.0.0.1:8848 +export NACOS_USERNAME=nacos +export NACOS_PASSWORD=nacos +``` + +确保本地有可用的 Nacos(可用 Docker 启动): +```bash +docker run --name nacos -d -p 8848:8848 -e MODE=standalone nacos/nacos-server:latest +``` + +### 2) 启动应用 + +```bash +mvn -q -pl examples/documentation -am spring-boot:run +``` + +### 3) 访问演示接口 + +```bash +curl http://localhost:8080/api/a2a/demo +``` + +### 4) 验证注册和发现 + +**本地 AgentCard**: +```bash +curl http://localhost:8080/.well-known/agent.json +``` + +**Nacos 控制台**: +- 打开 http://localhost:8848/nacos +- 登录(nacos/nacos) +- 查看 A2A 服务注册维度,应该能看到 `data_analysis_agent` + +**通过 API 查询 Nacos 中的 Agent**: +```bash +# 需要 Nacos A2A API(如果启用) +curl "http://localhost:8848/nacos/v1/ai/a2a/agent?agentName=data_analysis_agent" +``` + +## 关键代码 + +### 注册逻辑(自动) + +1. **定义 ReactAgent Bean**(`A2AAgentConfig.java`): +```java +@Bean(name = "dataAnalysisAgent") +public ReactAgent dataAnalysisAgent(@Qualifier("dashscopeChatModel") ChatModel chatModel) { + return ReactAgent.builder() + .name("data_analysis_agent") + .model(chatModel) + .description("专门用于数据分析和统计计算的本地智能体") + .instruction("你是一个专业的数据分析专家...") + .outputKey("messages") + .build(); +} +``` + +2. **配置 A2A Server 和 Nacos Registry**(`application.yml`): + - `spring.ai.alibaba.a2a.server.card` - 定义 AgentCard + - `spring.ai.alibaba.a2a.nacos.registry.enabled: true` - 启用注册 + +3. **自动注册流程**: + - Spring Boot 启动 → A2A Server AutoConfiguration 检测到 ReactAgent Bean + - 生成 AgentCard → 注册到 Nacos A2A Registry + - 暴露 REST API 端点 + +### 发现与调用逻辑(`A2AExample.java`) + +```java +// 1. 通过 AgentCardProvider 发现 Agent +A2aRemoteAgent remote = A2aRemoteAgent.builder() + .name("data_analysis_agent") + .agentCardProvider(agentCardProvider) // 自动从 Nacos 获取 AgentCard + .description("数据分析远程代理") + .build(); + +// 2. 远程调用 +Optional result = remote.invoke("请根据季度数据给出同比与环比分析概要。"); +``` + +## 注册与发现的区别 + +| 功能 | 配置项 | 作用 | 本示例中的角色 | +|------|--------|------|----------------| +| **Registry(注册)** | `registry.enabled: true` | 将本地 Agent 注册到 Nacos | 本应用作为 **服务提供者** | +| **Discovery(发现)** | `discovery.enabled: true` | 从 Nacos 查询其他 Agent | 本应用作为 **服务消费者** | + +本示例同时启用了两者,因此: +- 作为**提供者**:注册 `data_analysis_agent` 到 Nacos +- 作为**消费者**:可发现并调用其他已注册的 Agent(包括自己) + +## 注意事项 + +1. **依赖要求**: + - 需要添加 `spring-ai-alibaba-starter-a2a-nacos` 依赖 + - 确保 Nacos 服务正常运行 + +2. **Registry vs Discovery**: + - `registry.enabled: true` - 注册本地 Agent + - `discovery.enabled: true` - 发现远程 Agent + - 两者可独立配置,也可同时启用 + +3. **多 Agent 注册**: + - 默认情况下,只有一个 Agent Bean 会被注册 + - 如需注册多个 Agent,需运行多个应用实例,每个实例配置不同的 Agent + +4. **AgentCard 元数据**: + - `server.card.name` 必须与 ReactAgent Bean 的 `name` 一致 + - `server.card.provider` 可选,用于标识 Agent 提供者信息 + +## 故障排查 + +### Agent 没有注册到 Nacos +- 检查 `registry.enabled: true` 是否配置 +- 查看应用日志,确认 Nacos Registry AutoConfiguration 是否生效 +- 验证 Nacos 连接配置(server-addr、username、password) + +### AgentCardProvider 无法发现 Agent +- 检查 `discovery.enabled: true` 是否配置 +- 确认 Agent 已成功注册到 Nacos +- 验证 agent name 是否匹配 + +### 远程调用失败 +- 确认目标 Agent 的 REST API 端点可访问 +- 检查网络连接和防火墙配置 +- 查看 A2A 消息传输日志 diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/toolselection/README.md b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/toolselection/README.md new file mode 100644 index 00000000..b8a4e024 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/toolselection/README.md @@ -0,0 +1,275 @@ +# ToolSelectionInterceptor 工具选择拦截器使用指南 + +## 概述 + +`ToolSelectionInterceptor` 是 Spring AI Alibaba Agent Framework 中的一个模型拦截器,用于在 Agent 拥有大量工具时,通过 LLM 智能选择最相关的工具。这可以减少 token 使用量,并帮助主模型专注于正确的工具。 + +### 增强功能:工具描述支持 + +在最新版本中,`ToolSelectionInterceptor` 增加了对**工具描述**的支持。当进行工具选择时,不仅会传递工具名称,还会传递工具描述给选择模型,从而大幅提高工具选择的准确性。 + +**改进前(仅工具名称):** +``` +- searchProducts +- getOrderDetails +- updateInventory +``` + +**改进后(包含工具描述):** +``` +- searchProducts: 按名称、类别或价格范围搜索产品 +- getOrderDetails: 根据订单ID获取订单详情 +- updateInventory: 更新产品库存数量 +``` + +--- + +## 快速开始 + +### 1. 基本用法 + +```java +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.interceptor.toolselection.ToolSelectionInterceptor; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.ToolCallback; + +// 创建工具选择拦截器 +ToolSelectionInterceptor toolSelectionInterceptor = ToolSelectionInterceptor.builder() + .selectionModel(chatModel) // 用于选择工具的模型 + .maxTools(3) // 每次最多选择3个工具 + .build(); + +// 创建 ReactAgent 并配置拦截器 +ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .tools(weatherTool, ticketTool, hotelTool, mapTool, translatorTool) + .interceptors(toolSelectionInterceptor) + .build(); + +// 调用 Agent +Optional result = agent.invoke("帮我查询北京的天气"); +``` + +### 2. 使用 alwaysInclude 确保关键工具始终可用 + +```java +ToolSelectionInterceptor interceptor = ToolSelectionInterceptor.builder() + .selectionModel(chatModel) + .maxTools(3) + .alwaysInclude("error_handler", "logging_tool") // 这些工具始终包含 + .build(); +``` + +### 3. 自定义系统提示词 + +```java +ToolSelectionInterceptor interceptor = ToolSelectionInterceptor.builder() + .selectionModel(chatModel) + .maxTools(3) + .systemPrompt("你是一个工具选择专家,请根据用户查询选择最相关的工具。优先选择能直接解决用户问题的工具。") + .build(); +``` + +--- + +## 工具定义最佳实践 + +为了让 `ToolSelectionInterceptor` 更准确地选择工具,请遵循以下最佳实践: + +### 1. 清晰的工具名称 + +```java +// 好的命名 +@Tool(name = "searchProducts") +@Tool(name = "getUserProfile") +@Tool(name = "sendEmailNotification") + +// 不好的命名 +@Tool(name = "search") // 太模糊 +@Tool(name = "tool1") // 无意义 +@Tool(name = "doSomething") // 不明确 +``` + +### 2. 详细的工具描述 + +```java +// 好的描述 +@Tool(name = "searchProducts", + description = "按名称、类别或价格范围搜索产品。" + + "返回匹配产品的列表,包含名称、价格和库存信息。" + + "适用于用户想要查找或浏览产品的场景。") + +// 不好的描述 +@Tool(name = "search", description = "搜索") // 太简短,缺乏上下文 +``` + +### 3. 参数描述 + +```java +@Tool(name = "searchProducts", + description = "按条件搜索产品") +public List searchProducts( + @ToolParam(description = "产品名称关键词,支持模糊匹配") String keyword, + @ToolParam(description = "产品类别,如'电子产品'、'服装'") String category, + @ToolParam(description = "价格范围上限") Double maxPrice) { + // implementation +} +``` + +--- + +## 配置参数说明 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `selectionModel` | ChatModel | 是 | 用于选择工具的 LLM 模型 | +| `maxTools` | Integer | 否 | 每次最多选择的工具数量。如果工具总数 <= maxTools,则跳过选择 | +| `systemPrompt` | String | 否 | 自定义系统提示词,默认为 "Your goal is to select the most relevant tools for answering the user's query." | +| `alwaysInclude` | Set | 否 | 始终包含的工具名称列表,不受 maxTools 限制 | + +--- + +## 工作原理 + +### 数据流程 + +``` +1. Agent 接收用户查询 + ↓ +2. AgentLlmNode 提取所有工具的名称和描述 + ↓ +3. 构建 ModelRequest,包含: + - tools: List (工具名称列表) + - toolDescriptions: Map (工具名称→描述映射) + ↓ +4. ToolSelectionInterceptor 检查工具数量 + - 如果 tools.size() <= maxTools,跳过选择,直接传递 + - 否则,调用选择模型 + ↓ +5. 构建选择提示词,格式为: + "- toolName1: description1 + - toolName2: description2 + ..." + ↓ +6. 选择模型返回 JSON: {"tools": ["tool1", "tool2"]} + ↓ +7. 过滤工具列表,只保留选中的工具 + ↓ +8. 将过滤后的工具传递给主模型 +``` + +### 选择逻辑 + +```java +// 伪代码 +if (availableTools.size() <= maxTools) { + // 工具数量不超过限制,跳过选择 + return handler.call(request); +} + +// 构建工具列表(包含描述) +StringBuilder toolList = new StringBuilder(); +for (String toolName : toolNames) { + toolList.append("- ").append(toolName); + String description = toolDescriptions.get(toolName); + if (description != null && !description.isEmpty()) { + toolList.append(": ").append(description); + } + toolList.append("\n"); +} + +// 调用选择模型 +Set selectedTools = selectTools(toolList, userQuery); + +// 过滤并传递 +ModelRequest filteredRequest = ModelRequest.builder(request) + .tools(selectedTools) + .build(); +return handler.call(filteredRequest); +``` + +--- + +## 常见问题 + +### Q1: 什么时候应该使用 ToolSelectionInterceptor? + +当你的 Agent 有超过 5-10 个工具时,建议使用此拦截器。过多的工具会: +- 增加 token 消耗 +- 降低主模型选择正确工具的准确性 +- 增加响应延迟 + +### Q2: selectionModel 应该用什么模型? + +建议使用轻量级、快速的模型(如 qwen-turbo),因为工具选择任务相对简单,不需要最强大的模型。 + +### Q3: maxTools 应该设置多少? + +- 简单任务:2-3 个 +- 中等复杂任务:3-5 个 +- 复杂任务:5-7 个 + +### Q4: 工具描述为空会怎样? + +如果某个工具没有描述,只会显示工具名称,不会导致错误。但建议为所有工具添加描述以提高选择准确性。 + +### Q5: 如何调试工具选择? + +启用 DEBUG 日志级别,可以看到选择了哪些工具: + +```properties +logging.level.com.alibaba.cloud.ai.graph.agent.interceptor.toolselection=DEBUG +``` + +日志示例: +``` +INFO ToolSelectionInterceptor - Selected 2 tools from 5 available: [weather_tool, map_tool] +``` + +--- + +## 目录 + +- `ToolSelectionExample.java`:完整示例代码,包含基础用法、alwaysInclude、自定义提示词、多工具场景 + +--- + +## 运行示例 + +### 1) 准备环境 + +```bash +export AI_DASHSCOPE_API_KEY=your-api-key +``` + +### 2) 运行示例 + +```bash +mvn -q -pl examples/documentation -am exec:java \ + -Dexec.mainClass="com.alibaba.cloud.ai.examples.documentation.framework.advanced.toolselection.ToolSelectionExample" +``` + +--- + +## 相关类 + +| 类名 | 路径 | 说明 | +|------|------|------| +| `ToolSelectionInterceptor` | `agent-framework/.../interceptor/toolselection/ToolSelectionInterceptor.java` | 工具选择拦截器 | +| `ModelRequest` | `agent-framework/.../interceptor/ModelRequest.java` | 模型请求对象,包含工具名称和描述 | +| `ModelInterceptor` | `agent-framework/.../interceptor/ModelInterceptor.java` | 拦截器基类 | +| `AgentLlmNode` | `agent-framework/.../node/AgentLlmNode.java` | LLM 节点,负责提取工具信息 | + +--- + +## 版本历史 + +| 版本 | 变更 | +|------|------| +| 1.1.0.0-RC2 | 新增 `toolDescriptions` 支持,提高工具选择准确性 | + +--- + +*文档更新日期: 2025-12-17* diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/toolselection/ToolSelectionExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/toolselection/ToolSelectionExample.java new file mode 100644 index 00000000..3c1fc926 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/advanced/toolselection/ToolSelectionExample.java @@ -0,0 +1,366 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.advanced.toolselection; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.interceptor.toolselection.ToolSelectionInterceptor; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; + +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; + +import java.util.List; +import java.util.Optional; + +/** + * ToolSelectionInterceptor 示例 + * + * 本示例演示如何使用 ToolSelectionInterceptor 进行智能工具选择。 + * + * 核心功能: + * 1. 当 Agent 有多个工具时,使用 LLM 智能选择最相关的工具 + * 2. 工具描述会自动传递给选择模型,提高选择准确性 + * 3. 可配置 maxTools 限制每次选择的工具数量 + * 4. 支持 alwaysInclude 确保关键工具始终可用 + * + * 使用场景: + * - Agent 拥有大量工具(>5个),需要减少 token 消耗 + * - 需要提高工具选择的准确性 + * - 不同查询需要不同的工具子集 + */ +public class ToolSelectionExample { + + // ==================== 示例1:基础用法 ==================== + + /** + * 基础用法:创建带有工具选择的 Agent + */ + public static void basicToolSelection() throws Exception { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建工具类实例 + TravelTools travelTools = new TravelTools(); + + // 创建 ToolSelectionInterceptor + // 当工具数量超过 maxTools 时,会使用 LLM 选择最相关的工具 + ToolSelectionInterceptor interceptor = ToolSelectionInterceptor.builder() + .selectionModel(chatModel) // 用于选择工具的模型 + .maxTools(3) // 最多选择3个工具 + .build(); + + // 创建 Agent + ReactAgent agent = ReactAgent.builder() + .name("travel_assistant") + .model(chatModel) + .methodTools(travelTools) // 自动扫描 @Tool 注解的方法 + .interceptors(interceptor) + .saver(new MemorySaver()) + .build(); + + // 调用 Agent - 会自动选择最相关的工具 + Optional result = agent.invoke("北京今天天气怎么样?"); + printResult(result, "基础用法"); + } + + // ==================== 示例2:使用 alwaysInclude ==================== + + /** + * 高级用法:使用 alwaysInclude 确保关键工具始终可用 + */ + public static void toolSelectionWithAlwaysInclude() throws Exception { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + TravelTools travelTools = new TravelTools(); + + // 使用 alwaysInclude 确保某些工具始终可用 + ToolSelectionInterceptor interceptor = ToolSelectionInterceptor.builder() + .selectionModel(chatModel) + .maxTools(2) + .alwaysInclude("get_weather") // 天气工具始终包含 + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("travel_assistant") + .model(chatModel) + .methodTools(travelTools) + .interceptors(interceptor) + .saver(new MemorySaver()) + .build(); + + // 即使查询与天气无关,weather 工具也会被包含 + Optional result = agent.invoke("帮我预订一张去上海的机票"); + printResult(result, "alwaysInclude 示例"); + } + + // ==================== 示例3:自定义系统提示词 ==================== + + /** + * 高级用法:自定义工具选择的系统提示词 + */ + public static void toolSelectionWithCustomPrompt() throws Exception { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + TravelTools travelTools = new TravelTools(); + + // 自定义选择逻辑的系统提示词 + String customPrompt = """ + 你是一个旅行助手的工具选择器。 + 根据用户的查询,选择最相关的工具来帮助回答问题。 + + 选择原则: + 1. 优先选择能直接解决用户问题的工具 + 2. 如果用户询问多个方面,选择覆盖所有方面的工具 + 3. 避免选择明显不相关的工具 + """; + + ToolSelectionInterceptor interceptor = ToolSelectionInterceptor.builder() + .selectionModel(chatModel) + .maxTools(3) + .systemPrompt(customPrompt) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("travel_assistant") + .model(chatModel) + .methodTools(travelTools) + .interceptors(interceptor) + .saver(new MemorySaver()) + .build(); + + Optional result = agent.invoke("我下周要去杭州旅游,帮我看看天气和景点"); + printResult(result, "自定义提示词示例"); + } + + // ==================== 示例4:多工具场景 ==================== + + /** + * 复杂场景:拥有多个工具的 Agent + */ + public static void multiToolScenario() throws Exception { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建多个工具类 + TravelTools travelTools = new TravelTools(); + UtilityTools utilityTools = new UtilityTools(); + + // 配置工具选择 + ToolSelectionInterceptor interceptor = ToolSelectionInterceptor.builder() + .selectionModel(chatModel) + .maxTools(3) // 从8+个工具中选择3个 + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("smart_assistant") + .model(chatModel) + .methodTools(travelTools, utilityTools) // 注册多个工具类 + .interceptors(interceptor) + .saver(new MemorySaver()) + .build(); + + // 测试不同的查询 + System.out.println("\n--- 测试1:天气查询 ---"); + Optional result1 = agent.invoke("北京今天天气如何?"); + printResult(result1, "天气查询"); + + System.out.println("\n--- 测试2:机票查询 ---"); + Optional result2 = agent.invoke("查一下明天从上海到北京的机票"); + printResult(result2, "机票查询"); + + System.out.println("\n--- 测试3:货币转换 ---"); + Optional result3 = agent.invoke("100美元能换多少人民币?"); + printResult(result3, "货币转换"); + + System.out.println("\n--- 测试4:复合查询 ---"); + Optional result4 = agent.invoke("我要去杭州旅游,帮我看看天气、推荐景点、再找个酒店"); + printResult(result4, "复合查询"); + } + + // ==================== 工具类定义 ==================== + + /** + * 旅行相关工具 + * + * 注意:工具描述要详细、准确,这样 ToolSelectionInterceptor 才能做出正确选择 + */ + public static class TravelTools { + + @Tool(name = "get_weather", + description = "获取指定城市的实时天气信息,包括温度、湿度、天气状况和空气质量。" + + "当用户询问某个城市的天气时使用此工具。") + public String getWeather( + @ToolParam(description = "城市名称,如:北京、上海、广州") String city) { + return String.format("%s今日天气:晴,温度 18-25°C,湿度 45%%,空气质量良好。", city); + } + + @Tool(name = "search_flights", + description = "搜索两个城市之间的航班信息,返回航班号、出发时间、到达时间和票价。" + + "当用户想要查询或预订机票时使用此工具。") + public String searchFlights( + @ToolParam(description = "出发城市") String from, + @ToolParam(description = "到达城市") String to, + @ToolParam(description = "出发日期,格式:YYYY-MM-DD") String date) { + return String.format("找到 %s 到 %s 的航班(%s):\n" + + "1. CA1234 08:00-10:30 ¥680\n" + + "2. MU5678 12:00-14:30 ¥720\n" + + "3. CZ9012 18:00-20:30 ¥650", from, to, date); + } + + @Tool(name = "search_hotels", + description = "搜索指定城市的酒店,可按入住日期和价格范围筛选。" + + "当用户想要预订住宿时使用此工具。") + public String searchHotels( + @ToolParam(description = "城市名称") String city, + @ToolParam(description = "入住日期,格式:YYYY-MM-DD") String arrivalDate) { + return String.format("%s 酒店推荐(%s 入住):\n" + + "1. 希尔顿酒店 ★★★★★ ¥800/晚\n" + + "2. 如家酒店 ★★★ ¥280/晚\n" + + "3. 民宿小院 ★★★★ ¥450/晚", city, arrivalDate); + } + + @Tool(name = "get_attractions", + description = "获取指定城市的热门旅游景点列表,包括景点介绍、门票价格和推荐游览时间。" + + "当用户想要了解旅游目的地的景点时使用此工具。") + public String getAttractions( + @ToolParam(description = "城市名称") String city) { + return String.format("%s 热门景点:\n" + + "1. 西湖 - 免费,建议游览半天\n" + + "2. 灵隐寺 - 门票¥45,上香另付\n" + + "3. 宋城 - 门票¥300,含演出", city); + } + + @Tool(name = "search_restaurants", + description = "搜索指定城市的餐厅,可按菜系和价格范围筛选。" + + "当用户想要找地方吃饭或了解当地美食时使用此工具。") + public String searchRestaurants( + @ToolParam(description = "城市名称") String city, + @ToolParam(description = "菜系类型,如:火锅、川菜、粤菜等") String cuisine) { + return String.format("%s %s 餐厅推荐:\n" + + "1. 老字号餐厅 - 人均¥80 评分4.8\n" + + "2. 网红打卡店 - 人均¥120 评分4.5\n" + + "3. 本地特色馆 - 人均¥60 评分4.7", city, cuisine); + } + } + + /** + * 实用工具类 + */ + public static class UtilityTools { + + @Tool(name = "convert_currency", + description = "货币汇率转换,支持多种货币之间的转换(如 USD、EUR、CNY、JPY)。" + + "当用户需要了解汇率或进行货币换算时使用此工具。") + public String convertCurrency( + @ToolParam(description = "金额") double amount, + @ToolParam(description = "源货币代码,如 USD, EUR, CNY") String from, + @ToolParam(description = "目标货币代码") String to) { + double rate = 7.2; // 简化的汇率 + if ("USD".equals(from) && "CNY".equals(to)) { + return String.format("%.2f 美元 = %.2f 人民币(汇率: 1 USD = %.2f CNY)", + amount, amount * rate, rate); + } + return String.format("%.2f %s = %.2f %s", amount, from, amount, to); + } + + @Tool(name = "translate_text", + description = "文本翻译服务,支持中英日韩等多种语言互译。" + + "当用户需要翻译文字或了解外语含义时使用此工具。") + public String translateText( + @ToolParam(description = "要翻译的文本") String text, + @ToolParam(description = "目标语言:中文、英文、日文、韩文") String targetLang) { + return String.format("翻译结果(%s):[翻译后的内容]", targetLang); + } + + @Tool(name = "calculate", + description = "数学计算器,支持加减乘除、幂运算、百分比等计算。" + + "当用户需要进行数学计算时使用此工具。") + public String calculate( + @ToolParam(description = "数学表达式,如:100*1.1、50+30") String expression) { + return "计算结果:" + expression + " = [结果]"; + } + } + + // ==================== 辅助方法 ==================== + + private static void printResult(Optional result, String testName) { + System.out.println("[" + testName + "] 执行结果:"); + result.ifPresent(state -> { + List messages = state.value("messages", List.of()); + for (Message msg : messages) { + if (msg instanceof AssistantMessage) { + System.out.println("助手: " + msg.getText()); + } + } + }); + } + + // ==================== Main 方法 ==================== + + public static void main(String[] args) { + System.out.println("=== ToolSelectionInterceptor 示例 ==="); + System.out.println("注意:需要设置 AI_DASHSCOPE_API_KEY 环境变量\n"); + + try { + System.out.println("\n--- 示例1:基础用法 ---"); + basicToolSelection(); + + System.out.println("\n--- 示例2:使用 alwaysInclude ---"); + toolSelectionWithAlwaysInclude(); + + System.out.println("\n--- 示例3:自定义系统提示词 ---"); + toolSelectionWithCustomPrompt(); + + System.out.println("\n--- 示例4:多工具场景 ---"); + multiToolScenario(); + + System.out.println("\n=== 所有示例执行完成 ==="); + } + catch (Exception e) { + System.err.println("执行示例时发生错误: " + e.getMessage()); + e.printStackTrace(); + } + } + +} diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/AgentsExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/AgentsExample.java new file mode 100644 index 00000000..4e029a5b --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/AgentsExample.java @@ -0,0 +1,840 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.tutorials; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import com.alibaba.cloud.ai.graph.NodeOutput; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.hook.AgentHook; +import com.alibaba.cloud.ai.graph.agent.hook.HookPosition; +import com.alibaba.cloud.ai.graph.agent.hook.HookPositions; +import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand; +import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook; +import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy; +import com.alibaba.cloud.ai.graph.agent.interceptor.*; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.converter.BeanOutputConverter; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.ai.tool.function.FunctionToolCallback; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; + +/** + * Agents Tutorial - agents.md + */ +public class AgentsExample { + + // ==================== 基础模型配置 ==================== + + /** + * 示例1:基础模型配置 + */ + public static void basicModelConfiguration() { + // 创建 DashScope API 实例 + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 创建 ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建 Agent + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .build(); + } + + /** + * 示例2:高级模型配置 + */ + public static void advancedModelConfiguration() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(DashScopeChatOptions.builder() + .temperature(0.7) // 控制随机性 + .maxToken(2000) // 最大输出长度 + .topP(0.9) // 核采样参数 + .enableThinking(true) + .build()) + .build(); + } + + // ==================== 工具定义 ==================== + + public static void toolUsage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建工具回调 + ToolCallback searchTool = FunctionToolCallback + .builder("search", new SearchTool()) + .description("搜索信息的工具") + .inputType(String.class) + .build(); + + // 使用多个工具 + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .tools(searchTool) + .build(); + } + + /** + * 示例5:基础 System Prompt + */ + public static void basicSystemPrompt() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .systemPrompt("你是一个专业的技术助手。请准确、简洁地回答问题。") + .build(); + } + + /** + * 示例6:使用 instruction + */ + public static void instructionUsage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + String instruction = """ + 你是一个经验丰富的软件架构师。 + + 在回答问题时,请: + 1. 首先理解用户的核心需求 + 2. 分析可能的技术方案 + 3. 提供清晰的建议和理由 + 4. 如果需要更多信息,主动询问 + + 保持专业、友好的语气。 + """; + + ReactAgent agent = ReactAgent.builder() + .name("architect_agent") + .model(chatModel) + .instruction(instruction) + .build(); + } + + // ==================== System Prompt ==================== + + public static void dynamicSystemPrompt() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("adaptive_agent") + .model(chatModel) + .interceptors(new DynamicPromptInterceptor()) + .build(); + } + + /** + * 示例8:基础调用 + */ + public static void basicInvocation() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .build(); + + // 字符串输入 + AssistantMessage response = agent.call("杭州的天气怎么样?"); + System.out.println(response.getText()); + + // UserMessage 输入 + UserMessage userMessage = new UserMessage("帮我分析这个问题"); + AssistantMessage response2 = agent.call(userMessage); + + // 多个消息 + List messages = List.of( + new UserMessage("我想了解 Java 多线程"), + new UserMessage("特别是线程池的使用") + ); + AssistantMessage response3 = agent.call(messages); + } + + /** + * 示例9:获取完整状态 + */ + public static void getFullState() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .build(); + + Optional result = agent.invoke("帮我写一首诗"); + + if (result.isPresent()) { + OverAllState state = result.get(); + + // 访问消息历史 + Optional messages = state.value("messages"); + List messageList = (List) messages.get(); + + // 访问自定义状态 + Optional customData = state.value("custom_key"); + + System.out.println("完整状态:" + state); + } + } + + /** + * 示例10:使用配置 + */ + public static void useConfiguration() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .build(); + + String threadId = "thread_123"; + RunnableConfig runnableConfig = RunnableConfig.builder() + .threadId(threadId) + .addMetadata("key", "value") + .build(); + + AssistantMessage response = agent.call("你的问题", runnableConfig); + } + + // ==================== 调用 Agent ==================== + + /** + * 示例10.1:流式调用 - 基础用法 + */ + public static void basicStreamInvocation() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("streaming_agent") + .model(chatModel) + .build(); + + // 流式输出 + Flux stream = agent.stream("帮我写一首关于春天的诗"); + + stream.subscribe( + output -> { + // 处理每个节点输出 + System.out.println("节点: " + output.node()); + System.out.println("Agent: " + output.agent()); + if (output.tokenUsage() != null) { + System.out.println("Token使用: " + output.tokenUsage()); + } + }, + error -> System.err.println("错误: " + error.getMessage()), + () -> System.out.println("流式输出完成") + ); + } + + /** + * 示例10.2:流式调用 - 高级用法 + */ + public static void advancedStreamInvocation() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("streaming_agent") + .model(chatModel) + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("stream_thread_1") + .build(); + + // 使用配置的流式调用 + Flux stream = agent.stream(new UserMessage("解释一下量子计算"), config); + + // 使用 doOnNext 处理中间输出 + stream.doOnNext(output -> { + if (!output.isSTART() && !output.isEND()) { + System.out.println("处理中..."); + System.out.println("当前节点: " + output.node()); + } + }) + .doOnComplete(() -> System.out.println("所有节点处理完成")) + .doOnError(e -> System.err.println("流处理错误: " + e.getMessage())) + .blockLast(); // 阻塞等待完成 + } + + /** + * 示例10.3:流式调用 - 收集所有输出 + */ + public static void collectStreamOutputs() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("streaming_agent") + .model(chatModel) + .build(); + + Flux stream = agent.stream("分析机器学习的应用场景"); + + // 收集所有输出 + List outputs = stream.collectList().block(); + + if (outputs != null) { + System.out.println("总共收到 " + outputs.size() + " 个节点输出"); + + // 获取最终输出 + NodeOutput lastOutput = outputs.get(outputs.size() - 1); + System.out.println("最终状态: " + lastOutput.state()); + + // 获取消息 + Optional messages = lastOutput.state().value("messages"); + if (messages.isPresent()) { + List messageList = (List) messages.get(); + Message lastMessage = messageList.get(messageList.size() - 1); + if (lastMessage instanceof AssistantMessage assistantMsg) { + System.out.println("最终回复: " + assistantMsg.getText()); + } + } + } + } + + public static void structuredOutputWithType() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("poem_agent") + .model(chatModel) + .outputType(PoemOutput.class) + .saver(new MemorySaver()) + .build(); + + AssistantMessage response = agent.call("写一首关于春天的诗"); + // 输出会遵循 PoemOutput 的结构 + System.out.println(response.getText()); + } + + /** + * 示例12:使用 outputSchema + */ + public static void structuredOutputWithSchema() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // Use BeanOutputConverter to generate outputSchema + BeanOutputConverter outputConverter = new BeanOutputConverter<>(TextAnalysisResult.class); + String format = outputConverter.getFormat(); + + ReactAgent agent = ReactAgent.builder() + .name("analysis_agent") + .model(chatModel) + .outputSchema(format) + .saver(new MemorySaver()) + .build(); + + AssistantMessage response = agent.call("分析这段文本:春天来了,万物复苏。"); + } + + /** + * 示例13:配置记忆 + */ + public static void configureMemory() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 配置内存存储 + ReactAgent agent = ReactAgent.builder() + .name("chat_agent") + .model(chatModel) + .saver(new MemorySaver()) + .build(); + + // 使用 thread_id 维护对话上下文 + RunnableConfig config = RunnableConfig.builder() + .threadId("user_123") + .build(); + + agent.call("我叫张三", config); + agent.call("我叫什么名字?", config); // 输出: "你叫张三" + } + + // ==================== 结构化输出 ==================== + + public static void main(String[] args) { + System.out.println("=== Agents Tutorial Examples ==="); + System.out.println("注意:需要设置 AI_DASHSCOPE_API_KEY 环境变量\n"); + + try { + // System.out.println("\n--- 示例1:基础模型配置 ---"); + // basicModelConfiguration(); + // + // System.out.println("\n--- 示例2:高级模型配置 ---"); + // advancedModelConfiguration(); + // + // System.out.println("\n--- 示例3:工具使用 ---"); + // toolUsage(); + // + // System.out.println("\n--- 示例5:基础 System Prompt ---"); + // basicSystemPrompt(); + // + // System.out.println("\n--- 示例6:使用 instruction ---"); + // instructionUsage(); + // + // System.out.println("\n--- 示例7:动态 System Prompt ---"); + // dynamicSystemPrompt(); + // + // System.out.println("\n--- 示例8:基础调用 ---"); + // basicInvocation(); + // + // System.out.println("\n--- 示例9:获取完整状态 ---"); + // getFullState(); + // + // System.out.println("\n--- 示例10:使用配置 ---"); + useConfiguration(); + // + // System.out.println("\n--- 示例10.1:流式调用 - 基础用法 ---"); + // basicStreamInvocation(); + // + // System.out.println("\n--- 示例10.2:流式调用 - 高级用法 ---"); + // advancedStreamInvocation(); + // + // System.out.println("\n--- 示例10.3:流式调用 - 收集所有输出 ---"); + // collectStreamOutputs(); + // + // System.out.println("\n--- 示例11:使用 outputType ---"); + // structuredOutputWithType(); + // + // System.out.println("\n--- 示例12:使用 outputSchema ---"); + // structuredOutputWithSchema(); + // + // System.out.println("\n--- 示例13:配置记忆 ---"); + // configureMemory(); + + System.out.println("\n=== 所有示例执行完成 ==="); + } + catch (Exception e) { + System.err.println("发生未预期的错误: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 示例3:定义和使用工具 + */ + public static class SearchTool implements BiFunction { + @Override + public String apply( + @ToolParam(description = "搜索关键词") String query, + ToolContext toolContext) { + return "搜索结果:" + query; + } + } + + /** + * 示例4:工具错误处理 + */ + public static class ToolErrorInterceptor extends ToolInterceptor { + @Override + public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandler handler) { + try { + return handler.call(request); + } + catch (Exception e) { + return ToolCallResponse.of(request.getToolCallId(), request.getToolName(), + "Tool failed: " + e.getMessage()); + } + } + + @Override + public String getName() { + return "ToolErrorInterceptor"; + } + } + + // ==================== Memory ==================== + + /** + * 示例7:动态 System Prompt + */ + public static class DynamicPromptInterceptor extends ModelInterceptor { + @Override + public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) { + // 基于上下文动态调整 system prompt + Map context = request.getContext(); + + // 根据上下文构建动态提示词 + String dynamicPrompt = buildDynamicPrompt(context); + + // 增强 system message + SystemMessage enhancedSystemMessage; + if (request.getSystemMessage() == null) { + enhancedSystemMessage = new SystemMessage(dynamicPrompt); + } + else { + enhancedSystemMessage = new SystemMessage( + request.getSystemMessage().getText() + "\n\n" + dynamicPrompt + ); + } + + // 创建增强的请求 + ModelRequest modifiedRequest = ModelRequest.builder(request) + .systemMessage(enhancedSystemMessage) + .build(); + + return handler.call(modifiedRequest); + } + + private String buildDynamicPrompt(Map context) { + // 示例:根据用户角色动态生成提示词 + String userRole = (String) context.getOrDefault("user_role", "default"); + + return switch (userRole) { + case "expert" -> """ + 你正在与技术专家对话。 + - 使用专业术语 + - 深入技术细节 + - 提供高级建议 + """; + case "beginner" -> """ + 你正在与初学者对话。 + - 使用简单易懂的语言 + - 详细解释概念 + - 提供入门级建议 + """; + default -> """ + 你是一个专业的助手。 + - 根据问题复杂度调整回答 + - 保持友好和专业 + """; + }; + } + + @Override + public String getName() { + return "DynamicPromptInterceptor"; + } + } + + // ==================== Hooks ==================== + + /** + * 示例11:使用 outputType + */ + public static class PoemOutput { + private String title; + private String content; + private String style; + + // Getters and Setters + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getStyle() { + return style; + } + + public void setStyle(String style) { + this.style = style; + } + } + + /** + * 示例12:文本分析结果输出类 + */ + public static class TextAnalysisResult { + private String summary; + private List keywords; + private String sentiment; + private Double confidence; + + // Getters and Setters + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public List getKeywords() { + return keywords; + } + + public void setKeywords(List keywords) { + this.keywords = keywords; + } + + public String getSentiment() { + return sentiment; + } + + public void setSentiment(String sentiment) { + this.sentiment = sentiment; + } + + public Double getConfidence() { + return confidence; + } + + public void setConfidence(Double confidence) { + this.confidence = confidence; + } + } + + /** + * 示例14:AgentHook - 在 Agent 开始/结束时执行 + */ + public static class LoggingHook extends AgentHook { + @Override + public String getName() { + return "logging"; + } + + @Override + public HookPosition[] getHookPositions() { + return new HookPosition[] { + HookPosition.BEFORE_AGENT, + HookPosition.AFTER_AGENT + }; + } + + @Override + public CompletableFuture> beforeAgent(OverAllState state, RunnableConfig config) { + System.out.println("Agent 开始执行"); + return CompletableFuture.completedFuture(Map.of()); + } + + @Override + public CompletableFuture> afterAgent(OverAllState state, RunnableConfig config) { + System.out.println("Agent 执行完成"); + return CompletableFuture.completedFuture(Map.of()); + } + } + + // ==================== Interceptors ==================== + + /** + * 示例15:MessagesModelHook - 在模型调用前修剪消息 + * 使用 MessagesModelHook 实现,在模型调用前修剪消息列表,只保留最后 MAX_MESSAGES 条消息 + */ + @HookPositions({HookPosition.BEFORE_MODEL}) + public static class MessageTrimmingHook extends MessagesModelHook { + private static final int MAX_MESSAGES = 10; + + @Override + public String getName() { + return "message_trimming"; + } + + @Override + public AgentCommand beforeModel(List previousMessages, RunnableConfig config) { + // 如果消息数量超过限制,只保留最后 MAX_MESSAGES 条消息 + if (previousMessages.size() > MAX_MESSAGES) { + List trimmedMessages = previousMessages.subList( + previousMessages.size() - MAX_MESSAGES, + previousMessages.size() + ); + // 使用 REPLACE 策略替换所有消息 + return new AgentCommand(trimmedMessages, UpdatePolicy.REPLACE); + } + // 如果消息数量未超过限制,返回原始消息(不进行修改) + return new AgentCommand(previousMessages); + } + } + + /** + * 示例16:ModelInterceptor - 内容安全检查 + */ + public static class GuardrailInterceptor extends ModelInterceptor { + @Override + public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) { + // 前置:检查输入 + if (containsSensitiveContent(request.getMessages())) { + return ModelResponse.of(new AssistantMessage("检测到不适当的内容")); + } + + // 执行调用 + ModelResponse response = handler.call(request); + + // 后置:检查输出 + return sanitizeIfNeeded(response); + } + + private boolean containsSensitiveContent(List messages) { + // 实现敏感内容检测逻辑 + return false; + } + + private ModelResponse sanitizeIfNeeded(ModelResponse response) { + // 实现响应清理逻辑 + return response; + } + + @Override + public String getName() { + return "GuardrailInterceptor"; + } + } + + // ==================== Main 方法 ==================== + + /** + * 示例17:ToolInterceptor - 监控和错误处理 + */ + public static class ToolMonitoringInterceptor extends ToolInterceptor { + @Override + public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandler handler) { + long startTime = System.currentTimeMillis(); + try { + ToolCallResponse response = handler.call(request); + logSuccess(request, System.currentTimeMillis() - startTime); + return response; + } + catch (Exception e) { + logError(request, e, System.currentTimeMillis() - startTime); + return ToolCallResponse.of(request.getToolCallId(), request.getToolName(), + "工具执行遇到问题,请稍后重试"); + } + } + + private void logSuccess(ToolCallRequest request, long duration) { + System.out.println("Tool " + request.getToolName() + " succeeded in " + duration + "ms"); + } + + private void logError(ToolCallRequest request, Exception e, long duration) { + System.err.println("Tool " + request.getToolName() + " failed in " + duration + "ms: " + e.getMessage()); + } + + @Override + public String getName() { + return "ToolMonitoringInterceptor"; + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/HooksExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/HooksExample.java new file mode 100644 index 00000000..2c511836 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/HooksExample.java @@ -0,0 +1,650 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.tutorials; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.hook.AgentHook; +import com.alibaba.cloud.ai.graph.agent.hook.HookPosition; +import com.alibaba.cloud.ai.graph.agent.hook.HookPositions; +import com.alibaba.cloud.ai.graph.agent.hook.ModelHook; +import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook; +import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand; +import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy; +import com.alibaba.cloud.ai.graph.agent.hook.hip.HumanInTheLoopHook; +import com.alibaba.cloud.ai.graph.agent.hook.hip.ToolConfig; +import com.alibaba.cloud.ai.graph.agent.hook.modelcalllimit.ModelCallLimitHook; +import com.alibaba.cloud.ai.graph.agent.hook.pii.PIIDetectionHook; +import com.alibaba.cloud.ai.graph.agent.hook.pii.PIIType; +import com.alibaba.cloud.ai.graph.agent.hook.pii.RedactionStrategy; +import com.alibaba.cloud.ai.graph.agent.hook.summarization.SummarizationHook; +import com.alibaba.cloud.ai.graph.agent.interceptor.ModelCallHandler; +import com.alibaba.cloud.ai.graph.agent.interceptor.ModelInterceptor; +import com.alibaba.cloud.ai.graph.agent.interceptor.ModelRequest; +import com.alibaba.cloud.ai.graph.agent.interceptor.ModelResponse; +import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallHandler; +import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallRequest; +import com.alibaba.cloud.ai.graph.agent.interceptor.ToolCallResponse; +import com.alibaba.cloud.ai.graph.agent.interceptor.ToolInterceptor; +import com.alibaba.cloud.ai.graph.agent.interceptor.contextediting.ContextEditingInterceptor; +import com.alibaba.cloud.ai.graph.agent.interceptor.todolist.TodoListInterceptor; +import com.alibaba.cloud.ai.graph.agent.interceptor.toolemulator.ToolEmulatorInterceptor; +import com.alibaba.cloud.ai.graph.agent.interceptor.toolretry.ToolRetryInterceptor; +import com.alibaba.cloud.ai.graph.agent.interceptor.toolselection.ToolSelectionInterceptor; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; + +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +/** + * Hooks & Interceptors Tutorial - hooks.md + */ +public class HooksExample { + + // ==================== 基础 Hook 和 Interceptor 配置 ==================== + + /** + * 示例1:添加 Hooks 和 Interceptors + */ + public static void basicHooksAndInterceptors() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建工具(示例) + ToolCallback[] tools = new ToolCallback[0]; + + // 创建 Hooks 和 Interceptors + ModelHook loggingHook = new LoggingModelHook(); + MessagesModelHook messageTrimmingHook = new MessageTrimmingHook(); + ModelInterceptor guardrailInterceptor = new GuardrailInterceptor(); + ToolInterceptor retryInterceptor = new RetryToolInterceptor(); + + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .tools(tools) + .hooks(loggingHook, messageTrimmingHook) + .interceptors(guardrailInterceptor) + .interceptors(retryInterceptor) + .build(); + } + + // ==================== 消息压缩(Summarization) ==================== + + /** + * 示例2:消息压缩 Hook + */ + public static void messageSummarization() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建消息压缩 Hook + SummarizationHook summarizationHook = SummarizationHook.builder() + .model(chatModel) + .maxTokensBeforeSummary(4000) + .messagesToKeep(20) + .build(); + + // 使用 + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .hooks(summarizationHook) + .build(); + + } + + // ==================== Human-in-the-Loop ==================== + + /** + * 示例3:Human-in-the-Loop Hook + */ + public static void humanInTheLoop() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建工具(示例) + ToolCallback sendEmailTool = createSendEmailTool(); + ToolCallback deleteDataTool = createDeleteDataTool(); + + // 创建 Human-in-the-Loop Hook + HumanInTheLoopHook humanReviewHook = HumanInTheLoopHook.builder() + .approvalOn("sendEmailTool", ToolConfig.builder() + .description("Please confirm sending the email.") + .build()) + .approvalOn("deleteDataTool", ToolConfig.builder() + .description("Please confirm deleting the data.") + .build()) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("supervised_agent") + .model(chatModel) + .tools(sendEmailTool, deleteDataTool) + .hooks(humanReviewHook) + .saver(new MemorySaver()) + .build(); + } + + // ==================== 模型调用限制 ==================== + + /** + * 示例4:模型调用限制 + */ + public static void modelCallLimit() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .hooks(ModelCallLimitHook.builder().runLimit(5).build()) // 限制模型调用次数为5次 + .saver(new MemorySaver()) + .build(); + } + + + // ==================== PII 检测 ==================== + + /** + * 示例6:PII 检测 + */ + public static void piiDetection() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + PIIDetectionHook pii = PIIDetectionHook.builder() + .piiType(PIIType.EMAIL) + .strategy(RedactionStrategy.REDACT) + .applyToInput(true) + .build(); + + // 使用 + ReactAgent agent = ReactAgent.builder() + .name("secure_agent") + .model(chatModel) + .hooks(pii) + .build(); + } + + // ==================== 工具重试 ==================== + + /** + * 示例7:工具重试 + */ + public static void toolRetry() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建工具(示例) + ToolCallback searchTool = createSearchTool(); + ToolCallback databaseTool = createDatabaseTool(); + + // 使用 + ReactAgent agent = ReactAgent.builder() + .name("resilient_agent") + .model(chatModel) + .tools(searchTool, databaseTool) + .interceptors(ToolRetryInterceptor.builder().maxRetries(2) + .onFailure(ToolRetryInterceptor.OnFailureBehavior.RETURN_MESSAGE).build()) + .build(); + } + + // ==================== Planning ==================== + + /** + * 示例8:Planning Hook + */ + public static void planning() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ToolCallback myTool = createSampleTool(); + + // 使用 + ReactAgent agent = ReactAgent.builder() + .name("planning_agent") + .model(chatModel) + .tools(myTool) + .interceptors(TodoListInterceptor.builder().build()) + .build(); + } + + // ==================== LLM Tool Selector ==================== + + /** + * 示例9:LLM 工具选择器 + */ + public static void llmToolSelector() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ChatModel selectorModel = chatModel; // 用于选择的另一个ChatModel + + ToolCallback tool1 = createSampleTool(); + ToolCallback tool2 = createSampleTool(); + + // 使用 + ReactAgent agent = ReactAgent.builder() + .name("smart_selector_agent") + .model(chatModel) + .tools(tool1, tool2) + .interceptors(ToolSelectionInterceptor.builder().build()) + .build(); + } + + // ==================== LLM Tool Emulator ==================== + + /** + * 示例10:LLM 工具模拟器 + */ + public static void llmToolEmulator() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ToolCallback simulatedTool = createSampleTool(); + + // 使用 + ReactAgent agent = ReactAgent.builder() + .name("emulator_agent") + .model(chatModel) + .tools(simulatedTool) + .interceptors(ToolEmulatorInterceptor.builder().model(chatModel).build()) + .build(); + } + + // ==================== Context Editing ==================== + + /** + * 示例11:上下文编辑 + */ + public static void contextEditing() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 使用 + ReactAgent agent = ReactAgent.builder() + .name("context_aware_agent") + .model(chatModel) + .interceptors(ContextEditingInterceptor.builder().trigger(120000).clearAtLeast(60000).build()) + .build(); + } + + // ==================== 自定义 Hooks ==================== + + // 创建示例工具的辅助方法 + private static ToolCallback createSendEmailTool() { + return FunctionToolCallback.builder("sendEmailTool", (String input) -> "Email sent") + .description("Send an email") + .inputType(String.class) + .build(); + } + + private static ToolCallback createDeleteDataTool() { + return FunctionToolCallback.builder("deleteDataTool", (String input) -> "Data deleted") + .description("Delete data") + .inputType(String.class) + .build(); + } + + // ==================== 自定义 Interceptors ==================== + + private static ToolCallback createSearchTool() { + return FunctionToolCallback.builder("searchTool", (String input) -> "Search results") + .description("Search the web") + .inputType(String.class) + .build(); + } + + private static ToolCallback createDatabaseTool() { + return FunctionToolCallback.builder("databaseTool", (String input) -> "Database query results") + .description("Query database") + .inputType(String.class) + .build(); + } + + // ==================== 辅助类和方法 ==================== + + private static ToolCallback createSampleTool() { + return FunctionToolCallback.builder("sampleTool", (String input) -> "Sample result") + .description("A sample tool") + .inputType(String.class) + .build(); + } + + public static void main(String[] args) { + System.out.println("=== Hooks and Interceptors Tutorial Examples ==="); + System.out.println("注意:需要设置 AI_DASHSCOPE_API_KEY 环境变量\n"); + + try { + System.out.println("\n--- 示例1:基础 Hooks 和 Interceptors ---"); + basicHooksAndInterceptors(); + + System.out.println("\n--- 示例2:消息压缩 Hook ---"); + messageSummarization(); + + System.out.println("\n--- 示例3:人工介入循环 ---"); + humanInTheLoop(); + + System.out.println("\n--- 示例4:模型调用限制 ---"); + modelCallLimit(); + + System.out.println("\n--- 示例5:PII 检测 ---"); + piiDetection(); + + System.out.println("\n--- 示例6:工具重试 ---"); + toolRetry(); + + System.out.println("\n--- 示例7:规划(Planning) ---"); + planning(); + + System.out.println("\n--- 示例8:LLM 工具选择器 ---"); + llmToolSelector(); + + System.out.println("\n--- 示例9:LLM 工具模拟器 ---"); + llmToolEmulator(); + + System.out.println("\n--- 示例10:上下文编辑 ---"); + contextEditing(); + + System.out.println("\n=== 所有示例执行完成 ==="); + } + catch (Exception e) { + System.err.println("执行示例时发生错误: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 示例12:自定义 ModelHook + */ + @HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL}) + public static class CustomModelHook extends ModelHook { + + @Override + public String getName() { + return "custom_model_hook"; + } + + @Override + public CompletableFuture> beforeModel(OverAllState state, RunnableConfig config) { + // 在模型调用前执行 + System.out.println("准备调用模型..."); + + // 可以修改状态 + // 例如:添加额外的上下文 + return CompletableFuture.completedFuture(Map.of("extra_context", "某些额外信息")); + } + + @Override + public CompletableFuture> afterModel(OverAllState state, RunnableConfig config) { + // 在模型调用后执行 + System.out.println("模型调用完成"); + + // 可以记录响应信息 + return CompletableFuture.completedFuture(Map.of()); + } + } + + /** + * 示例13:自定义 AgentHook + */ + @HookPositions({HookPosition.BEFORE_AGENT, HookPosition.AFTER_AGENT}) + public static class CustomAgentHook extends AgentHook { + + @Override + public String getName() { + return "custom_agent_hook"; + } + + @Override + public CompletableFuture> beforeAgent(OverAllState state, RunnableConfig config) { + System.out.println("Agent 开始执行"); + // 可以初始化资源、记录开始时间等 + return CompletableFuture.completedFuture(Map.of("start_time", System.currentTimeMillis())); + } + + @Override + public CompletableFuture> afterAgent(OverAllState state, RunnableConfig config) { + System.out.println("Agent 执行完成"); + // 可以清理资源、计算执行时间等 + Optional startTime = state.value("start_time"); + if (startTime.isPresent()) { + long duration = System.currentTimeMillis() - (Long) startTime.get(); + System.out.println("执行耗时: " + duration + "ms"); + } + return CompletableFuture.completedFuture(Map.of()); + } + } + + /** + * 示例14:自定义 ModelInterceptor + */ + public static class LoggingInterceptor extends ModelInterceptor { + + @Override + public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) { + // 请求前记录 + System.out.println("发送请求到模型: " + request.getMessages().size() + " 条消息"); + + long startTime = System.currentTimeMillis(); + + // 执行实际调用 + ModelResponse response = handler.call(request); + + // 响应后记录 + long duration = System.currentTimeMillis() - startTime; + System.out.println("模型响应耗时: " + duration + "ms"); + + return response; + } + + @Override + public String getName() { + return "LoggingInterceptor"; + } + } + + /** + * 示例15:自定义 ToolInterceptor + */ + public static class ToolMonitoringInterceptor extends ToolInterceptor { + + @Override + public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandler handler) { + String toolName = request.getToolName(); + long startTime = System.currentTimeMillis(); + + System.out.println("执行工具: " + toolName); + + try { + ToolCallResponse response = handler.call(request); + + long duration = System.currentTimeMillis() - startTime; + System.out.println("工具 " + toolName + " 执行成功 (耗时: " + duration + "ms)"); + + return response; + } + catch (Exception e) { + long duration = System.currentTimeMillis() - startTime; + System.err.println("工具 " + toolName + " 执行失败 (耗时: " + duration + "ms): " + e.getMessage()); + + return ToolCallResponse.of( + request.getToolCallId(), + request.getToolName(), + "工具执行失败: " + e.getMessage() + ); + } + } + + @Override + public String getName() { + return "ToolMonitoringInterceptor"; + } + } + + /** + * 日志记录 ModelHook + */ + @HookPositions({HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL}) + private static class LoggingModelHook extends ModelHook { + @Override + public String getName() { + return "logging_model_hook"; + } + + @Override + public HookPosition[] getHookPositions() { + return new HookPosition[] {HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL}; + } + + @Override + public CompletableFuture> beforeModel(OverAllState state, RunnableConfig config) { + System.out.println("Before model call"); + return CompletableFuture.completedFuture(Map.of()); + } + + @Override + public CompletableFuture> afterModel(OverAllState state, RunnableConfig config) { + System.out.println("After model call"); + return CompletableFuture.completedFuture(Map.of()); + } + } + + /** + * 消息修剪 Hook + * 使用 MessagesModelHook 实现,在模型调用前修剪消息列表,只保留最后 10 条消息 + */ + @HookPositions({HookPosition.BEFORE_MODEL}) + private static class MessageTrimmingHook extends MessagesModelHook { + private static final int MAX_MESSAGES = 10; + + @Override + public String getName() { + return "message_trimming"; + } + + @Override + public AgentCommand beforeModel(List previousMessages, RunnableConfig config) { + // 如果消息数量超过限制,只保留最后 MAX_MESSAGES 条消息 + if (previousMessages.size() > MAX_MESSAGES) { + List trimmedMessages = previousMessages.subList( + previousMessages.size() - MAX_MESSAGES, + previousMessages.size() + ); + // 使用 REPLACE 策略替换所有消息 + return new AgentCommand(trimmedMessages, UpdatePolicy.REPLACE); + } + // 如果消息数量未超过限制,返回原始消息(不进行修改) + return new AgentCommand(previousMessages); + } + } + + /** + * 护栏拦截器 + */ + private static class GuardrailInterceptor extends ModelInterceptor { + @Override + public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) { + // 简化的实现 + return handler.call(request); + } + + @Override + public String getName() { + return "GuardrailInterceptor"; + } + } + + // ==================== Main 方法 ==================== + + /** + * 重试工具拦截器 + */ + private static class RetryToolInterceptor extends ToolInterceptor { + @Override + public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandler handler) { + // 简化的实现 + return handler.call(request); + } + + @Override + public String getName() { + return "RetryToolInterceptor"; + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/MemoryExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/MemoryExample.java new file mode 100644 index 00000000..7c70b9fd --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/MemoryExample.java @@ -0,0 +1,557 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.tutorials; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.agent.hook.HookPosition; +import com.alibaba.cloud.ai.graph.agent.hook.HookPositions; +import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand; +import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook; +import com.alibaba.cloud.ai.graph.agent.hook.messages.UpdatePolicy; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.checkpoint.savers.redis.RedisSaver; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; +import org.redisson.api.RedissonClient; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; + +/** + * Memory Tutorial - 完整代码示例 + * 展示如何使用短期记忆让Agent记住先前交互 + * + * 来源:memory.md + */ +public class MemoryExample { + + // ==================== 基础使用 ==================== + + /** + * 示例1:基础记忆配置 + */ + public static void basicMemoryConfiguration() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建示例工具 + ToolCallback getUserInfoTool = createGetUserInfoTool(); + + // 配置 checkpointer + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .tools(getUserInfoTool) + .saver(new MemorySaver()) + .build(); + + // 使用 thread_id 维护对话上下文 + RunnableConfig config = RunnableConfig.builder() + .threadId("1") // threadId 指定会话 ID + .build(); + + AssistantMessage message = agent.call("你好!我叫 Bob。", config); + System.out.println(message.getText()); + } + + /** + * 示例2:生产环境使用 Redis Checkpointer + */ + public static void productionMemoryConfiguration(RedissonClient redissonClient) { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ToolCallback getUserInfoTool = createGetUserInfoTool(); + + // 配置 Redis checkpointer + RedisSaver redisSaver = RedisSaver.builder().redisson(redissonClient).build(); + + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .tools(getUserInfoTool) + .saver(redisSaver) + .build(); + } + + // ==================== 自定义 Agent 记忆 ==================== + + /** + * 示例5:使用消息修剪 + */ + public static void useMessageTrimming() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ToolCallback[] tools = new ToolCallback[0]; + + // 使用 + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .tools(tools) + .hooks(new MessageTrimmingHook()) + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("1") + .build(); + + agent.call("你好,我叫 bob", config); + agent.call("写一首关于猫的短诗", config); + agent.call("现在对狗做同样的事情", config); + agent.call("写一首关于狗的短诗", config); + agent.call("写一首关于牛的短诗", config); + AssistantMessage finalResponse = agent.call("我叫什么名字?", config); + + System.out.println(finalResponse.getText()); + // 输出:你的名字是 Bob。你之前告诉我的。 + } + + // ==================== 修剪消息 ==================== + + /** + * 示例8:使用消息删除 + */ + public static void useMessageDeletion() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .systemPrompt("请简洁明了。") + .hooks(new MessageDeletionHook()) + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("1") + .build(); + + // 第一次调用 + agent.call("你好!我是 bob", config); + // 输出:[('human', "你好!我是 bob"), ('assistant', '你好 Bob!很高兴见到你...')] + + // 第二次调用 + agent.call("我叫什么名字?", config); + // 输出:[('human', "我叫什么名字?"), ('assistant', '你的名字是 Bob...')] + } + + /** + * 示例10:使用消息总结 + */ + public static void useMessageSummarization() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 用于总结的模型(可以是更便宜的模型) + ChatModel summaryModel = chatModel; + + MessageSummarizationHook summarizationHook = new MessageSummarizationHook( + summaryModel, + 100, // 在 4000 tokens 时触发总结 + 3 // 总结后保留最后 20 条消息 + ); + + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .hooks(summarizationHook) + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("1") + .build(); + + agent.call("你好,我叫 bob", config); + agent.call("陈晓红生了一个儿子,请给她儿子取一个名字", config); + agent.call("写一首关于猫的短诗", config); + agent.call("写一首关于狗的短诗", config); + agent.call("现在对狗做同样的事情", config); + agent.call("介绍红烧肉的做法", config); + agent.call("她又生了一个女儿,请给她女儿取一个名字", config); + AssistantMessage finalResponse = agent.call("我叫什么名字?", config); + + System.out.println(finalResponse.getText()); + // 输出:你的名字是 Bob! + } + + // ==================== 删除消息 ==================== + + /** + * 示例12:使用工具访问记忆 + */ + public static void accessMemoryInTool() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建工具 + ToolCallback getUserInfoTool = FunctionToolCallback + .builder("get_user_info", new UserInfoTool()) + .description("查找用户信息") + .inputType(String.class) + .build(); + + // 使用 + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .tools(getUserInfoTool) + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("1") + .addMetadata("user_id", "user_123") + .build(); + + AssistantMessage response = agent.call("获取用户信息", config); + System.out.println(response.getText()); + } + + /** + * 创建示例工具 + */ + private static ToolCallback createGetUserInfoTool() { + return FunctionToolCallback.builder("get_user_info", (String query) -> { + return "User info: " + query; + }) + .description("Get user information") + .inputType(String.class) + .build(); + } + + public static void main(String[] args) { + System.out.println("=== Memory Tutorial Examples ==="); + System.out.println("注意:需要设置 AI_DASHSCOPE_API_KEY 环境变量\n"); + + try { + // 示例1:基础记忆配置 + // System.out.println("\n--- 示例1:基础记忆配置 ---"); + // basicMemoryConfiguration(); + + // 示例2:生产环境使用 Redis Checkpointer (需要 RedissonClient 实例,此处跳过) + // System.out.println("\n--- 示例2:生产环境使用 Redis Checkpointer (跳过,需要 RedissonClient) ---"); + // productionMemoryConfiguration(redissonClient); + + // // 示例5:使用消息修剪 + // System.out.println("\n--- 示例5:使用消息修剪 ---"); +// useMessageTrimming(); + // + // // 示例8:使用消息删除 + // System.out.println("\n--- 示例8:使用消息删除 ---"); + // useMessageDeletion(); + // + // // 示例10:使用消息总结 + // System.out.println("\n--- 示例10:使用消息总结 ---"); +// useMessageSummarization(); + // + // // 示例12:使用工具访问记忆 + // System.out.println("\n--- 示例12:使用工具访问记忆 ---"); + accessMemoryInTool(); + + // System.out.println("\n=== 所有示例执行完成 ==="); + } + // catch (GraphRunnerException e) { + // System.err.println("执行示例时发生错误: " + e.getMessage()); + // e.printStackTrace(); + // } + catch (Exception e) { + System.err.println("发生未预期的错误: " + e.getMessage()); + e.printStackTrace(); + } + } + + // ==================== 总结消息 ==================== + + /** + * 示例3:在 Hook 中访问和修改状态 + * 注意:这个 Hook 主要用于访问消息历史,不修改消息,所以可以继续使用 ModelHook + * 但如果需要修改消息,应该使用 MessagesModelHook + */ + @HookPositions({HookPosition.BEFORE_MODEL}) + public static class CustomMemoryHook extends MessagesModelHook { + + @Override + public String getName() { + return "custom_memory"; + } + + @Override + public AgentCommand beforeModel(List previousMessages, RunnableConfig config) { + // 访问消息历史(previousMessages 已经提供了消息列表) + // 处理消息... + // 如果需要修改消息,可以返回新的 AgentCommand + // 这里只是访问,不修改消息,所以返回原始消息 + return new AgentCommand(previousMessages); + } + } + + /** + * 示例4:消息修剪 Hook + * 使用 MessagesModelHook 实现,在模型调用前修剪消息列表 + * 保留第一条消息和最后 keepCount 条消息,删除中间的消息 + */ + @HookPositions({HookPosition.BEFORE_MODEL}) + public static class MessageTrimmingHook extends MessagesModelHook { + + private static final int MAX_MESSAGES = 3; + + @Override + public String getName() { + return "message_trimming"; + } + + @Override + public AgentCommand beforeModel(List previousMessages, RunnableConfig config) { + if (previousMessages.size() <= MAX_MESSAGES) { + // 如果消息数量未超过限制,无需更改 + return new AgentCommand(previousMessages); + } + + int keepCount = previousMessages.size() % 2 == 0 ? 3 : 4; + + // 构建要保留的消息列表:第一条消息 + 最后 keepCount 条消息 + List trimmedMessages = new ArrayList<>(); + // 保留第一条消息 + if (!previousMessages.isEmpty()) { + trimmedMessages.add(previousMessages.get(0)); + } + // 保留最后 keepCount 条消息 + if (previousMessages.size() - keepCount > 0) { + trimmedMessages.addAll(previousMessages.subList( + previousMessages.size() - keepCount, + previousMessages.size() + )); + } + + // 使用 REPLACE 策略替换所有消息 + return new AgentCommand(trimmedMessages, UpdatePolicy.REPLACE); + } + } + + // ==================== 访问记忆 ==================== + + /** + * 示例6:消息删除 Hook + * 使用 MessagesModelHook 实现,在模型调用后删除最早的两条消息 + */ + @HookPositions({HookPosition.AFTER_MODEL}) + public static class MessageDeletionHook extends MessagesModelHook { + + @Override + public String getName() { + return "message_deletion"; + } + + @Override + public AgentCommand afterModel(List previousMessages, RunnableConfig config) { + if (previousMessages.size() <= 2) { + // 如果消息数量不超过2条,无需删除 + return new AgentCommand(previousMessages); + } + + // 删除最早的两条消息,保留其余消息 + List remainingMessages = previousMessages.subList(2, previousMessages.size()); + + // 使用 REPLACE 策略替换所有消息 + return new AgentCommand(remainingMessages, UpdatePolicy.REPLACE); + } + } + + /** + * 示例7:删除所有消息 + * 使用 MessagesModelHook 实现,在模型调用后删除所有消息 + */ + @HookPositions({HookPosition.AFTER_MODEL}) + public static class ClearAllMessagesHook extends MessagesModelHook { + + @Override + public String getName() { + return "clear_all_messages"; + } + + @Override + public AgentCommand afterModel(List previousMessages, RunnableConfig config) { + // 删除所有消息,返回空列表 + List emptyMessages = new ArrayList<>(); + // 使用 REPLACE 策略替换所有消息为空列表 + return new AgentCommand(emptyMessages, UpdatePolicy.REPLACE); + } + } + + // ==================== 辅助方法 ==================== + + /** + * 示例9:消息总结 Hook + * 使用 MessagesModelHook 实现,在模型调用前检查消息数量,如果超过阈值则生成摘要 + * 删除旧消息,保留摘要消息和最近的消息 + */ + @HookPositions({HookPosition.BEFORE_MODEL}) + public static class MessageSummarizationHook extends MessagesModelHook { + + private final ChatModel summaryModel; + private final int maxTokensBeforeSummary; + private final int messagesToKeep; + + public MessageSummarizationHook( + ChatModel summaryModel, + int maxTokensBeforeSummary, + int messagesToKeep + ) { + this.summaryModel = summaryModel; + this.maxTokensBeforeSummary = maxTokensBeforeSummary; + this.messagesToKeep = messagesToKeep; + } + + @Override + public String getName() { + return "message_summarization"; + } + + @Override + public AgentCommand beforeModel(List previousMessages, RunnableConfig config) { + // 估算 token 数量(简化版) + int estimatedTokens = previousMessages.stream() + .mapToInt(m -> m.getText().length() / 4) + .sum(); + + if (estimatedTokens < maxTokensBeforeSummary) { + // 如果 token 数量未超过阈值,无需总结 + return new AgentCommand(previousMessages); + } + + // 需要总结 + int messagesToSummarize = previousMessages.size() - messagesToKeep; + if (messagesToSummarize <= 0) { + // 如果消息数量不足以总结,无需更改 + return new AgentCommand(previousMessages); + } + + List oldMessages = previousMessages.subList(0, messagesToSummarize); + List recentMessages = previousMessages.subList( + messagesToSummarize, + previousMessages.size() + ); + + // 生成摘要 + String summary = generateSummary(oldMessages); + + // 创建摘要消息 + SystemMessage summaryMessage = new SystemMessage( + "## 之前对话摘要:\n" + summary + ); + + // 构建新的消息列表:摘要消息 + 最近的消息 + List newMessages = new ArrayList<>(); + newMessages.add(summaryMessage); + newMessages.addAll(recentMessages); + + // 使用 REPLACE 策略替换所有消息 + return new AgentCommand(newMessages, UpdatePolicy.REPLACE); + } + + private String generateSummary(List messages) { + StringBuilder conversation = new StringBuilder(); + for (Message msg : messages) { + conversation.append(msg.getMessageType()) + .append(": ") + .append(msg.getText()) + .append("\n"); + } + + String summaryPrompt = "请简要总结以下对话:\n\n" + conversation; + + ChatResponse response = summaryModel.call( + new Prompt(new UserMessage(summaryPrompt)) + ); + + return response.getResult().getOutput().getText(); + } + } + + // ==================== Main 方法 ==================== + + /** + * 示例11:在工具中读取短期记忆 + */ + public static class UserInfoTool implements BiFunction { + + @Override + public String apply(String query, ToolContext toolContext) { + // 从上下文中获取用户信息 + RunnableConfig config = (RunnableConfig) toolContext.getContext().get("config"); + if (config == null) { + return "未知用户"; + } + String userId = (String) config.metadata("user_id").orElse(""); + + if ("user_123".equals(userId)) { + return "用户是 John Smith"; + } + else { + return "未知用户"; + } + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/MessagesExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/MessagesExample.java new file mode 100644 index 00000000..50a96f98 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/MessagesExample.java @@ -0,0 +1,606 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.tutorials; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; + +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.ToolResponseMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.metadata.ChatResponseMetadata; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.content.Media; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.MimeTypeUtils; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Flux; + +/** + * Messages Tutorial - 完整代码示例 + * 展示Messages作为模型交互的基本单元的使用方法 + * + * 来源:messages.md + */ +public class MessagesExample { + + // ==================== 基础使用 ==================== + + /** + * 示例1:基础消息使用 + */ + public static void basicMessageUsage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 使用 DashScope ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + SystemMessage systemMsg = new SystemMessage("你是一个有帮助的助手。"); + UserMessage userMsg = new UserMessage("你好,你好吗?"); + + // 与聊天模型一起使用 + List messages = List.of(systemMsg, userMsg); + Prompt prompt = new Prompt(messages); + ChatResponse response = chatModel.call(prompt); // 返回 ChatResponse,包含 AssistantMessage + } + + // ==================== 文本提示 vs 消息提示 ==================== + + /** + * 示例2:文本提示 + */ + public static void textPromptUsage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 使用字符串直接调用 + String response = chatModel.call("写一首关于春天的俳句"); + } + + /** + * 示例3:消息提示 + */ + public static void messagePromptUsage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + List messages = List.of( + new SystemMessage("你是一个诗歌专家"), + new UserMessage("写一首关于春天的俳句"), + new AssistantMessage("樱花盛开时...") + ); + Prompt prompt = new Prompt(messages); + ChatResponse response = chatModel.call(prompt); + } + + // ==================== System Message ==================== + + /** + * 示例4:基础指令 + */ + public static void basicSystemMessage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 基础指令 + SystemMessage systemMsg = new SystemMessage("你是一个有帮助的编程助手。"); + + List messages = List.of( + systemMsg, + new UserMessage("如何创建 REST API?") + ); + ChatResponse response = chatModel.call(new Prompt(messages)); + } + + /** + * 示例5:详细的角色设定 + */ + public static void detailedSystemMessage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 详细的角色设定 + SystemMessage systemMsg = new SystemMessage(""" + 你是一位资深的 Java 开发者,擅长 Web 框架。 + 始终提供代码示例并解释你的推理。 + 在解释中要简洁但透彻。 + """); + + List messages = List.of( + systemMsg, + new UserMessage("如何创建 REST API?") + ); + ChatResponse response = chatModel.call(new Prompt(messages)); + } + + // ==================== User Message ==================== + + /** + * 示例6:文本内容 + */ + public static void textUserMessage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 使用消息对象 + ChatResponse response = chatModel.call( + new Prompt(List.of(new UserMessage("什么是机器学习?"))) + ); + + // 使用字符串快捷方式 + // 使用字符串是单个 UserMessage 的快捷方式 + String response2 = chatModel.call("什么是机器学习?"); + } + + /** + * 示例7:消息元数据 + */ + public static void userMessageMetadata() { + UserMessage userMsg = UserMessage.builder() + .text("你好!") + .metadata(Map.of( + "user_id", "alice", // 可选:识别不同用户 + "session_id", "sess_123" // 可选:会话标识符 + )) + .build(); + } + + /** + * 示例8:多模态内容 - 图像 + */ + public static void multimodalImageMessage() throws Exception { + // 从 URL 创建图像 + UserMessage userMsg = UserMessage.builder() + .text("描述这张图片的内容。") + .media(Media.builder().mimeType(MimeTypeUtils.IMAGE_JPEG).data(new URL("https://example.com/image.jpg")) + .build()).build(); + } + + // ==================== Assistant Message ==================== + + /** + * 示例9:Assistant Message 基础使用 + */ + public static void basicAssistantMessage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ChatResponse response = chatModel.call(new Prompt("解释 AI")); + AssistantMessage aiMessage = response.getResult().getOutput(); + System.out.println(aiMessage.getText()); + } + + /** + * 示例10:手动创建 AI 消息 + */ + public static void manualAssistantMessage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 手动创建 AI 消息(例如,用于对话历史) + AssistantMessage aiMsg = new AssistantMessage("我很乐意帮助你回答这个问题!"); + + // 添加到对话历史 + List messages = List.of( + new SystemMessage("你是一个有帮助的助手"), + new UserMessage("你能帮我吗?"), + aiMsg, // 插入,就像它来自模型一样 + new UserMessage("太好了!2+2 等于多少?") + ); + + ChatResponse response = chatModel.call(new Prompt(messages)); + } + + /** + * 示例11:工具调用 + */ + public static void toolCallsInAssistantMessage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + Prompt prompt = new Prompt("北京的天气怎么样?"); + ChatResponse response = chatModel.call(prompt); + AssistantMessage aiMessage = response.getResult().getOutput(); + + if (aiMessage.hasToolCalls()) { + for (AssistantMessage.ToolCall toolCall : aiMessage.getToolCalls()) { + System.out.println("Tool: " + toolCall.name()); + System.out.println("Args: " + toolCall.arguments()); + System.out.println("ID: " + toolCall.id()); + } + } + } + + /** + * 示例12:Token 使用 + */ + public static void tokenUsage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ChatResponse response = chatModel.call(new Prompt("你好!")); + ChatResponseMetadata metadata = response.getMetadata(); + + // 访问使用信息 + if (metadata != null && metadata.getUsage() != null) { + System.out.println("Input tokens: " + metadata.getUsage().getPromptTokens()); + System.out.println("Output tokens: " + metadata.getUsage().getCompletionTokens()); + System.out.println("Total tokens: " + metadata.getUsage().getTotalTokens()); + } + } + + /** + * 示例13:流式和块 + */ + public static void streamingMessages() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + Flux responseStream = chatModel.stream(new Prompt("你好")); + + StringBuilder fullResponse = new StringBuilder(); + responseStream.subscribe( + chunk -> { + String content = chunk.getResult().getOutput().getText(); + fullResponse.append(content); + System.out.print(content); + } + ); + } + + // ==================== Tool Response Message ==================== + + /** + * 示例14:Tool Response Message + */ + public static void toolResponseMessage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 在模型进行工具调用后 + AssistantMessage aiMessage = AssistantMessage.builder() + .content("") + .toolCalls(List.of( + new AssistantMessage.ToolCall( + "call_123", + "tool", + "get_weather", + "{\"location\": \"San Francisco\"}" + ) + )) + .build(); + + // 执行工具并创建结果消息 + String weatherResult = "晴朗,22°C"; + ToolResponseMessage toolMessage = ToolResponseMessage.builder() + .responses(List.of( + new ToolResponseMessage.ToolResponse("call_123", "get_weather", weatherResult) + )) + .build(); + + // 继续对话 + List messages = List.of( + new UserMessage("旧金山的天气怎么样?"), + aiMessage, // 模型的工具调用 + toolMessage // 工具执行结果 + ); + ChatResponse response = chatModel.call(new Prompt(messages)); + } + + // ==================== 多模态内容 ==================== + + /** + * 示例15:图像输入 - 从 URL + */ + public static void imageInputFromURL() throws Exception { + // 从 URL + UserMessage message = UserMessage.builder() + .text("描述这张图片的内容。") + .media(Media.builder().mimeType(MimeTypeUtils.IMAGE_JPEG).data(new URL("https://example.com/image.jpg")) + .build()) + .build(); + } + + /** + * 示例16:图像输入 - 从本地文件 + */ + public static void imageInputFromFile() { + // 从本地文件 + ClassPathResource resource = new ClassPathResource("images/photo.jpg"); + UserMessage message = UserMessage.builder() + .text("描述这张图片的内容。") + .media(new Media( + MimeTypeUtils.IMAGE_JPEG, + resource + )) + .build(); + } + + /** + * 示例17:音频输入 + */ + public static void audioInput() { + UserMessage message = UserMessage.builder() + .text("描述这段音频的内容。") + .media(new Media( + MimeTypeUtils.parseMimeType("audio/wav"), + new ClassPathResource("audio/recording.wav") + )) + .build(); + } + + /** + * 示例18:视频输入 + */ + public static void videoInput() throws Exception { + UserMessage message = UserMessage.builder() + .text("描述这段视频的内容。") + .media(Media.builder().mimeType(MimeTypeUtils.parseMimeType("video/mp4")) + .data(new URL("\"https://example.com/path/to/video.mp4")) + .build()) + .build(); + } + + // ==================== 与 Chat Models 一起使用 ==================== + + /** + * 示例19:基础对话示例 + */ + public static void basicConversationExample() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + List conversationHistory = new ArrayList<>(); + + // 第一轮对话 + conversationHistory.add(new UserMessage("你好!")); + ChatResponse response1 = chatModel.call(new Prompt(conversationHistory)); + conversationHistory.add(response1.getResult().getOutput()); + + // 第二轮对话 + conversationHistory.add(new UserMessage("你能帮我学习 Java 吗?")); + ChatResponse response2 = chatModel.call(new Prompt(conversationHistory)); + conversationHistory.add(response2.getResult().getOutput()); + + // 第三轮对话 + conversationHistory.add(new UserMessage("从哪里开始?")); + ChatResponse response3 = chatModel.call(new Prompt(conversationHistory)); + } + + /** + * 示例20:使用 Builder 模式 + */ + public static void builderPattern() { + // UserMessage with builder + UserMessage userMsg = UserMessage.builder() + .text("你好,我想学习 Spring AI Alibaba") + .metadata(Map.of("user_id", "user_123")) + .build(); + + // SystemMessage with builder + SystemMessage systemMsg = SystemMessage.builder() + .text("你是一个 Spring 框架专家") + .metadata(Map.of("version", "1.0")) + .build(); + + // AssistantMessage with builder + AssistantMessage assistantMsg = AssistantMessage.builder() + .content("我很乐意帮助你学习 Spring AI Alibaba!") + .build(); + } + + /** + * 示例21:消息复制和修改 + */ + public static void messageCopyAndModify() { + // 复制消息 + UserMessage original = new UserMessage("原始消息"); + UserMessage copy = original.copy(); + + // 使用 mutate 创建修改的副本 + UserMessage modified = original.mutate() + .text("修改后的消息") + .metadata(Map.of("modified", true)) + .build(); + } + + // ==================== 在 ReactAgent 中使用 ==================== + + /** + * 示例22:在 ReactAgent 中使用消息 + */ + public static void messagesInReactAgent() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .systemPrompt("你是一个有帮助的助手") + .build(); + + // 使用字符串 + AssistantMessage response1 = agent.call("你好"); + + // 使用 UserMessage + UserMessage userMsg = new UserMessage("帮我写一首诗"); + AssistantMessage response2 = agent.call(userMsg); + + // 使用消息列表 + List messages = List.of( + new UserMessage("我喜欢春天"), + new UserMessage("写一首关于春天的诗") + ); + AssistantMessage response3 = agent.call(messages); + } + + // ==================== Main 方法 ==================== + + public static void main(String[] args) { + System.out.println("=== Messages Tutorial Examples ==="); + System.out.println("注意:需要设置 AI_DASHSCOPE_API_KEY 环境变量\n"); + + try { +// System.out.println("\n--- 示例1:基础消息使用 ---"); +// basicMessageUsage(); +// +// System.out.println("\n--- 示例2:文本提示使用 ---"); +// textPromptUsage(); +// +// System.out.println("\n--- 示例3:消息提示使用 ---"); +// messagePromptUsage(); +// +// System.out.println("\n--- 示例4:基础系统消息 ---"); +// basicSystemMessage(); +// +// System.out.println("\n--- 示例5:详细系统消息 ---"); +// detailedSystemMessage(); +// +// System.out.println("\n--- 示例6:文本用户消息 ---"); +// textUserMessage(); +// +// System.out.println("\n--- 示例7:用户消息元数据 ---"); +// userMessageMetadata(); +// +// System.out.println("\n--- 示例8:多模态图像消息 ---"); +// multimodalImageMessage(); +// +// System.out.println("\n--- 示例9:基础助手消息 ---"); +// basicAssistantMessage(); +// +// System.out.println("\n--- 示例10:手动助手消息 ---"); +// manualAssistantMessage(); +// +// System.out.println("\n--- 示例11:工具调用在助手消息中 ---"); +// toolCallsInAssistantMessage(); +// +// System.out.println("\n--- 示例12:Token 使用 ---"); +// tokenUsage(); +// +// System.out.println("\n--- 示例13:流式消息 ---"); +// streamingMessages(); +// +// System.out.println("\n--- 示例14:工具响应消息 ---"); +// toolResponseMessage(); +// +// System.out.println("\n--- 示例15:从 URL 输入图像 ---"); +// imageInputFromURL(); +// +// System.out.println("\n--- 示例16:从文件输入图像 ---"); + imageInputFromFile(); +// +// System.out.println("\n--- 示例17:音频输入 ---"); +// audioInput(); +// +// System.out.println("\n--- 示例18:视频输入 ---"); +// videoInput(); +// +// System.out.println("\n--- 示例19:基础对话示例 ---"); +// basicConversationExample(); +// +// System.out.println("\n--- 示例20:构建器模式 ---"); +// builderPattern(); +// + System.out.println("\n=== 所有示例执行完成 ==="); + } + catch (Exception e) { + System.err.println("执行示例时发生错误: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/ModelsExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/ModelsExample.java new file mode 100644 index 00000000..41541c5b --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/ModelsExample.java @@ -0,0 +1,457 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.tutorials; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; + +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; + +import java.util.List; + +import reactor.core.publisher.Flux; + +/** + * Models Tutorial - 完整代码示例 + * 展示如何使用Chat Model API与各种AI模型交互 + * + * 来源:models.md + */ +public class ModelsExample { + + // ==================== DashScopeChatModel ==================== + + /** + * 示例1:创建 ChatModel + */ + public static void createChatModel() { + // 创建 DashScope API 实例 + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 创建 ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + } + + /** + * 示例2:简单调用 + */ + public static void simpleCall() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 使用字符串直接调用 + String response = chatModel.call("介绍一下Spring框架"); + System.out.println(response); + } + + /** + * 示例3:使用 Prompt + */ + public static void usePrompt() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建 Prompt + Prompt prompt = new Prompt(new UserMessage("解释什么是微服务架构")); + + // 调用并获取响应 + ChatResponse response = chatModel.call(prompt); + String answer = response.getResult().getOutput().getText(); + System.out.println(answer); + } + + // ==================== 配置选项 ==================== + + /** + * 示例4:使用 ChatOptions + */ + public static void useChatOptions() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + DashScopeChatOptions options = DashScopeChatOptions.builder() + .withModel("qwen-plus") // 模型名称 + .withTemperature(0.7) // 温度参数 + .withMaxToken(2000) // 最大令牌数 + .withTopP(0.9) // Top-P 采样 + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(options) + .build(); + } + + /** + * 示例5:运行时覆盖选项 + */ + public static void runtimeOptionsOverride() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建带有特定选项的 Prompt + DashScopeChatOptions runtimeOptions = DashScopeChatOptions.builder() + .withTemperature(0.3) // 更低的温度,更确定的输出 + .withMaxToken(500) + .build(); + + Prompt prompt = new Prompt( + new UserMessage("用一句话总结Java的特点"), + runtimeOptions + ); + + ChatResponse response = chatModel.call(prompt); + } + + // ==================== 流式响应 ==================== + + /** + * 示例6:流式响应 + */ + public static void streamingResponse() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 使用流式 API + Flux responseStream = chatModel.stream( + new Prompt("详细解释Spring Boot的自动配置原理") + ); + + // 订阅并处理流式响应 + responseStream.subscribe( + chatResponse -> { + String content = chatResponse.getResult() + .getOutput() + .getText(); + System.out.print(content); + }, + error -> System.err.println("错误: " + error.getMessage()), + () -> System.out.println("\n流式响应完成") + ); + } + + // ==================== 多轮对话 ==================== + + /** + * 示例7:多轮对话 + */ + public static void multiTurnConversation() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建对话历史 + List messages = List.of( + new SystemMessage("你是一个Java专家"), + new UserMessage("什么是Spring Boot?"), + new AssistantMessage("Spring Boot是..."), + new UserMessage("它有什么优势?") + ); + + Prompt prompt = new Prompt(messages); + ChatResponse response = chatModel.call(prompt); + } + + // ==================== 函数调用 ==================== + + /** + * 示例8:函数调用 + */ + public static void functionCalling() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 定义函数 + ToolCallback weatherFunction = FunctionToolCallback.builder("getWeather", (city) -> { + // 实际的天气查询逻辑 + return "晴朗,25°C"; + }) + .description("获取指定城市的天气") + .inputType(String.class) + .build(); + + // 使用函数 + DashScopeChatOptions options = DashScopeChatOptions.builder() + .withToolCallbacks(List.of(weatherFunction)) + .build(); + + Prompt prompt = new Prompt("北京的天气怎么样?", options); + ChatResponse response = chatModel.call(prompt); + } + + // ==================== 与 ReactAgent 集成 ==================== + + /** + * 示例9:与 ReactAgent 集成 + */ + public static void integrationWithReactAgent() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .systemPrompt("你是一个有帮助的AI助手") + .build(); + + // 调用 Agent + AssistantMessage response = agent.call("帮我分析这个问题"); + } + + // ==================== 高级配置示例 ==================== + + /** + * 示例10:完整的配置示例 + */ + public static void comprehensiveConfiguration() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 配置各种选项 + DashScopeChatOptions options = DashScopeChatOptions.builder() + .withModel("qwen-max") // 使用旗舰版模型 + .withTemperature(0.7) // 控制随机性 + .withMaxToken(4000) // 最大输出长度 + .withTopP(0.9) // 核采样 + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(options) + .build(); + + // 创建复杂的对话 + List messages = List.of( + new SystemMessage("你是一位资深的软件架构师,精通微服务和云原生技术。"), + new UserMessage("如何设计一个高可用的微服务系统?") + ); + + Prompt prompt = new Prompt(messages); + ChatResponse response = chatModel.call(prompt); + + System.out.println("Response: " + response.getResult().getOutput().getText()); + } + + /** + * 示例11:不同模型的使用 + */ + public static void differentModelsUsage() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // qwen-turbo: 通义千问超大规模语言模型 + ChatModel turboModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(DashScopeChatOptions.builder() + .withModel("qwen-turbo") + .build()) + .build(); + + // qwen-plus: 通义千问增强版 + ChatModel plusModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(DashScopeChatOptions.builder() + .withModel("qwen-plus") + .build()) + .build(); + + // qwen-max: 通义千问旗舰版 + ChatModel maxModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(DashScopeChatOptions.builder() + .withModel("qwen-max") + .build()) + .build(); + + // 使用不同的模型 + String question = "什么是人工智能?"; + String turboResponse = turboModel.call(question); + String plusResponse = plusModel.call(question); + String maxResponse = maxModel.call(question); + } + + /** + * 示例12:错误处理 + */ + public static void errorHandling() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + try { + ChatResponse response = chatModel.call(new Prompt("你好")); + System.out.println("Response: " + response.getResult().getOutput().getText()); + } + catch (Exception e) { + System.err.println("Error calling model: " + e.getMessage()); + // 处理错误,例如重试、降级等 + } + } + + /** + * 示例13:温度参数的影响 + */ + public static void temperatureEffect() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + String question = "给我讲一个有趣的故事"; + + // 低温度 - 更确定、更保守的输出 + ChatModel conservativeModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(DashScopeChatOptions.builder() + .withTemperature(0.1) + .build()) + .build(); + + // 中温度 - 平衡的输出 + ChatModel balancedModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(DashScopeChatOptions.builder() + .withTemperature(0.7) + .build()) + .build(); + + // 高温度 - 更有创意、更随机的输出 + ChatModel creativeModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(DashScopeChatOptions.builder() + .withTemperature(1.5) + .build()) + .build(); + + String conservativeResponse = conservativeModel.call(question); + String balancedResponse = balancedModel.call(question); + String creativeResponse = creativeModel.call(question); + + System.out.println("Conservative (temp=0.1): " + conservativeResponse); + System.out.println("Balanced (temp=0.7): " + balancedResponse); + System.out.println("Creative (temp=1.5): " + creativeResponse); + } + + // ==================== Main 方法 ==================== + + public static void main(String[] args) { + System.out.println("=== Models Tutorial Examples ==="); + System.out.println("注意:需要设置 AI_DASHSCOPE_API_KEY 环境变量\n"); + + try { + System.out.println("\n--- 示例1:创建 ChatModel ---"); + createChatModel(); + + System.out.println("\n--- 示例2:简单调用 ---"); + simpleCall(); + + System.out.println("\n--- 示例3:使用 Prompt ---"); + usePrompt(); + + System.out.println("\n--- 示例4:使用 ChatOptions ---"); + useChatOptions(); + + System.out.println("\n--- 示例5:运行时选项覆盖 ---"); + runtimeOptionsOverride(); + + System.out.println("\n--- 示例6:流式响应 ---"); + streamingResponse(); + + System.out.println("\n--- 示例7:多轮对话 ---"); + multiTurnConversation(); + + System.out.println("\n--- 示例8:函数调用 ---"); + functionCalling(); + + System.out.println("\n--- 示例9:与 ReactAgent 集成 ---"); + integrationWithReactAgent(); + + System.out.println("\n--- 示例10:综合配置 ---"); + comprehensiveConfiguration(); + + System.out.println("\n--- 示例11:不同模型使用 ---"); + differentModelsUsage(); + + System.out.println("\n--- 示例12:错误处理 ---"); + errorHandling(); + + System.out.println("\n--- 示例13:温度效果 ---"); + temperatureEffect(); + + System.out.println("\n=== 所有示例执行完成 ==="); + } + catch (Exception e) { + System.err.println("执行示例时发生错误: " + e.getMessage()); + e.printStackTrace(); + } + } + +} diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/StructuredOutputExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/StructuredOutputExample.java new file mode 100644 index 00000000..21ee704c --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/StructuredOutputExample.java @@ -0,0 +1,608 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.tutorials; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; + +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.converter.BeanOutputConverter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Structured Output Tutorial - 完整代码示例 + * 展示如何让Agent返回特定格式的结构化数据 + * + * 来源:structured-output.md + */ +public class StructuredOutputExample { + + // ==================== 基础类定义 ==================== + + /** + * 示例1:基本 JSON Schema + */ + public static void basicJsonSchema() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // Use BeanOutputConverter to generate outputSchema + BeanOutputConverter outputConverter = new BeanOutputConverter<>(ContactInfo.class); + String format = outputConverter.getFormat(); + + ReactAgent agent = ReactAgent.builder() + .name("contact_extractor") + .model(chatModel) + .outputSchema(format) + .build(); + + AssistantMessage result = agent.call( + "从以下信息提取联系方式:张三,zhangsan@example.com,(555) 123-4567" + ); + + System.out.println(result.getText()); + // 输出: {"name": "张三", "email": "zhangsan@example.com", "phone": "(555) 123-4567"} + } + + /** + * 示例2:复杂嵌套 Schema + */ + public static void complexNestedSchema() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // Use BeanOutputConverter to generate outputSchema + BeanOutputConverter outputConverter = new BeanOutputConverter<>(ProductReview.class); + String format = outputConverter.getFormat(); + + ReactAgent agent = ReactAgent.builder() + .name("review_analyzer") + .model(chatModel) + .outputSchema(format) + .build(); + + AssistantMessage result = agent.call( + "分析评价:这个产品很棒,5星好评。配送快速,但价格稍贵。" + ); + + System.out.println(result.getText()); + // 输出: {"rating": 5, "sentiment": "正面", "keyPoints": [...], "details": {...}} + } + + /** + * 示例3:结构化分析 Schema + */ + public static void structuredAnalysisSchema() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // Use BeanOutputConverter to generate outputSchema + BeanOutputConverter outputConverter = new BeanOutputConverter<>(TextAnalysis.class); + String format = outputConverter.getFormat(); + + ReactAgent agent = ReactAgent.builder() + .name("text_analyzer") + .model(chatModel) + .outputSchema(format) + .build(); + + AssistantMessage result = agent.call( + "分析这段文字:昨天,李明在北京参加了阿里巴巴公司的技术大会,感受到了创新的力量。" + ); + + System.out.println(result.getText()); + } + + // ==================== 输出 Schema 策略 ==================== + + /** + * 示例4:使用 outputType - ContactInfo + */ + public static void outputTypeContactInfo() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("contact_extractor") + .model(chatModel) + .outputType(ContactInfo.class) + .saver(new MemorySaver()) + .build(); + + AssistantMessage result = agent.call( + "从以下信息提取联系方式:张三,zhangsan@example.com,(555) 123-4567" + ); + + System.out.println(result.getText()); + } + + /** + * 示例5:使用 outputType - ProductReview + */ + public static void outputTypeProductReview() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("review_analyzer") + .model(chatModel) + .outputType(ProductReview.class) + .saver(new MemorySaver()) + .build(); + + AssistantMessage result = agent.call( + "分析评价:这个产品很棒,5星好评。配送快速,但价格稍贵。" + ); + + System.out.println(result.getText()); + } + + /** + * 示例6:使用 outputType - TextAnalysis + */ + public static void outputTypeTextAnalysis() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("text_analyzer") + .model(chatModel) + .outputType(TextAnalysis.class) + .saver(new MemorySaver()) + .build(); + + AssistantMessage result = agent.call( + "分析这段文字:昨天,李明在北京参加了阿里巴巴公司的技术大会,感受到了创新的力量。" + ); + + System.out.println(result.getText()); + } + + // ==================== 输出类型策略 ==================== + + /** + * 示例7:Try-Catch 模式 + */ + public static void tryCatchPattern() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("data_extractor") + .model(chatModel) + .outputType(ContactInfo.class) + .build(); + + try { + AssistantMessage result = agent.call("提取数据"); + ObjectMapper mapper = new ObjectMapper(); + ContactInfo data = mapper.readValue(result.getText(), ContactInfo.class); + // 处理数据 + System.out.println("Name: " + data.getName()); + } + catch (JsonProcessingException | GraphRunnerException e) { + System.err.println("JSON解析失败: " + e.getMessage()); + // 回退处理 + } + } + + /** + * 示例8:验证模式 + */ + public static void validationPattern() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("validated_agent") + .model(chatModel) + .outputType(ValidatedOutput.class) + .build(); + + try { + AssistantMessage result = agent.call("生成评价"); + ObjectMapper mapper = new ObjectMapper(); + ValidatedOutput output = mapper.readValue(result.getText(), ValidatedOutput.class); + output.validate(); // 如果无效则抛出异常 + System.out.println("Valid output: " + output.getTitle()); + } + catch (Exception e) { + System.err.println("Validation failed: " + e.getMessage()); + } + } + + /** + * 示例9:重试模式 + */ + public static void retryPattern() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ReactAgent agent = ReactAgent.builder() + .name("retry_agent") + .model(chatModel) + .outputType(ContactInfo.class) + .build(); + + int maxRetries = 3; + ContactInfo data = null; + ObjectMapper mapper = new ObjectMapper(); + + for (int i = 0; i < maxRetries; i++) { + try { + AssistantMessage result = agent.call("提取数据"); + data = mapper.readValue(result.getText(), ContactInfo.class); + break; // 成功 + } + catch (Exception e) { + if (i == maxRetries - 1) { + throw new RuntimeException("多次尝试后仍然失败", e); + } + System.out.println("第" + (i + 1) + "次尝试失败,重试中..."); + } + } + + if (data != null) { + System.out.println("Successfully extracted: " + data.getName()); + } + } + + // ==================== 错误处理 ==================== + + /** + * 示例10:完整的结构化输出示例 + */ + public static void comprehensiveExample() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 使用 outputType + ReactAgent typeAgent = ReactAgent.builder() + .name("type_agent") + .model(chatModel) + .outputType(ContactInfo.class) + .saver(new MemorySaver()) + .build(); + + // 使用 outputSchema (通过 BeanOutputConverter 生成) + BeanOutputConverter outputConverter = new BeanOutputConverter<>(ContactInfo.class); + String format = outputConverter.getFormat(); + + ReactAgent schemaAgent = ReactAgent.builder() + .name("schema_agent") + .model(chatModel) + .outputSchema(format) + .saver(new MemorySaver()) + .build(); + + String input = "联系人:王五,wangwu@example.com,13800138000"; + + // 使用 outputType + AssistantMessage typeResult = typeAgent.call(input); + System.out.println("Type-based: " + typeResult.getText()); + + // 使用 outputSchema + AssistantMessage schemaResult = schemaAgent.call(input); + System.out.println("Schema-based: " + schemaResult.getText()); + } + + public static void main(String[] args) { + System.out.println("=== Structured Output Tutorial Examples ==="); + System.out.println("注意:需要设置 AI_DASHSCOPE_API_KEY 环境变量\n"); + + try { +// System.out.println("\n--- 示例1:基础 JSON Schema ---"); +// basicJsonSchema(); + + System.out.println("\n--- 示例2:复杂嵌套 Schema ---"); + // complexNestedSchema(); +// +// System.out.println("\n--- 示例3:结构化分析 Schema ---"); + structuredAnalysisSchema(); +// +// System.out.println("\n--- 示例4:OutputType - 联系信息 ---"); +// outputTypeContactInfo(); +// +// System.out.println("\n--- 示例5:OutputType - 产品评论 ---"); +// outputTypeProductReview(); +// +// System.out.println("\n--- 示例6:OutputType - 文本分析 ---"); +// outputTypeTextAnalysis(); +// +// System.out.println("\n--- 示例7:Try-Catch 模式 ---"); +// tryCatchPattern(); +// +// System.out.println("\n--- 示例8:验证模式 ---"); +// validationPattern(); +// +// System.out.println("\n--- 示例9:重试模式 ---"); +// retryPattern(); +// +// System.out.println("\n--- 示例10:综合示例 ---"); +// comprehensiveExample(); + + System.out.println("\n=== 所有示例执行完成 ==="); + } + catch (Exception e) { + System.err.println("执行示例时发生错误: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 联系信息输出类 + */ + public static class ContactInfo { + private String name; + private String email; + private String phone; + + // Getters and Setters + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + } + + /** + * 产品评价输出类 + */ + public static class ProductReview { + private int rating; + private String sentiment; + private String[] keyPoints; + private ReviewDetails details; + + public int getRating() { + return rating; + } + + public void setRating(int rating) { + this.rating = rating; + } + + public String getSentiment() { + return sentiment; + } + + public void setSentiment(String sentiment) { + this.sentiment = sentiment; + } + + public String[] getKeyPoints() { + return keyPoints; + } + + public void setKeyPoints(String[] keyPoints) { + this.keyPoints = keyPoints; + } + + public ReviewDetails getDetails() { + return details; + } + + public void setDetails(ReviewDetails details) { + this.details = details; + } + + public static class ReviewDetails { + private String[] pros; + private String[] cons; + + public String[] getPros() { + return pros; + } + + public void setPros(String[] pros) { + this.pros = pros; + } + + public String[] getCons() { + return cons; + } + + public void setCons(String[] cons) { + this.cons = cons; + } + } + } + + // ==================== 综合示例 ==================== + + /** + * 文本分析输出类 + */ + public static class TextAnalysis { + private String summary; + private String[] keywords; + private String sentiment; + private Entities entities; + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + public String[] getKeywords() { + return keywords; + } + + public void setKeywords(String[] keywords) { + this.keywords = keywords; + } + + public String getSentiment() { + return sentiment; + } + + public void setSentiment(String sentiment) { + this.sentiment = sentiment; + } + + public Entities getEntities() { + return entities; + } + + public void setEntities(Entities entities) { + this.entities = entities; + } + + public static class Entities { + private String[] persons; + private String[] locations; + private String[] organizations; + + public String[] getPersons() { + return persons; + } + + public void setPersons(String[] persons) { + this.persons = persons; + } + + public String[] getLocations() { + return locations; + } + + public void setLocations(String[] locations) { + this.locations = locations; + } + + public String[] getOrganizations() { + return organizations; + } + + public void setOrganizations(String[] organizations) { + this.organizations = organizations; + } + } + } + + // ==================== Main 方法 ==================== + + /** + * 验证输出类 + */ + public static class ValidatedOutput { + private String title; + private Integer rating; + + public void validate() throws IllegalArgumentException { + if (title == null || title.isEmpty()) { + throw new IllegalArgumentException("标题不能为空"); + } + if (rating != null && (rating < 1 || rating > 5)) { + throw new IllegalArgumentException("评分必须在1-5之间"); + } + } + + // Getter 和 Setter 方法 + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Integer getRating() { + return rating; + } + + public void setRating(Integer rating) { + this.rating = rating; + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/ToolsExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/ToolsExample.java new file mode 100644 index 00000000..c59fa8a3 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/framework/tutorials/ToolsExample.java @@ -0,0 +1,957 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.framework.tutorials; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.agent.ReactAgent; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphRunnerException; + +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.ai.tool.function.FunctionToolCallback; +import org.springframework.ai.tool.resolution.StaticToolCallbackResolver; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Tools Tutorial - 完整代码示例 + * 展示如何创建和使用Tools让Agent与外部系统交互 + * + * 来源:tools.md + */ +public class ToolsExample { + + // ==================== 基础工具定义 ==================== + + /** + * 示例1:编程方式规范 - FunctionToolCallback + */ + public static void programmaticToolSpecification() { + ToolCallback toolCallback = FunctionToolCallback + .builder("currentWeather", new WeatherService()) + .description("Get the weather in location") + .inputType(WeatherRequest.class) + .build(); + } + + /** + * 示例2:添加工具到 ChatClient(使用编程规范) + */ + public static void addToolToChatClient() { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ToolCallback toolCallback = FunctionToolCallback + .builder("currentWeather", new WeatherService()) + .description("Get the weather in location") + .inputType(WeatherRequest.class) + .build(); + + // Note: ChatClient usage would be shown here in actual implementation + // This is a simplified example + } + + /** + * 示例3:自定义工具名称 + */ + public static void customToolName() { + ToolCallback searchTool = FunctionToolCallback + .builder("web_search", new SearchFunction()) // 自定义名称 + .description("Search the web for information") + .inputType(String.class) + .build(); + + System.out.println(searchTool.getToolDefinition().name()); // web_search + } + + /** + * 示例4:自定义工具描述 + */ + public static void customToolDescription() { + ToolCallback calculatorTool = FunctionToolCallback + .builder("calculator", new CalculatorFunction()) + .description("Performs arithmetic calculations. Use this for any math problems.") + .inputType(String.class) + .build(); + } + + /** + * 示例5:高级模式定义 + */ + public static void advancedSchemaDefinition() { + ToolCallback weatherTool = FunctionToolCallback + .builder("get_weather", new WeatherFunction()) + .description("Get current weather and optional forecast") + .inputType(WeatherInput.class) + .build(); + } + + /** + * 示例6:访问状态 + */ + public static void accessingState() { + // 创建工具 + ToolCallback summaryTool = FunctionToolCallback + .builder("summarize_conversation", new ConversationSummaryTool()) + .description("Summarize the conversation so far") + .inputType(String.class) + .build(); + } + + // ==================== 自定义工具属性 ==================== + + /** + * 示例7:访问上下文 + */ + public static void accessingContext() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + ToolCallback accountTool = FunctionToolCallback + .builder("get_account_info", new AccountInfoTool()) + .description("Get the current user's account information") + .inputType(String.class) + .build(); + + // 在 ReactAgent 中使用 + ReactAgent agent = ReactAgent.builder() + .name("financial_assistant") + .model(chatModel) + .tools(accountTool) + .systemPrompt("You are a financial assistant.") + .build(); + + // 调用时传递上下文 + RunnableConfig config = RunnableConfig.builder() + .addMetadata("user_id", "user123") + .build(); + + agent.call("question", config); + } + + /** + * 示例8:使用存储访问跨对话的持久数据 + */ + public static void accessingMemoryStore() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 配置持久化存储 + MemorySaver memorySaver = new MemorySaver(); + + // 创建工具 + ToolCallback saveUserInfoTool = createSaveUserInfoTool(); + ToolCallback getUserInfoTool = createGetUserInfoTool(); + + // 创建带有持久化记忆的 Agent + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .tools(saveUserInfoTool, getUserInfoTool) + .saver(memorySaver) + .build(); + + // 第一个会话:保存用户信息 + RunnableConfig config1 = RunnableConfig.builder() + .threadId("session_1") + .build(); + + agent.call("Save user: userid: abc123, name: Foo, age: 25, email: foo@example.com", config1); + + // 第二个会话:获取用户信息,注意这里用的是不同的 threadId + RunnableConfig config2 = RunnableConfig.builder() + .threadId("session_2") + .build(); + + agent.call("Get user info for user with id 'abc123'", config2); + } + + /** + * 示例9:在 ReactAgent 中使用工具 + */ + public static void toolsInReactAgent() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建工具 + ToolCallback weatherTool = FunctionToolCallback + .builder("get_weather", new WeatherFunction()) + .description("Get weather for a given city") + .inputType(WeatherInput.class) + .build(); + + ToolCallback searchTool = FunctionToolCallback + .builder("search", new SearchFunction()) + .description("Search for information") + .inputType(String.class) + .build(); + + // 创建带有工具的 Agent + ReactAgent agent = ReactAgent.builder() + .name("my_agent") + .model(chatModel) + .tools(weatherTool, searchTool) + .systemPrompt("You are a helpful assistant with access to weather and search tools.") + .saver(new MemorySaver()) + .build(); + + // 使用 Agent + AssistantMessage response = agent.call("What's the weather like in San Francisco?"); + System.out.println(response.getText()); + } + + /** + * 示例10:完整的工具使用示例(使用 tools 方法) + */ + public static void comprehensiveToolExample() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 定义多个工具 + ToolCallback weatherTool = FunctionToolCallback + .builder("get_weather", new WeatherFunction()) + .description("Get current weather and optional forecast for a city") + .inputType(WeatherInput.class) + .build(); + + ToolCallback calculatorTool = FunctionToolCallback + .builder("calculator", new CalculatorFunction()) + .description("Perform arithmetic calculations") + .inputType(String.class) + .build(); + + ToolCallback searchTool = FunctionToolCallback + .builder("web_search", new SearchFunction()) + .description("Search the web for information") + .inputType(String.class) + .build(); + + // 创建 Agent + ReactAgent agent = ReactAgent.builder() + .name("multi_tool_agent") + .model(chatModel) + .tools(weatherTool, calculatorTool, searchTool) + .systemPrompt(""" + You are a helpful AI assistant with access to multiple tools: + - Weather information + - Calculator for math operations + - Web search for general information + + Use the appropriate tool based on the user's question. + """) + .saver(new MemorySaver()) + .build(); + + // 使用不同的工具 + RunnableConfig config = RunnableConfig.builder() + .threadId("session_1") + .build(); + + agent.call("What's the weather in New York?", config); + agent.call("Calculate 25 * 4 + 10", config); + agent.call("Search for latest AI news", config); + } + + /** + * 示例11:使用 methodTools - 基于 @Tool 注解的方法工具 + */ + public static void methodToolsExample() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建带有 @Tool 注解方法的工具对象 + CalculatorTools calculatorTools = new CalculatorTools(); + + // 使用 methodTools 方法,传入带有 @Tool 注解方法的对象 + ReactAgent agent = ReactAgent.builder() + .name("calculator_agent") + .model(chatModel) + .description("An agent that can perform calculations") + .instruction("You are a helpful calculator assistant. Use the available tools to perform calculations.") + .methodTools(calculatorTools) // 传入带有 @Tool 注解方法的对象 + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("method_tools_session") + .build(); + + agent.call("What is 15 + 27?", config); + agent.call("What is 8 * 9?", config); + } + + /** + * 示例12:使用多个 methodTools 对象 + */ + public static void multipleMethodToolsExample() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建多个工具对象 + CalculatorTools calculatorTools = new CalculatorTools(); + WeatherTools weatherTools = new WeatherTools(); + + // 可以传入多个 methodTools 对象 + ReactAgent agent = ReactAgent.builder() + .name("multi_method_tool_agent") + .model(chatModel) + .description("An agent with multiple method-based tools") + .instruction("You are a helpful assistant with calculator and weather tools.") + .methodTools(calculatorTools, weatherTools) // 传入多个工具对象 + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("multi_method_tools_session") + .build(); + + agent.call("What is 10 * 8 and what's the weather in Beijing?", config); + } + + /** + * 示例13:使用 ToolCallbackProvider + */ + public static void toolCallbackProviderExample() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建工具 + ToolCallback searchTool = FunctionToolCallback.builder("search", new SearchToolWithContext()) + .description("Search for information") + .inputType(String.class) + .build(); + + // 创建 ToolCallbackProvider + ToolCallbackProvider toolProvider = new CustomToolCallbackProvider(List.of(searchTool)); + + // 使用 toolCallbackProviders 方法 + ReactAgent agent = ReactAgent.builder() + .name("search_agent") + .model(chatModel) + .description("An agent that can search for information") + .instruction("You are a helpful assistant with search capabilities.") + .toolCallbackProviders(toolProvider) // 使用 ToolCallbackProvider + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("tool_provider_session") + .build(); + + agent.call("Search for information about Spring AI", config); + } + + /** + * 示例14:使用 toolNames 和 resolver(必须配合使用) + */ + public static void toolNamesWithResolverExample() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建工具(使用复合类型) + ToolCallback searchTool = FunctionToolCallback.builder("search", new SearchFunctionWithRequest()) + .description("Search for information") + .inputType(SearchRequest.class) + .build(); + + ToolCallback calculatorTool = FunctionToolCallback.builder("calculator", new CalculatorFunctionWithRequest()) + .description("Perform arithmetic calculations") + .inputType(CalculatorRequest.class) + .build(); + + // 创建 StaticToolCallbackResolver,包含所有工具 + StaticToolCallbackResolver resolver = new StaticToolCallbackResolver( + List.of(calculatorTool, searchTool)); + + // 使用 toolNames 指定要使用的工具名称,必须配合 resolver 使用 + ReactAgent agent = ReactAgent.builder() + .name("multi_tool_agent") + .model(chatModel) + .description("An agent with multiple tools") + .instruction("You are a helpful assistant with access to calculator and search tools.") + .toolNames("calculator", "search") // 使用工具名称而不是 ToolCallback 实例 + .resolver(resolver) // 必须提供 resolver 来解析工具名称 + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("tool_names_session") + .build(); + + agent.call("Calculate 25 + 4 and then search for information about the result", config); + } + + /** + * 示例15:使用 resolver 直接解析工具 + */ + public static void resolverExample() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建工具 + ToolCallback calculatorTool = FunctionToolCallback.builder("calculator", new CalculatorFunctionWithContext()) + .description("Perform arithmetic calculations") + .inputType(String.class) + .build(); + + // 创建 resolver + StaticToolCallbackResolver resolver = new StaticToolCallbackResolver( + List.of(calculatorTool)); + + // 使用 resolver,可以直接在 tools 中使用,也可以仅通过 resolver 提供 + ReactAgent agent = ReactAgent.builder() + .name("resolver_agent") + .model(chatModel) + .description("An agent using ToolCallbackResolver") + .instruction("You are a helpful calculator assistant.") + .tools(calculatorTool) // 直接指定工具 + .resolver(resolver) // 同时设置 resolver 供工具节点使用 + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("resolver_session") + .build(); + + agent.call("What is 100 divided by 4?", config); + } + + /** + * 示例16:组合使用多种工具提供方式 + */ + public static void combinedToolProvisionExample() throws GraphRunnerException { + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // Method tools + CalculatorTools calculatorTools = new CalculatorTools(); + + // Direct tool + ToolCallback searchTool = FunctionToolCallback.builder("search", new SearchToolWithContext()) + .description("Search for information") + .inputType(String.class) + .build(); + + // ToolCallbackProvider + ToolCallbackProvider toolProvider = new CustomToolCallbackProvider(List.of(searchTool)); + + // 组合使用多种方式 + ReactAgent agent = ReactAgent.builder() + .name("combined_tool_agent") + .model(chatModel) + .description("An agent with multiple tool provision methods") + .instruction("You are a helpful assistant with calculator and search capabilities.") + .methodTools(calculatorTools) // Method-based tools + .toolCallbackProviders(toolProvider) // Provider-based tools + .tools(searchTool) // Direct tools + .saver(new MemorySaver()) + .build(); + + RunnableConfig config = RunnableConfig.builder() + .threadId("combined_session") + .build(); + + agent.call("Calculate 50 + 75 and search for information about mathematics", config); + } + + // ==================== 高级模式定义 ==================== + + /** + * 创建保存用户信息工具 + */ + private static ToolCallback createSaveUserInfoTool() { + return FunctionToolCallback.builder("save_user_info", (String input) -> { + // 简化的实现 + return "User info saved: " + input; + }) + .description("Save user information") + .inputType(String.class) + .build(); + } + + /** + * 创建获取用户信息工具 + */ + private static ToolCallback createGetUserInfoTool() { + return FunctionToolCallback.builder("get_user_info", (String userId) -> { + // 简化的实现 + return "User info for: " + userId; + }) + .description("Get user information by ID") + .inputType(String.class) + .build(); + } + + public static void main(String[] args) { + System.out.println("=== Tools Tutorial Examples ==="); + System.out.println("注意:需要设置 AI_DASHSCOPE_API_KEY 环境变量\n"); + + try { + System.out.println("\n--- 示例1:编程式工具规范 ---"); + programmaticToolSpecification(); + +// System.out.println("\n--- 示例2:添加工具到 ChatClient ---"); +// addToolToChatClient(); +// +// System.out.println("\n--- 示例3:自定义工具名称 ---"); +// customToolName(); +// +// System.out.println("\n--- 示例4:自定义工具描述 ---"); +// customToolDescription(); +// +// System.out.println("\n--- 示例5:高级 Schema 定义 ---"); +// advancedSchemaDefinition(); +// +// System.out.println("\n--- 示例6:访问状态 ---"); +// accessingState(); +// +// System.out.println("\n--- 示例7:访问上下文 ---"); +// accessingContext(); +// +// System.out.println("\n--- 示例8:访问内存存储 ---"); +// accessingMemoryStore(); +// +// System.out.println("\n--- 示例9:ReactAgent 中的工具 ---"); +// toolsInReactAgent(); +// +// System.out.println("\n--- 示例10:综合工具示例(tools 方法) ---"); +// comprehensiveToolExample(); +// +// System.out.println("\n--- 示例11:使用 methodTools(@Tool 注解) ---"); +// methodToolsExample(); +// +// System.out.println("\n--- 示例12:多个 methodTools 对象 ---"); +// multipleMethodToolsExample(); +// +// System.out.println("\n--- 示例13:使用 ToolCallbackProvider ---"); +// toolCallbackProviderExample(); +// +// System.out.println("\n--- 示例14:使用 toolNames 和 resolver ---"); +// toolNamesWithResolverExample(); +// +// System.out.println("\n--- 示例15:使用 resolver ---"); +// resolverExample(); +// +// System.out.println("\n--- 示例16:组合使用多种工具提供方式 ---"); +// combinedToolProvisionExample(); +// +// System.out.println("\n=== 所有示例执行完成 ==="); + } + catch (Exception e) { + System.err.println("执行示例时发生错误: " + e.getMessage()); + e.printStackTrace(); + } + } + + public enum Unit {C, F} + + // ==================== 访问上下文 ==================== + + public enum UnitType {CELSIUS, FAHRENHEIT} + + /** + * 天气服务 + */ + public static class WeatherService implements Function { + @Override + public WeatherResponse apply(WeatherRequest request) { + return new WeatherResponse(30.0, Unit.C); + } + } + + // ==================== Context(上下文) ==================== + + public record WeatherRequest( + @ToolParam(description = "城市或坐标") String location, + Unit unit + ) { } + + public record WeatherResponse(double temp, Unit unit) { } + + // ==================== Memory(存储) ==================== + + /** + * 搜索函数 + */ + public static class SearchFunction implements Function { + @Override + public String apply(String query) { + return "Search results for: " + query; + } + } + + // ==================== 在 ReactAgent 中使用工具 ==================== + + /** + * 计算器函数 + */ + public static class CalculatorFunction implements Function { + @Override + public String apply(String expression) { + // 简化的计算逻辑 + return "Result: " + expression; + } + } + + // ==================== 完整示例 ==================== + + /** + * 天气输入(使用记录类) + */ + public record WeatherInput( + @ToolParam(description = "City name or coordinates") String location, + @ToolParam(description = "Temperature unit preference") Unit units, + @ToolParam(description = "Include 5-day forecast") boolean includeForecast + ) { } + + // ==================== 辅助方法 ==================== + + /** + * 天气函数(高级版) + */ + public static class WeatherFunction implements Function { + @Override + public String apply(WeatherInput input) { + double temp = input.units() == Unit.F ? 22 : 72; + String result = String.format( + "Current weather in %s: %.0f degrees %s", + input.location(), + temp, + input.units().toString().substring(0, 1).toUpperCase() + ); + + if (input.includeForecast()) { + result += "\nNext 5 days: Sunny"; + } + + return result; + } + } + + /** + * 对话摘要工具 + */ + public static class ConversationSummaryTool implements BiFunction { + + @Override + public String apply(String input, ToolContext toolContext) { + OverAllState state = (OverAllState) toolContext.getContext().get("state"); + RunnableConfig config = (RunnableConfig) toolContext.getContext().get("config"); + + // 从state中获取消息 + Optional messagesOpt = state.value("messages"); + List messages = messagesOpt.isPresent() + ? (List) messagesOpt.get() + : new ArrayList<>(); + + if (messages.isEmpty()) { + return "No conversation history available"; + } + + long userMsgs = messages.stream() + .filter(m -> m.getMessageType().getValue().equals("user")) + .count(); + long aiMsgs = messages.stream() + .filter(m -> m.getMessageType().getValue().equals("assistant")) + .count(); + long toolMsgs = messages.stream() + .filter(m -> m.getMessageType().getValue().equals("tool")) + .count(); + + return String.format( + "Conversation has %d user messages, %d AI responses, and %d tool results", + userMsgs, aiMsgs, toolMsgs + ); + } + } + + // ==================== Main 方法 ==================== + + /** + * 账户信息工具 + */ + public static class AccountInfoTool implements BiFunction { + + private static final Map> USER_DATABASE = Map.of( + "user123", Map.of( + "name", "Alice Johnson", + "account_type", "Premium", + "balance", 5000, + "email", "alice@example.com" + ), + "user456", Map.of( + "name", "Bob Smith", + "account_type", "Standard", + "balance", 1200, + "email", "bob@example.com" + ) + ); + + @Override + public String apply(String query, ToolContext toolContext) { + RunnableConfig config = (RunnableConfig) toolContext.getContext().get("config"); + String userId = (String) config.metadata("user_id").orElse(null); + + if (userId == null) { + return "User ID not provided"; + } + + Map user = USER_DATABASE.get(userId); + if (user != null) { + return String.format( + "Account holder: %s\nType: %s\nBalance: $%d", + user.get("name"), + user.get("account_type"), + user.get("balance") + ); + } + + return "User not found"; + } + } + + // ==================== MethodTools 相关类 ==================== + + /** + * 计算器工具类 - 使用 @Tool 注解 + */ + public static class CalculatorTools { + public static int callCount = 0; + + @Tool(description = "Add two numbers together") + public String add( + @ToolParam(description = "First number") int a, + @ToolParam(description = "Second number") int b) { + callCount++; + return String.valueOf(a + b); + } + + @Tool(description = "Multiply two numbers together") + public String multiply( + @ToolParam(description = "First number") int a, + @ToolParam(description = "Second number") int b) { + callCount++; + return String.valueOf(a * b); + } + + @Tool(description = "Subtract second number from first number") + public String subtract( + @ToolParam(description = "First number") int a, + @ToolParam(description = "Second number") int b) { + callCount++; + return String.valueOf(a - b); + } + } + + /** + * 天气工具类 - 使用 @Tool 注解 + */ + public static class WeatherTools { + @Tool(description = "Get current weather for a location") + public String getWeather(@ToolParam(description = "City name") String city) { + return "Sunny, 25°C in " + city; + } + + @Tool(description = "Get weather forecast for a location") + public String getForecast( + @ToolParam(description = "City name") String city, + @ToolParam(description = "Number of days") int days) { + return String.format("Weather forecast for %s for next %d days: Mostly sunny", city, days); + } + } + + // ==================== ToolCallbackProvider 相关类 ==================== + + /** + * 自定义 ToolCallbackProvider 实现 + */ + public static class CustomToolCallbackProvider implements ToolCallbackProvider { + private final List toolCallbacks; + + public CustomToolCallbackProvider(List toolCallbacks) { + this.toolCallbacks = toolCallbacks; + } + + @Override + public ToolCallback[] getToolCallbacks() { + return toolCallbacks.toArray(new ToolCallback[0]); + } + } + + /** + * 带上下文的搜索工具 + */ + public static class SearchToolWithContext implements BiFunction { + @Override + public String apply(String query, ToolContext toolContext) { + return "Search results for: " + query; + } + } + + // ==================== Resolver 相关类 ==================== + + /** + * 搜索请求类(用于复合类型) + */ + public static class SearchRequest { + @JsonProperty(required = true) + @JsonPropertyDescription("The search query string") + public String query; + + public SearchRequest() { + } + + public SearchRequest(String query) { + this.query = query; + } + } + + /** + * 使用复合类型的搜索函数 + */ + public static class SearchFunctionWithRequest implements BiFunction { + @Override + public String apply(SearchRequest request, ToolContext toolContext) { + return "Search results for: " + request.query; + } + } + + /** + * 计算器请求类(用于复合类型) + */ + public static class CalculatorRequest { + @JsonProperty(required = true) + @JsonPropertyDescription("First number for the calculation") + public int a; + + @JsonProperty(required = true) + @JsonPropertyDescription("Second number for the calculation") + public int b; + + public CalculatorRequest() { + } + + public CalculatorRequest(int a, int b) { + this.a = a; + this.b = b; + } + } + + /** + * 使用复合类型的计算器函数 + */ + public static class CalculatorFunctionWithRequest implements BiFunction { + @Override + public String apply(CalculatorRequest request, ToolContext toolContext) { + return String.valueOf(request.a + request.b); + } + } + + /** + * 带上下文的计算器函数 + */ + public static class CalculatorFunctionWithContext implements BiFunction { + @Override + public String apply(String expression, ToolContext toolContext) { + // 简单的计算解析(用于演示) + if (expression.contains("/")) { + String[] parts = expression.split("/"); + double result = Double.parseDouble(parts[0].trim()) / Double.parseDouble(parts[1].trim()); + return String.valueOf(result); + } + if (expression.contains("*")) { + String[] parts = expression.split("\\*"); + double result = Double.parseDouble(parts[0].trim()) * Double.parseDouble(parts[1].trim()); + return String.valueOf(result); + } + return "Calculation result for: " + expression; + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/QuickStartExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/QuickStartExample.java new file mode 100644 index 00000000..ae0a6014 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/QuickStartExample.java @@ -0,0 +1,666 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.*; +import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; +import com.alibaba.fastjson.JSON; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import reactor.core.publisher.Flux; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * Graph 工作流编排快速入门示例 + * + * 本示例演示如何通过将客服邮件处理流程分解为离散步骤来使用 Spring AI Alibaba Graph 构建智能工作流。 + * + * 示例包含: + * 1. 状态定义(EmailClassification) + * 2. 节点实现(读取邮件、分类意图、搜索文档、Bug跟踪、起草回复、人工审核、发送回复) + * 3. Graph 组装和配置 + * 4. 测试执行 + */ +public class QuickStartExample { + + private static final Logger log = LoggerFactory.getLogger(QuickStartExample.class); + + // ==================== 状态定义 ==================== + + /** + * 邮件分类结构 + */ + public static class EmailClassification { + private String intent; // "question", "bug", "billing", "feature", "complex" + private String urgency; // "low", "medium", "high", "critical" + private String topic; + private String summary; + + public EmailClassification() { + } + + public EmailClassification(String intent, String urgency, String topic, String summary) { + this.intent = intent; + this.urgency = urgency; + this.topic = topic; + this.summary = summary; + } + + public String getIntent() { + return intent; + } + + public void setIntent(String intent) { + this.intent = intent; + } + + public String getUrgency() { + return urgency; + } + + public void setUrgency(String urgency) { + this.urgency = urgency; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + @Override + public String toString() { + return String.format("EmailClassification{intent='%s', urgency='%s', topic='%s', summary='%s'}", + intent, urgency, topic, summary); + } + } + + /** + * 配置状态键策略 + */ + public static KeyStrategyFactory createKeyStrategyFactory() { + return () -> { + HashMap strategies = new HashMap<>(); + strategies.put("email_content", new ReplaceStrategy()); + strategies.put("sender_email", new ReplaceStrategy()); + strategies.put("email_id", new ReplaceStrategy()); + strategies.put("classification", new ReplaceStrategy()); + strategies.put("search_results", new ReplaceStrategy()); + strategies.put("customer_history", new ReplaceStrategy()); + strategies.put("draft_response", new ReplaceStrategy()); + strategies.put("messages", new AppendStrategy()); + strategies.put("next_node", new ReplaceStrategy()); + strategies.put("status", new ReplaceStrategy()); + strategies.put("review_data", new ReplaceStrategy()); + return strategies; + }; + } + + // ==================== 节点实现 ==================== + + /** + * 读取邮件节点 + */ + public static class ReadEmailNode implements NodeAction { + + @Override + public Map apply(OverAllState state) throws Exception { + // 在生产环境中,这将连接到您的邮件服务 + String emailContent = state.value("email_content") + .map(v -> (String) v) + .orElse(""); + + log.info("ReadEmailNode----Processing email: {}", emailContent); + + List messages = new ArrayList<>(); + messages.add("Processing email: " + emailContent); + + return Map.of("messages", messages); + } + } + + /** + * 分类意图节点 + */ + public static class ClassifyIntentNode implements NodeAction { + + private final ChatClient chatClient; + + public ClassifyIntentNode(ChatClient.Builder chatClientBuilder) { + this.chatClient = chatClientBuilder.build(); + } + + @Override + public Map apply(OverAllState state) throws Exception { + String emailContent = state.value("email_content") + .map(v -> (String) v) + .orElseThrow(() -> new IllegalStateException("No email content")); + String senderEmail = state.value("sender_email") + .map(v -> (String) v) + .orElse("unknown"); + + // 按需格式化提示,不存储在状态中 + String classificationPrompt = String.format(""" + 分析这封客户邮件并进行分类: + + 邮件: %s + 发件人: %s + + 提供分类,包括意图、紧急程度、主题和摘要。 + + 意图应该是以下之一: question, bug, billing, feature, complex + 紧急程度应该是以下之一: low, medium, high, critical + + 以JSON格式返回: {"intent": "...", "urgency": "...", "topic": "...", "summary": "..."} + """, emailContent, senderEmail); + + // 获取结构化响应 + String response = chatClient.prompt() + .user(classificationPrompt) + .call() + .content(); + + // 解析为 EmailClassification 对象 + EmailClassification classification = JSON.parseObject(response, EmailClassification.class); + + // 根据分类确定下一个节点 + String nextNode; + if ("billing".equals(classification.getIntent()) || + "critical".equals(classification.getUrgency())) { + nextNode = "human_review"; + } else if (List.of("question", "feature").contains(classification.getIntent())) { + nextNode = "search_documentation"; + } else if ("bug".equals(classification.getIntent())) { + nextNode = "bug_tracking"; + } else { + nextNode = "draft_response"; + } + + // 将分类作为单个对象存储在状态中 + return Map.of( + "classification", classification, + "next_node", nextNode + ); + } + + /** + * 简化的JSON解析(实际应用中使用Jackson或Gson) + */ + private EmailClassification parseClassification(String jsonResponse) { + EmailClassification classification = new EmailClassification(); + + // 简单的正则表达式解析 + Pattern intentPattern = Pattern.compile("\"intent\"\\s*:\\s*\"([^\"]+)\""); + Pattern urgencyPattern = Pattern.compile("\"urgency\"\\s*:\\s*\"([^\"]+)\""); + Pattern topicPattern = Pattern.compile("\"topic\"\\s*:\\s*\"([^\"]+)\""); + Pattern summaryPattern = Pattern.compile("\"summary\"\\s*:\\s*\"([^\"]+)\""); + + Matcher matcher = intentPattern.matcher(jsonResponse); + if (matcher.find()) { + classification.setIntent(matcher.group(1)); + } + + matcher = urgencyPattern.matcher(jsonResponse); + if (matcher.find()) { + classification.setUrgency(matcher.group(1)); + } + + matcher = topicPattern.matcher(jsonResponse); + if (matcher.find()) { + classification.setTopic(matcher.group(1)); + } + + matcher = summaryPattern.matcher(jsonResponse); + if (matcher.find()) { + classification.setSummary(matcher.group(1)); + } + + // 如果解析失败,设置默认值 + if (classification.getIntent() == null) { + classification.setIntent("question"); + } + if (classification.getUrgency() == null) { + classification.setUrgency("medium"); + } + if (classification.getTopic() == null) { + classification.setTopic("general"); + } + if (classification.getSummary() == null) { + classification.setSummary("需要处理的客户邮件"); + } + + return classification; + } + } + + /** + * 文档搜索节点 + */ + public static class SearchDocumentationNode implements NodeAction { + + @Override + public Map apply(OverAllState state) throws Exception { + // 从分类构建搜索查询 + EmailClassification classification = state.value("classification") + .map(v -> (EmailClassification) v) + .orElse(new EmailClassification()); + String query = classification.getIntent() + " " + classification.getTopic(); + + try { + // 实现您的搜索逻辑 + // 存储原始搜索结果,而不是格式化的文本 + List searchResults = List.of( + "通过设置 > 安全 > 更改密码重置密码", + "密码必须至少12个字符", + "包含大写字母、小写字母、数字和符号" + ); + + log.info("SearchDocumentationNode--Searching documentation for: {}", query); + + return Map.of( + "search_results", searchResults, + "next_node", "draft_response" + ); + } catch (Exception e) { + // 对于可恢复的搜索错误,存储错误并继续 + log.warn("SearchDocumentationNode--Search error: {}", e.getMessage()); + List errorResult = List.of("搜索暂时不可用: " + e.getMessage()); + return Map.of( + "search_results", errorResult, + "next_node", "draft_response" + ); + } + } + } + + /** + * Bug跟踪节点 + */ + public static class BugTrackingNode implements NodeAction { + + @Override + public Map apply(OverAllState state) throws Exception { + // 在您的bug跟踪系统中创建票据 + String ticketId = "BUG-12345"; // 将通过API创建 + + log.info("BugTrackingNode---Created bug ticket: {}", ticketId); + + return Map.of( + "search_results", List.of("已创建Bug票据 " + ticketId), + "current_step", "bug_tracked", + "next_node", "draft_response" + ); + } + } + + /** + * 起草回复节点 + */ + public static class DraftResponseNode implements NodeAction { + + private final ChatClient chatClient; + + public DraftResponseNode(ChatClient.Builder chatClientBuilder) { + this.chatClient = chatClientBuilder.build(); + } + + @Override + public Map apply(OverAllState state) throws Exception { + EmailClassification classification = state.value("classification") + .map(v -> (EmailClassification) v) + .orElse(new EmailClassification()); + String emailContent = state.value("email_content") + .map(v -> (String) v) + .orElse(""); + + // 从原始状态数据按需格式化上下文 + List contextSections = new ArrayList<>(); + + Optional> searchResults = state.value("search_results") + .map(v -> (List) v); + if (searchResults.isPresent()) { + // 为提示格式化搜索结果 + List docs = searchResults.get(); + String formattedDocs = docs.stream() + .map(doc -> "- " + doc) + .collect(Collectors.joining("\n")); + contextSections.add("相关文档:\n" + formattedDocs); + } + + Optional> customerHistory = state.value("customer_history") + .map(v -> (Map) v); + if (customerHistory.isPresent()) { + // 为提示格式化客户数据 + Map history = customerHistory.get(); + contextSections.add("客户等级: " + history.getOrDefault("tier", "standard")); + } + + // 使用格式化的上下文构建提示 + String draftPrompt = String.format(""" + 为这封客户邮件起草回复: + %s + + 邮件意图: %s + 紧急程度: %s + + %s + + 指南: + - 专业且有帮助 + - 解决他们的具体问题 + - 在相关时使用提供的文档 + """, + emailContent, + classification.getIntent(), + classification.getUrgency(), + String.join("\n", contextSections) + ); + + String response = chatClient.prompt() + .user(draftPrompt) + .call() + .content(); + + // 根据紧急程度和意图确定是否需要人工审核 + boolean needsReview = + List.of("high", "critical").contains(classification.getUrgency()) || + "complex".equals(classification.getIntent()); + + // 路由到适当的下一个节点 + String nextNode = needsReview ? "human_review" : "send_reply"; + log.info("DraftResponseNode--Routing to {}", nextNode); + return Map.of( + "draft_response", response, // 仅存储原始响应 + "next_node", nextNode + ); + } + } + + /** + * 人工审核节点 + * + * 注意:在 interruptBefore 模式下,中断是在编译配置中设置的(见 createEmailAgentGraph 方法)。 + * 节点本身不需要做任何特殊处理,只需要正常返回状态即可。 + * 当执行到此节点前时,Graph 会自动中断,等待人工输入。 + */ + public static class HumanReviewNode implements NodeAction { + + @Override + public Map apply(OverAllState state) throws Exception { + EmailClassification classification = state.value("classification") + .map(v -> (EmailClassification) v) + .orElse(new EmailClassification()); + + // 准备审核数据 + Map reviewData = Map.of( + "email_id", state.value("email_id").map(v -> (String) v).orElse(""), + "original_email", state.value("email_content").map(v -> (String) v).orElse(""), + "draft_response", state.value("draft_response").map(v -> (String) v).orElse(""), + "urgency", classification.getUrgency(), + "intent", classification.getIntent(), + "action", "请审核并批准/编辑此响应" + ); + + log.info("HumanReviewNode---Waiting for human review: {}", reviewData); + + // 返回审核数据和下一个节点 + // 注意:在 interruptBefore 模式下,此节点在人工输入后才会执行 + return Map.of( + "review_data", reviewData, + "status", "waiting_for_review", + "next_node", "send_reply" + ); + } + } + + /** + * 发送回复节点 + */ + public static class SendReplyNode implements NodeAction { + + @Override + public Map apply(OverAllState state) throws Exception { + String draftResponse = state.value("draft_response") + .map(v -> (String) v) + .orElse(""); + + // 与邮件服务集成 + log.info("SendReplyNode---Sending reply: {}...", + draftResponse.length() > 100 + ? draftResponse.substring(0, 100) + : draftResponse); + + return Map.of("status", "sent"); + } + } + + // ==================== Graph 组装 ==================== + + /** + * 创建邮件处理 Graph + */ + public static CompiledGraph createEmailAgentGraph(ChatModel chatModel) throws GraphStateException { + // 配置 ChatClient + ChatClient.Builder chatClientBuilder = ChatClient.builder(chatModel); + + // 创建节点 + var readEmail = node_async(new ReadEmailNode()); + var classifyIntent = node_async(new ClassifyIntentNode(chatClientBuilder)); + var searchDocumentation = node_async(new SearchDocumentationNode()); + var bugTracking = node_async(new BugTrackingNode()); + var draftResponse = node_async(new DraftResponseNode(chatClientBuilder)); + var humanReview = node_async(new HumanReviewNode()); + var sendReply = node_async(new SendReplyNode()); + + // 创建图 + StateGraph workflow = new StateGraph(createKeyStrategyFactory()) + .addNode("read_email", readEmail) + .addNode("classify_intent", classifyIntent) + .addNode("search_documentation", searchDocumentation) + .addNode("bug_tracking", bugTracking) + .addNode("draft_response", draftResponse) + .addNode("human_review", humanReview) + .addNode("send_reply", sendReply); + + // 添加基本边 + workflow.addEdge(START, "read_email"); + workflow.addEdge("read_email", "classify_intent"); + workflow.addEdge("send_reply", END); + + // 添加条件边(基于节点返回的 next_node) + workflow.addConditionalEdges("classify_intent", + edge_async(state -> { + return (String) state.value("next_node").orElse("draft_response"); + }), + Map.of( + "search_documentation", "search_documentation", + "bug_tracking", "bug_tracking", + "human_review", "human_review", + "draft_response", "draft_response" + )); + + workflow.addConditionalEdges("draft_response", + edge_async(state -> { + return (String) state.value("next_node").orElse("send_reply"); + }), + Map.of( + "human_review", "human_review", + "send_reply", "send_reply" + )); + + workflow.addConditionalEdges("human_review", + edge_async(state -> { + return (String) state.value("next_node").orElse("send_reply"); + }), + Map.of( + "send_reply", "send_reply" + )); + + workflow.addEdge("search_documentation", "draft_response"); + workflow.addEdge("bug_tracking", "draft_response"); + + // 配置持久化 + var memory = new MemorySaver(); + var compileConfig = CompileConfig.builder() + .saverConfig(SaverConfig.builder() + .register(memory) + .build()) + .interruptBefore("human_review") // 在人工审核前中断 + .build(); + + return workflow.compile(compileConfig); + } + + // ==================== 测试方法 ==================== + + /** + * 测试紧急账单问题 + */ + public static void testBillingIssue(CompiledGraph app) throws Exception { + log.info("=== 测试紧急账单问题 ==="); + + // 测试紧急账单问题 + Map initialState = Map.of( + "email_content", "我的订阅被收费两次了!这很紧急!", + "sender_email", "customer@example.com", + "email_id", "email_123", + "messages", new ArrayList() + ); + + // 使用 thread_id 运行以实现持久化 + var config = RunnableConfig.builder() + .threadId("customer_123") + .build(); + + // 使用 stream 执行,直到中断点(human_review) + // 图将在 human_review 处暂停(因为配置了 interruptBefore) + Flux stream = app.stream(initialState, config); + stream + .doOnNext(output -> log.info("节点输出: {}", output)) + .doOnError(error -> log.error("执行错误: {}", error.getMessage())) + .doOnComplete(() -> log.info("流完成")) + .blockLast(); + + // 获取当前状态,检查是否有草稿回复 + var currentState = app.getState(config); + Map stateData = currentState.state().data(); + String draftResponse = (String) stateData.get("draft_response"); + if (draftResponse != null) { + log.info("Draft ready for review: {}...", + draftResponse.length() > 100 + ? draftResponse.substring(0, 100) + : draftResponse); + } + + // 准备好后,提供人工输入以恢复 + // 使用 updateState 更新状态(interruptBefore 模式下,传入 null 作为节点 ID) + var updatedConfig = app.updateState(config, Map.of( + "approved", true, + "edited_response", "我们对重复收费深表歉意。我已经立即启动了退款..." + ), null); + + // 继续执行(input 为 null,使用之前的状态) + app.stream(null, updatedConfig) + .doOnNext(output -> log.info("节点输出: {}", output)) + .doOnError(error -> log.error("执行错误: {}", error.getMessage())) + .doOnComplete(() -> log.info("流完成")) + .blockLast(); + +// 获取最终状态 + var finalState = app.getState(updatedConfig); + String status = (String) finalState.state().data().get("status"); + log.info("Email sent successfully! Status: {}", status); + TimeUnit.SECONDS.sleep(999999); + } + + /** + * 测试简单问题 + */ + public static void testSimpleQuestion(CompiledGraph app) throws Exception { + log.info("=== 测试简单问题 ==="); + + Map initialState = Map.of( + "email_content", "如何重置我的密码?", + "sender_email", "user@example.com", + "email_id", "email_456", + "messages", new ArrayList() + ); + + var config = RunnableConfig.builder() + .threadId("user_456") + .build(); + + // invoke 返回 Optional,需要使用 orElseThrow() 获取结果 + var result = app.invoke(initialState, config).orElseThrow(); + log.info("Simple question processed. Status: {}", result.data().get("status")); + TimeUnit.SECONDS.sleep(999999); + } + + /** + * 主方法 + */ + public static void main(String[] args) throws Exception { + log.info("========================================"); + log.info("Graph 工作流编排快速入门示例"); + log.info("========================================\n"); + + // 注意:实际使用时需要提供 ChatModel 实例 + // 创建 DashScope API 实例 + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(System.getenv("ALI_AI_KEY")) + .build(); + + // 创建 ChatModel + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + CompiledGraph app = createEmailAgentGraph(chatModel); + + testBillingIssue(app); +// testSimpleQuestion(app); + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/QuickStartExample1.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/QuickStartExample1.java new file mode 100644 index 00000000..9ca7bf06 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/QuickStartExample1.java @@ -0,0 +1,264 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph; + +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; +import com.alibaba.fastjson.JSON; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.client.ChatClient; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Graph 工作流编排快速入门示例 + * + * 本示例演示如何通过将客服邮件处理流程分解为离散步骤来使用 Spring AI Alibaba Graph 构建智能工作流。 + * + * 示例包含: + * 1. 状态定义(EmailClassification) + * 2. 节点实现(读取邮件、分类意图、搜索文档、Bug跟踪、起草回复、人工审核、发送回复) + * 3. Graph 组装和配置 + * 4. 测试执行 + */ +public class QuickStartExample1 { + + private static final Logger log = LoggerFactory.getLogger(QuickStartExample1.class); + + /** + * 邮件分类 + */ + public static class EmailClassification { + private String intent; + + private String urgency; + + private String topic; + + private String summary; + + public EmailClassification() { + } + + public EmailClassification(String intent, String urgency, String topic, String summary) { + this.intent = intent; + this.urgency = urgency; + this.topic = topic; + this.summary = summary; + } + + public String getIntent() { + return intent; + } + + public void setIntent(String intent) { + this.intent = intent; + } + + public String getUrgency() { + return urgency; + } + + public void setUrgency(String urgency) { + this.urgency = urgency; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + @Override + public String toString() { + return String.format("EmailClassification{intent='%s', urgency='%s', topic='%s', summary='%s'}", + intent, urgency, topic, summary); + } + } + + /** + * 配置key状态策略 + */ + public static KeyStrategyFactory createKeyStrategyFactory() { + return () -> { + HashMap strategies = new HashMap<>(); + strategies.put("email_content", new ReplaceStrategy()); + strategies.put("sender_email", new ReplaceStrategy()); + strategies.put("email_id", new ReplaceStrategy()); + strategies.put("classification", new ReplaceStrategy()); + strategies.put("search_results", new ReplaceStrategy()); + strategies.put("customer_history", new ReplaceStrategy()); + strategies.put("draft_response", new ReplaceStrategy()); + strategies.put("messages", new AppendStrategy()); + strategies.put("next_node", new ReplaceStrategy()); + strategies.put("status", new ReplaceStrategy()); + strategies.put("review_data", new ReplaceStrategy()); + return strategies; + }; + } + + /** + * 读取邮件 + */ + public static class ReadEmailNode implements NodeAction { + + @Override + public Map apply(OverAllState state) throws Exception { + String emailContent = state.value("email_content").map(v -> (String)v).orElse(""); + log.info("ReadEmailNode----Processing email: {}", emailContent); + ArrayList messages = new ArrayList<>(); + messages.add("Processing email: " + emailContent); + return Map.of("messages", messages); + } + } + + /** + * 分类意图节点 + */ + public static class ClassifyIntentNode implements NodeAction { + + private final ChatClient chatClient; + + public ClassifyIntentNode(ChatClient.Builder chatClientBuilder) { + this.chatClient = chatClientBuilder.build(); + } + + @Override + public Map apply(OverAllState state) throws Exception { + String emailContent = state.value("email_content") + .map(v -> (String) v) + .orElseThrow(() -> new IllegalStateException("No email content")); + String senderEmail = state.value("sender_email") + .map(v -> (String) v) + .orElse("unknown"); + + // 按需格式化提示,不存储在状态中 + String classificationPrompt = String.format(""" + 分析这封客户邮件并进行分类: + + 邮件: %s + 发件人: %s + + 提供分类,包括意图、紧急程度、主题和摘要。 + + 意图应该是以下之一: question, bug, billing, feature, complex + 紧急程度应该是以下之一: low, medium, high, critical + + 以JSON格式返回: {"intent": "...", "urgency": "...", "topic": "...", "summary": "..."} + """, emailContent, senderEmail); + + // 获取结构化响应 + String response = chatClient.prompt() + .user(classificationPrompt) + .call() + .content(); + + // 解析为 EmailClassification 对象 + QuickStartExample.EmailClassification classification = JSON.parseObject(response, QuickStartExample.EmailClassification.class); + + // 根据分类确定下一个节点 + String nextNode; + if ("billing".equals(classification.getIntent()) || + "critical".equals(classification.getUrgency())) { + nextNode = "human_review"; + } else if (List.of("question", "feature").contains(classification.getIntent())) { + nextNode = "search_documentation"; + } else if ("bug".equals(classification.getIntent())) { + nextNode = "bug_tracking"; + } else { + nextNode = "draft_response"; + } + + // 将分类作为单个对象存储在状态中 + return Map.of( + "classification", classification, + "next_node", nextNode + ); + } + + /** + * 简化的JSON解析(实际应用中使用Jackson或Gson) + */ + private QuickStartExample.EmailClassification parseClassification(String jsonResponse) { + QuickStartExample.EmailClassification classification = new QuickStartExample.EmailClassification(); + + // 简单的正则表达式解析 + Pattern intentPattern = Pattern.compile("\"intent\"\\s*:\\s*\"([^\"]+)\""); + Pattern urgencyPattern = Pattern.compile("\"urgency\"\\s*:\\s*\"([^\"]+)\""); + Pattern topicPattern = Pattern.compile("\"topic\"\\s*:\\s*\"([^\"]+)\""); + Pattern summaryPattern = Pattern.compile("\"summary\"\\s*:\\s*\"([^\"]+)\""); + + Matcher matcher = intentPattern.matcher(jsonResponse); + if (matcher.find()) { + classification.setIntent(matcher.group(1)); + } + + matcher = urgencyPattern.matcher(jsonResponse); + if (matcher.find()) { + classification.setUrgency(matcher.group(1)); + } + + matcher = topicPattern.matcher(jsonResponse); + if (matcher.find()) { + classification.setTopic(matcher.group(1)); + } + + matcher = summaryPattern.matcher(jsonResponse); + if (matcher.find()) { + classification.setSummary(matcher.group(1)); + } + + // 如果解析失败,设置默认值 + if (classification.getIntent() == null) { + classification.setIntent("question"); + } + if (classification.getUrgency() == null) { + classification.setUrgency("medium"); + } + if (classification.getTopic() == null) { + classification.setTopic("general"); + } + if (classification.getSummary() == null) { + classification.setSummary("需要处理的客户邮件"); + } + + return classification; + } + } + + +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/CoreLibraryExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/CoreLibraryExample.java new file mode 100644 index 00000000..8ceb832e --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/CoreLibraryExample.java @@ -0,0 +1,158 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.core; + +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.RemoveByHash; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; + +import java.util.HashMap; +import java.util.Map; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 核心库概念指南示例 + * 演示 State、Nodes、Edges 的基本用法 + */ +public class CoreLibraryExample { + + /** + * 示例 A: 使用 AppendStrategy + */ + public static KeyStrategyFactory createKeyStrategyFactory() { + return () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("messages", new AppendStrategy()); + return keyStrategyMap; + }; + } + + /** + * 示例 B: 自定义 KeyStrategyFactory + */ + public static KeyStrategyFactory createCustomKeyStrategyFactory() { + return () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("property", (oldValue, newValue) -> + ((String) newValue).toUpperCase() + ); + return keyStrategyMap; + }; + } + + /** + * 基本节点示例 + */ + public static void basicNodeExample() throws GraphStateException { + KeyStrategyFactory keyStrategyFactory = createKeyStrategyFactory(); + + var myNode = node_async(state -> { + System.out.println("In myNode: "); + String input = (String) state.value("input").orElse(""); + return Map.of("results", "Hello " + input); + }); + + var myOtherNode = node_async(state -> Map.of()); + + StateGraph builder = new StateGraph(keyStrategyFactory) + .addNode("myNode", myNode) + .addNode("myOtherNode", myOtherNode) + .addEdge(START, "myNode") + .addEdge("myNode", "myOtherNode") + .addEdge("myOtherNode", END); + + CompiledGraph graph = builder.compile(); + System.out.println("Graph compiled successfully"); + } + + /** + * 使用 RemoveByHash 删除消息示例 + */ + public static void removeMessagesExample() throws GraphStateException { + KeyStrategyFactory keyStrategyFactory = createKeyStrategyFactory(); + + var workflow = new StateGraph(keyStrategyFactory) + .addNode("agent_1", node_async(state -> + Map.of("messages", "message1"))) + .addNode("agent_2", node_async(state -> + Map.of("messages", "message2.1"))) + .addNode("agent_3", node_async(state -> + Map.of("messages", RemoveByHash.of("message2.1")))) + .addEdge(START, "agent_1") + .addEdge("agent_1", "agent_2") + .addEdge("agent_2", "agent_3") + .addEdge("agent_3", END); + + CompiledGraph graph = workflow.compile(); + System.out.println("Remove messages graph compiled successfully"); + } + + /** + * 条件边示例 + */ + public static void conditionalEdgesExample() throws GraphStateException { + KeyStrategyFactory keyStrategyFactory = createKeyStrategyFactory(); + + var workflow = new StateGraph(keyStrategyFactory) + .addNode("nodeA", node_async(state -> Map.of("data", "A"))) + .addNode("nodeB", node_async(state -> Map.of("data", "B"))) + .addNode("nodeC", node_async(state -> Map.of("data", "C"))) + .addEdge(START, "nodeA") + .addConditionalEdges("nodeA", edge_async(state -> "nodeB"), + Map.of("nodeB", "nodeB", "nodeC", "nodeC")) + .addEdge("nodeB", END) + .addEdge("nodeC", END); + + CompiledGraph graph = workflow.compile(); + System.out.println("Conditional edges graph compiled successfully"); + } + + public static void main(String[] args) { + System.out.println("=== 核心库概念示例 ===\n"); + + try { + // 示例 1: 基本节点示例 + System.out.println("示例 1: 基本节点示例"); + basicNodeExample(); + System.out.println(); + + // 示例 2: 使用 RemoveByHash 删除消息示例 + System.out.println("示例 2: 使用 RemoveByHash 删除消息示例"); + removeMessagesExample(); + System.out.println(); + + // 示例 3: 条件边示例 + System.out.println("示例 3: 条件边示例"); + conditionalEdgesExample(); + System.out.println(); + + System.out.println("所有示例执行完成"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/MemoryExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/MemoryExample.java new file mode 100644 index 00000000..8bbfef71 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/MemoryExample.java @@ -0,0 +1,400 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.core; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; +import com.alibaba.cloud.ai.graph.store.Store; +import com.alibaba.cloud.ai.graph.store.StoreItem; +import com.alibaba.cloud.ai.graph.store.stores.MemoryStore; + +import org.springframework.ai.chat.client.ChatClient; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 内存管理示例 + * 演示短期和长期内存管理 + */ +public class MemoryExample { + + /** + * 示例 1: 添加短期内存 + */ + public static void addShortTermMemory(ChatClient.Builder chatClientBuilder) throws GraphStateException { + // 创建内存检查点器 + MemorySaver checkpointer = new MemorySaver(); + + SaverConfig saverConfig = SaverConfig.builder() + .register(checkpointer) + .build(); + + // 定义状态策略 + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("messages", new AppendStrategy()); + return keyStrategyMap; + }; + + // 创建聊天节点 + var chatNode = node_async(state -> { + List> messages = + (List>) state.value("messages").orElse(List.of()); + + // 使用 ChatClient 调用 AI 模型 + ChatClient chatClient = chatClientBuilder.build(); + String response = chatClient.prompt() + .user(messages.get(messages.size() - 1).get("content")) + .call() + .content(); + + return Map.of("messages", List.of( + Map.of("role", "assistant", "content", response) + )); + }); + + // 构建图 + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("chat", chatNode) + .addEdge(START, "chat") + .addEdge("chat", END); + + // 编译图 + CompiledGraph graph = stateGraph.compile( + CompileConfig.builder() + .saverConfig(saverConfig) + .build() + ); + + // 第一轮对话 + RunnableConfig config = RunnableConfig.builder() + .threadId("conversation-1") + .build(); + + graph.invoke(Map.of("messages", List.of( + Map.of("role", "user", "content", "你好!我是 Bob") + )), config); + + // 第二轮对话(使用相同的 threadId) + graph.invoke(Map.of("messages", List.of( + Map.of("role", "user", "content", "我的名字是什么?") + )), config); + // AI 将能够记住之前的对话,回答 "Bob" + System.out.println("Short-term memory example executed"); + } + + /** + * 示例 2: 使用 Store 实现长期内存 + */ + public static void longTermMemoryWithDatabase() throws GraphStateException { + // 在节点中使用 Store 存储用户信息 + var userProfileNode = com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig.node_async((state, config) -> { + String userId = (String) state.value("userId").orElse(""); + + if (userId.isEmpty()) { + return Map.of("userProfile", Map.of("name", "Unknown", "preferences", "default")); + } + + // 从 Store 获取用户配置 + Store store = config.store(); + if (store != null) { + Optional itemOpt = store.getItem(List.of("user_profiles"), userId); + if (itemOpt.isPresent()) { + Map userProfile = itemOpt.get().getValue(); + return Map.of("userProfile", userProfile); + } + } + + // 如果未找到,返回默认值 + Map userProfile = Map.of("name", "User", "preferences", "default"); + return Map.of("userProfile", userProfile); + }); + + // 创建图 + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("userId", new ReplaceStrategy()); + keyStrategyMap.put("userProfile", new ReplaceStrategy()); + return keyStrategyMap; + }; + + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("load_profile", userProfileNode) + .addEdge(START, "load_profile") + .addEdge("load_profile", END); + + CompiledGraph graph = stateGraph.compile(CompileConfig.builder().build()); + + // 创建长期记忆存储并预填充数据 + MemoryStore memoryStore = new MemoryStore(); + Map profileData = new HashMap<>(); + profileData.put("name", "张三"); + profileData.put("preferences", "喜欢编程"); + StoreItem profileItem = StoreItem.of(List.of("user_profiles"), "user_001", profileData); + memoryStore.putItem(profileItem); + + // 运行图 + RunnableConfig config = RunnableConfig.builder() + .threadId("profile_thread") + .store(memoryStore) + .build(); + + Optional stateOptiona = graph.invoke(Map.of("userId", "user_001"), config); + Map result = stateOptiona.get().data(); + System.out.println("加载的用户配置: " + result.get("userProfile")); + + System.out.println("Long-term memory with Store example executed"); + } + + /** + * 示例 3: 使用 Store 缓存实现长期内存 + */ + public static void longTermMemoryWithRedis() throws GraphStateException { + var cacheNode = com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig.node_async((state, config) -> { + String key = (String) state.value("cacheKey").orElse(""); + + if (key.isEmpty()) { + return Map.of("result", "no_key"); + } + + // 从 Store 获取缓存数据 + Store store = config.store(); + if (store != null) { + Optional itemOpt = store.getItem(List.of("cache"), key); + if (itemOpt.isPresent()) { + // 缓存命中 + Map cachedData = itemOpt.get().getValue(); + return Map.of("result", cachedData.get("value")); + } + } + + // 缓存未命中,执行计算或查询 + Object computedData = performExpensiveOperation(key); + + // 存储到 Store + if (store != null) { + Map cacheValue = new HashMap<>(); + cacheValue.put("value", computedData); + StoreItem cacheItem = StoreItem.of(List.of("cache"), key, cacheValue); + store.putItem(cacheItem); + } + + return Map.of("result", computedData); + }); + + // 创建图 + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("cacheKey", new ReplaceStrategy()); + keyStrategyMap.put("result", new ReplaceStrategy()); + return keyStrategyMap; + }; + + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("cache", cacheNode) + .addEdge(START, "cache") + .addEdge("cache", END); + + CompiledGraph graph = stateGraph.compile(CompileConfig.builder().build()); + + // 创建长期记忆存储 + MemoryStore memoryStore = new MemoryStore(); + + // 第一次调用(缓存未命中) + RunnableConfig config = RunnableConfig.builder() + .threadId("cache_thread") + .store(memoryStore) + .build(); + + Optional stateOptional = graph.invoke(Map.of("cacheKey", "expensive_key"), config); + Map result1 = stateOptional.get().data(); + System.out.println("第一次调用结果: " + result1.get("result")); + + // 第二次调用(缓存命中) + Optional stateOptiona = graph.invoke(Map.of("cacheKey", "expensive_key"), config); + Map result2 = stateOptional.get().data(); + System.out.println("第二次调用结果(从缓存): " + result2.get("result")); + + System.out.println("Long-term memory with Store cache example executed"); + } + + // 模拟耗时操作 + private static Object performExpensiveOperation(String key) { + // 模拟耗时计算 + return "computed_result_for_" + key; + } + + /** + * 示例 4: 结合短期和长期内存 + */ + public static void combinedMemoryExample(ChatClient.Builder chatClientBuilder) throws GraphStateException { + // 定义状态 + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("userId", new ReplaceStrategy()); + keyStrategyMap.put("messages", new AppendStrategy()); + keyStrategyMap.put("userPreferences", new ReplaceStrategy()); + return keyStrategyMap; + }; + + // 加载用户偏好(长期内存) + var loadUserPreferences = com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig.node_async((state, config) -> { + String userId = (String) state.value("userId").orElse(""); + + if (userId.isEmpty()) { + return Map.of("userPreferences", Map.of("theme", "default", "language", "zh")); + } + + // 从 Store 加载用户偏好 + Store store = config.store(); + if (store != null) { + Optional itemOpt = store.getItem(List.of("user_preferences"), userId); + if (itemOpt.isPresent()) { + Map preferences = itemOpt.get().getValue(); + return Map.of("userPreferences", preferences); + } + } + + // 如果未找到,返回默认偏好 + Map preferences = Map.of("theme", "dark", "language", "zh"); + return Map.of("userPreferences", preferences); + }); + + // 聊天节点(使用短期和长期内存) + var chatNode = node_async(state -> { + List> messages = + (List>) state.value("messages").orElse(List.of()); + Map preferences = + (Map) state.value("userPreferences").orElse(Map.of()); + + // 构建包含用户偏好的提示 + String userPrompt = messages.get(messages.size() - 1).get("content"); + String enhancedPrompt = "用户偏好: " + preferences + "\n用户问题: " + userPrompt; + + // 调用 AI + ChatClient chatClient = chatClientBuilder.build(); + String response = chatClient.prompt() + .user(enhancedPrompt) + .call() + .content(); + + return Map.of("messages", List.of( + Map.of("role", "assistant", "content", response) + )); + }); + + // 构建图 + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("load_preferences", loadUserPreferences) + .addNode("chat", chatNode) + .addEdge(START, "load_preferences") + .addEdge("load_preferences", "chat") + .addEdge("chat", END); + + // 配置检查点(短期内存) + SaverConfig saverConfig = SaverConfig.builder() + .register(new MemorySaver()) + .build(); + + // 编译图 + CompiledGraph graph = stateGraph.compile( + CompileConfig.builder() + .saverConfig(saverConfig) + .build() + ); + + // 创建长期记忆存储并预填充用户偏好 + MemoryStore memoryStore = new MemoryStore(); + Map preferencesData = new HashMap<>(); + preferencesData.put("theme", "dark"); + preferencesData.put("language", "zh"); + preferencesData.put("timezone", "Asia/Shanghai"); + StoreItem preferencesItem = StoreItem.of(List.of("user_preferences"), "user_002", preferencesData); + memoryStore.putItem(preferencesItem); + + // 运行图 + RunnableConfig config = RunnableConfig.builder() + .threadId("combined_thread") + .store(memoryStore) + .build(); + + // 第一轮对话(加载偏好并开始对话) + graph.invoke(Map.of( + "userId", "user_002", + "messages", List.of(Map.of("role", "user", "content", "你好")) + ), config); + + // 第二轮对话(使用短期和长期记忆) + graph.invoke(Map.of( + "userId", "user_002", + "messages", List.of(Map.of("role", "user", "content", "根据我的偏好给我一些建议")) + ), config); + + System.out.println("Combined memory example created"); + } + + public static void main(String[] args) { + System.out.println("=== 内存管理示例 ===\n"); + + try { + // 示例 1: 添加短期内存(需要 ChatClient) + System.out.println("示例 1: 添加短期内存"); + System.out.println("注意: 此示例需要 ChatClient,跳过执行"); + // addShortTermMemory(ChatClient.builder(...)); + System.out.println(); + + // 示例 2: 使用 Store 实现长期内存 + System.out.println("示例 2: 使用 Store 实现长期内存"); + longTermMemoryWithDatabase(); + System.out.println(); + + // 示例 3: 使用 Store 缓存实现长期内存 + System.out.println("示例 3: 使用 Store 缓存实现长期内存"); + longTermMemoryWithRedis(); + System.out.println(); + + // 示例 4: 结合短期和长期内存(需要 ChatClient) + System.out.println("示例 4: 结合短期和长期内存"); + System.out.println("注意: 此示例需要 ChatClient,跳过执行"); + // combinedMemoryExample(ChatClient.builder(...)); + System.out.println(); + + System.out.println("所有示例执行完成"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/PersistenceExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/PersistenceExample.java new file mode 100644 index 00000000..300af15f --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/PersistenceExample.java @@ -0,0 +1,245 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.core; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.StateSnapshot; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 持久化示例 + * 演示如何使用 Checkpointer 实现工作流状态持久化 + */ +public class PersistenceExample { + + /** + * 示例 1: 基本持久化配置 + */ + public static void basicPersistenceExample() throws GraphStateException { + // 定义状态策略 + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("foo", new ReplaceStrategy()); + keyStrategyMap.put("bar", new AppendStrategy()); + return keyStrategyMap; + }; + + // 定义节点操作 + var nodeA = node_async(state -> { + return Map.of("foo", "a", "bar", List.of("a")); + }); + + var nodeB = node_async(state -> { + return Map.of("foo", "b", "bar", List.of("b")); + }); + + // 创建图 + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("node_a", nodeA) + .addNode("node_b", nodeB) + .addEdge(START, "node_a") + .addEdge("node_a", "node_b") + .addEdge("node_b", END); + + // 配置检查点 + SaverConfig saverConfig = SaverConfig.builder() + .register(new MemorySaver()) + .build(); + + // 编译图 + CompiledGraph graph = stateGraph.compile( + CompileConfig.builder() + .saverConfig(saverConfig) + .build() + ); + + // 运行图 + RunnableConfig config = RunnableConfig.builder() + .threadId("1") + .build(); + + Map input = new HashMap<>(); + input.put("foo", ""); + + graph.invoke(input, config); + System.out.println("Basic persistence example executed"); + } + + /** + * 示例 2: 获取状态 + */ + public static void getStateExample(CompiledGraph graph) { + RunnableConfig config = RunnableConfig.builder() + .threadId("1") + .build(); + + // 获取最新的状态快照 + StateSnapshot stateSnapshot = graph.getState(config); + System.out.println("Current state: " + stateSnapshot.state()); + System.out.println("Current node: " + stateSnapshot.node()); + + // 获取特定 checkpoint_id 的状态快照 + RunnableConfig configWithCheckpoint = RunnableConfig.builder() + .threadId("1") + .checkPointId("1ef663ba-28fe-6528-8002-5a559208592c") + .build(); + StateSnapshot specificSnapshot = graph.getState(configWithCheckpoint); + System.out.println("Specific checkpoint state: " + specificSnapshot.state()); + } + + /** + * 示例 3: 获取状态历史 + */ + public static void getStateHistoryExample(CompiledGraph graph) { + RunnableConfig config = RunnableConfig.builder() + .threadId("1") + .build(); + + List history = (List) graph.getStateHistory(config); + System.out.println("State history:"); + for (int i = 0; i < history.size(); i++) { + StateSnapshot snapshot = history.get(i); + System.out.printf("Step %d: %s\n", i, snapshot.state()); + System.out.printf(" Checkpoint ID: %s\n", snapshot.config().checkPointId()); + System.out.printf(" Node: %s\n", snapshot.node()); + } + } + + /** + * 示例 4: 更新状态 + */ + public static void updateStateExample(CompiledGraph graph) throws Exception { + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("foo", new ReplaceStrategy()); // 替换策略 + keyStrategyMap.put("bar", new AppendStrategy()); // 追加策略 + return keyStrategyMap; + }; + + RunnableConfig config = RunnableConfig.builder() + .threadId("1") + .build(); + + Map updates = new HashMap<>(); + updates.put("foo", 2); + updates.put("bar", List.of("b")); + + graph.updateState(config, updates, null); + System.out.println("State updated successfully"); + } + + /** + * 示例 5: 重放(Replay) + */ + public static void replayExample(CompiledGraph graph) { + RunnableConfig config = RunnableConfig.builder() + .threadId("1") + .checkPointId("0c62ca34-ac19-445d-bbb0-5b4984975b2a") + .build(); + + graph.invoke(Map.of(), config); + System.out.println("Replay executed"); + } + + public static void main(String[] args) { + System.out.println("=== 持久化示例 ===\n"); + + try { + // 示例 1: 基本持久化配置 + System.out.println("示例 1: 基本持久化配置"); + basicPersistenceExample(); + System.out.println(); + + // 创建图用于后续示例 + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("foo", new ReplaceStrategy()); + keyStrategyMap.put("bar", new AppendStrategy()); + return keyStrategyMap; + }; + + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("node_a", node_async(state -> Map.of("foo", "a", "bar", List.of("a")))) + .addNode("node_b", node_async(state -> Map.of("foo", "b", "bar", List.of("b")))) + .addEdge(START, "node_a") + .addEdge("node_a", "node_b") + .addEdge("node_b", END); + + SaverConfig saverConfig = SaverConfig.builder() + .register(new MemorySaver()) + .build(); + + CompiledGraph graph = stateGraph.compile( + CompileConfig.builder() + .saverConfig(saverConfig) + .build() + ); + + RunnableConfig config = RunnableConfig.builder() + .threadId("1") + .build(); + + Map input = new HashMap<>(); + input.put("foo", ""); + graph.invoke(input, config); + + // 示例 2: 获取状态 + System.out.println("示例 2: 获取状态"); + getStateExample(graph); + System.out.println(); + + // 示例 3: 获取状态历史 + System.out.println("示例 3: 获取状态历史"); + getStateHistoryExample(graph); + System.out.println(); + + // 示例 4: 更新状态 + System.out.println("示例 4: 更新状态"); + updateStateExample(graph); + System.out.println(); + + // 示例 5: 重放(需要有效的 checkpointId) + System.out.println("示例 5: 重放(需要有效的 checkpointId)"); + System.out.println("注意: 此示例需要有效的 checkpointId,跳过执行"); + // replayExample(graph); + System.out.println(); + + System.out.println("所有示例执行完成"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/StreamingExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/StreamingExample.java new file mode 100644 index 00000000..a729f229 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/core/StreamingExample.java @@ -0,0 +1,179 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.core; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.action.AsyncNodeAction; +import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.streaming.StreamingOutput; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatResponse; + +import java.util.HashMap; +import java.util.Map; + +import reactor.core.publisher.Flux; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; + +/** + * 流式输出示例 + * 演示如何在 Spring AI Alibaba Graph 中实现流式输出 + */ +public class StreamingExample { + + /** + * 使用 StateGraph 实现流式输出的完整示例 + * + * @param chatClientBuilder ChatClient 构建器 + * @throws GraphStateException 图执行异常 + */ + public static void streamLLMTokens(ChatClient.Builder chatClientBuilder) throws GraphStateException { + // 定义状态策略 + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("query", new AppendStrategy()); + keyStrategyMap.put("messages", new AppendStrategy()); + keyStrategyMap.put("result", new AppendStrategy()); + return keyStrategyMap; + }; + + // 创建流式节点 + StreamingNode streamingNode = new StreamingNode(chatClientBuilder, "streaming_node"); + + // 创建处理节点 + ProcessStreamingNode processNode = new ProcessStreamingNode(); + + // 构建图 + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("streaming_node", AsyncNodeAction.node_async(streamingNode)) + .addNode("process_node", AsyncNodeAction.node_async(processNode)) + .addEdge(START, "streaming_node") + .addEdge("streaming_node", "process_node") + .addEdge("process_node", END); + + // 编译图 + CompiledGraph graph = stateGraph.compile( + CompileConfig.builder() + .build() + ); + + // 创建配置 + RunnableConfig config = RunnableConfig.builder() + .threadId("streaming_thread") + .build(); + + // 使用流式方式执行图 + System.out.println("开始流式输出...\n"); + + graph.stream(Map.of("query", "请用一句话介绍 Spring AI"), config) + .doOnNext(output -> { + // 处理流式输出 + if (output instanceof StreamingOutput streamingOutput) { + // 流式输出块 + String chunk = streamingOutput.chunk(); + if (chunk != null && !chunk.isEmpty()) { + System.out.print(chunk); // 实时打印流式内容 + } + } + else { + // 普通节点输出 + String nodeId = output.node(); + Map state = output.state().data(); + System.out.println("\n节点 '" + nodeId + "' 执行完成"); + if (state.containsKey("result")) { + System.out.println("最终结果: " + state.get("result")); + } + } + }) + .doOnComplete(() -> { + System.out.println("\n\n流式输出完成"); + }) + .doOnError(error -> { + System.err.println("流式输出错误: " + error.getMessage()); + }) + .blockLast(); // 阻塞等待流完成 + } + + public static void main(String[] args) { + System.out.println("=== 流式输出示例 ===\n"); + + try { + // 示例 1: 使用 Spring AI 的流式 LLM tokens(需要 ChatClient) + System.out.println("示例 1: 使用 Spring AI 的流式 LLM tokens"); + System.out.println("注意: 此示例需要 ChatClient,跳过执行"); + System.out.println("使用方法: streamLLMTokens(ChatClient.builder()...)"); + // streamLLMTokens(ChatClient.builder()...); + System.out.println(); + + System.out.println("所有示例执行完成"); + System.out.println("提示: 请配置 ChatClient 后运行完整示例"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } + + public static class StreamingNode implements NodeAction { + + private final ChatClient chatClient; + private final String nodeId; + + public StreamingNode(ChatClient.Builder chatClientBuilder, String nodeId) { + this.chatClient = chatClientBuilder.build(); + this.nodeId = nodeId; + } + + @Override + public Map apply(OverAllState state) { + String query = (String) state.value("query").orElse(""); + + // 获取流式响应 + Flux chatResponseFlux = chatClient.prompt() + .user(query) + .stream() + .chatResponse(); + + return Map.of("messages", chatResponseFlux); + } + } + + /** + * 处理流式输出的节点 - 接收并处理流式响应 + */ + public static class ProcessStreamingNode implements NodeAction { + + @Override + public Map apply(OverAllState state) { + // 从状态中获取流式响应结果 + Object messages = state.value("messages").orElse(""); + String result = "流式响应已处理完成: " + messages; + return Map.of("result", result); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/CancellationExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/CancellationExample.java new file mode 100644 index 00000000..0ec5376f --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/CancellationExample.java @@ -0,0 +1,171 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.NodeOutput; +import com.alibaba.cloud.ai.graph.RunnableConfig; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import reactor.core.Disposable; +import reactor.core.publisher.Flux; + +/** + * Graph 执行取消示例 + * 演示如何取消图的执行 + */ +public class CancellationExample { + + /** + * 示例 1: 使用 forEachAsync 消费流时取消 + */ + public static void cancelWithForEachAsync(CompiledGraph compiledGraph, boolean mayInterruptIfRunning) { + // 创建运行配置 + RunnableConfig runnableConfig = RunnableConfig.builder() + .threadId("test-thread") + .build(); + + // 准备输入数据 + Map inputData = new HashMap<>(); + // ... 添加输入数据 + + // 执行图并获取流 + Flux stream = compiledGraph.stream(inputData, runnableConfig); + + // 从新线程在 500 毫秒后请求取消 + CompletableFuture.runAsync(() -> { + try { + Thread.sleep(500); + // Flux 使用 dispose() 来取消 + System.out.println("请求取消执行"); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + // 异步处理每个输出 + var disposable = stream.subscribe( + output -> System.out.println("当前迭代节点: " + output), + error -> System.out.println("流错误: " + error.getMessage()), + () -> System.out.println("流完成") + ); + + // 等待流完成或取消 + try { + stream.blockLast(); + } + catch (Exception e) { + System.err.println("执行异常: " + e.getMessage()); + } + + // 验证是否已取消(Flux 使用 isDisposed 检查) + System.out.println("是否已取消: " + disposable.isDisposed()); + } + + /** + * 示例 2: 使用迭代器消费流时取消 + */ + public static void cancelWithIterator(CompiledGraph compiledGraph, boolean mayInterruptIfRunning) { + // 创建运行配置 + RunnableConfig runnableConfig = RunnableConfig.builder() + .threadId("test-thread") + .build(); + + // 准备输入数据 + Map inputData = new HashMap<>(); + // ... 添加输入数据 + + // 执行图并获取流 + Flux stream = compiledGraph.stream(inputData, runnableConfig); + + // 从新线程在 500 毫秒后请求取消 + var disposable = stream.subscribe( + output -> { + System.out.println("当前迭代节点: " + output); + }, + error -> { + System.out.println("流错误: " + error.getMessage()); + }, + () -> { + System.out.println("流完成"); + } + ); + + CompletableFuture.runAsync(() -> { + try { + Thread.sleep(500); + disposable.dispose(); // 取消流 + System.out.println("已请求取消执行"); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + // 等待流完成或取消 + try { + stream.blockLast(); + } + catch (Exception e) { + System.out.println("流被中断: " + e.getMessage()); + } + + // 验证取消状态 + System.out.println("是否已取消: " + disposable.isDisposed()); + } + + /** + * 检查取消状态 + */ + public static void checkCancellationStatus(Disposable disposable) { + if (disposable.isDisposed()) { + System.out.println("流已被取消"); + } + else { + System.out.println("流仍在运行"); + } + } + + public static void main(String[] args) { + System.out.println("=== Graph 执行取消示例 ===\n"); + + try { + // 示例 1: 使用 forEachAsync 消费流时取消(需要 CompiledGraph) + System.out.println("示例 1: 使用 forEachAsync 消费流时取消"); + System.out.println("注意: 此示例需要 CompiledGraph,跳过执行"); + // cancelWithForEachAsync(compiledGraph, true); + System.out.println(); + + // 示例 2: 使用迭代器消费流时取消(需要 CompiledGraph) + System.out.println("示例 2: 使用迭代器消费流时取消"); + System.out.println("注意: 此示例需要 CompiledGraph,跳过执行"); + // cancelWithIterator(compiledGraph, true); + System.out.println(); + + System.out.println("所有示例执行完成"); + System.out.println("提示: 请配置 CompiledGraph 后运行完整示例"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/CheckpointRedisExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/CheckpointRedisExample.java new file mode 100644 index 00000000..514564eb --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/CheckpointRedisExample.java @@ -0,0 +1,255 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.redis.RedisSaver; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.StateSnapshot; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * Redis 检查点持久化示例 + * 演示如何使用 Redis 数据库持久化工作流状态 + */ +public class CheckpointRedisExample { + + /** + * 初始化 RedisSaver + */ + public static RedisSaver createRedisSaver() { + // 配置 Redisson 客户端 + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://localhost:6379"); // Redis 地址 + + RedissonClient redisson = Redisson.create(config); + return RedisSaver.builder().redisson(redisson).build(); + } + + /** + * 使用自定义 Redis 地址创建 RedisSaver + */ + public static RedisSaver createRedisSaver(String host, int port) { + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + host + ":" + port); + + RedissonClient redisson = Redisson.create(config); + return RedisSaver.builder().redisson(redisson).build(); + } + + /** + * 完整示例: 使用 Redis 检查点持久化 + * + * @return + */ + public static void testCheckpointWithRedis(StateGraph stateGraph) throws Exception { + // 初始化 Redis Saver + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://localhost:6379"); + + RedissonClient redisson = Redisson.create(config); + try { + RedisSaver saver = RedisSaver.builder().redisson(redisson).build(); + + SaverConfig saverConfig = SaverConfig.builder() + .register(saver) + .build(); + + // 使用检查点编译图 + CompiledGraph workflow = stateGraph.compile( + CompileConfig.builder() + .saverConfig(saverConfig) + .build() + ); + + // 执行工作流 + RunnableConfig runnableConfig = RunnableConfig.builder() + .threadId("test-thread-1") + .build(); + + Map inputs = Map.of("input", "test1"); + OverAllState result = workflow.invoke(inputs, runnableConfig).orElseThrow(); + + // 获取检查点历史 + List history = (List) workflow.getStateHistory(runnableConfig); + + System.out.println("检查点历史数量: " + history.size()); + + // 获取最后保存的检查点 + StateSnapshot lastSnapshot = workflow.getState(runnableConfig); + + System.out.println("最后检查点节点: " + lastSnapshot.node()); + + } finally { + redisson.shutdown(); + } + } + + /** + * 从 Redis 重新加载检查点 + * + * @return + */ + public static void reloadCheckpointFromRedis(StateGraph stateGraph) throws GraphStateException { + // 创建新的 saver(重置缓存) + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://localhost:6379"); + + RedissonClient redisson = Redisson.create(config); + try { + RedisSaver newSaver = RedisSaver.builder().redisson(redisson).build(); + + SaverConfig newSaverConfig = SaverConfig.builder() + .register(newSaver) + .build(); + + // 重新编译图 + CompiledGraph reloadedWorkflow = stateGraph.compile( + CompileConfig.builder() + .saverConfig(newSaverConfig) + .build() + ); + + // 使用相同的 threadId 获取历史 + RunnableConfig reloadConfig = RunnableConfig.builder() + .threadId("test-thread-1") + .build(); + + Collection reloadedHistory = reloadedWorkflow.getStateHistory(reloadConfig); + + System.out.println("重新加载的检查点历史数量: " + reloadedHistory.size()); + } finally { + redisson.shutdown(); + } + + } + + /** + * 从特定检查点恢复 + */ + public static void restoreFromCheckpoint(StateGraph stateGraph) throws GraphStateException{ + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://localhost:6379"); + + RedissonClient redisson = Redisson.create(config); + try { + RedisSaver newSaver = RedisSaver.builder().redisson(redisson).build(); + + SaverConfig newSaverConfig = SaverConfig.builder() + .register(newSaver) + .build(); + + // 重新编译图 + CompiledGraph reloadedWorkflow = stateGraph.compile( + CompileConfig.builder() + .saverConfig(newSaverConfig) + .build() + ); + // 获取特定检查点 + RunnableConfig checkpointConfig = RunnableConfig.builder() + .threadId("thread-id") + .checkPointId("specific-checkpoint-id") + .build(); + + // 从该检查点继续 + reloadedWorkflow.invoke(Map.of(), checkpointConfig); + System.out.println("从检查点恢复执行完成"); + } + finally { + redisson.shutdown(); + } + + } + + public static void main(String[] args) { + System.out.println("=== Redis 检查点持久化示例 ===\n"); + + try { + + // 定义状态策略 + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("input", new ReplaceStrategy()); + keyStrategyMap.put("agent_1:prop1", new ReplaceStrategy()); + return keyStrategyMap; + }; + + // 定义节点 + var agent1 = node_async(state -> { + System.out.println("agent_1 执行中"); + return Map.of("agent_1:prop1", "agent_1:test"); + }); + + // 构建图 + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("agent_1", agent1) + .addEdge(START, "agent_1") + .addEdge("agent_1", END); + + // 示例 1: 完整示例 - 使用 Redis 检查点持久化 + System.out.println("示例 1: 使用 Redis 检查点持久化"); + System.out.println("注意: 此示例需要 Redis 连接"); + testCheckpointWithRedis(stateGraph); + System.out.println(); + + // 示例 2: 从 Redis 重新加载检查点 + System.out.println("示例 2: 从 Redis 重新加载检查点"); + System.out.println("注意: 此示例需要 Redis 连接"); + reloadCheckpointFromRedis(stateGraph); + System.out.println(); + + // 示例 3: 从特定检查点恢复 + System.out.println("示例 3: 从特定检查点恢复"); + System.out.println("注意: 此示例需要有效的 CompiledGraph 和 checkpointId"); + restoreFromCheckpoint(stateGraph); + System.out.println(); + + System.out.println("所有示例执行完成"); + System.out.println("提示: 请配置 Redis 连接后运行完整示例"); + System.out.println("提示: 需要添加 Redisson 依赖: org.redisson:redisson"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/HumanInTheLoopExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/HumanInTheLoopExample.java new file mode 100644 index 00000000..f86fe815 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/HumanInTheLoopExample.java @@ -0,0 +1,448 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.NodeOutput; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.action.AsyncNodeActionWithConfig; +import com.alibaba.cloud.ai.graph.action.InterruptableAction; +import com.alibaba.cloud.ai.graph.action.InterruptionMetadata; +import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 人类反馈(Human-in-the-Loop)示例 + * + * 在实际业务场景中,经常会遇到人类介入的场景,人类的不同操作将影响工作流不同的走向。 + * Spring AI Alibaba Graph 提供了两种方式来实现人类反馈: + * + * 1. InterruptionMetadata 模式:可以在任意节点随时中断,通过实现 InterruptableAction 接口来控制中断时机 + * 2. interruptBefore 模式:需要提前在编译配置中定义中断点,在指定节点执行前中断 + */ +public class HumanInTheLoopExample { + + // ==================== 模式一:InterruptionMetadata 模式 ==================== + + /** + * 定义带中断的 Graph(InterruptionMetadata 模式) + * 使用 InterruptableAction 实现中断,不需要 interruptBefore 配置 + */ + public static CompiledGraph createGraphWithInterruptableAction() throws GraphStateException { + // 定义普通节点 + var step1 = node_async(state -> { + return Map.of("messages", "Step 1"); + }); + + // 定义可中断节点(实现 InterruptableAction) + var humanFeedback = new InterruptableNodeAction("human_feedback", "等待用户输入"); + + var step3 = node_async(state -> { + return Map.of("messages", "Step 3"); + }); + + // 定义条件边:根据 human_feedback 的值决定路由 + var evalHumanFeedback = edge_async(state -> { + var feedback = (String) state.value("human_feedback").orElse("unknown"); + return (feedback.equals("next") || feedback.equals("back")) ? feedback : "unknown"; + }); + + // 配置 KeyStrategyFactory + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap keyStrategyHashMap = new HashMap<>(); + keyStrategyHashMap.put("messages", new AppendStrategy()); + keyStrategyHashMap.put("human_feedback", new ReplaceStrategy()); + return keyStrategyHashMap; + }; + + // 构建 Graph + StateGraph builder = new StateGraph(keyStrategyFactory) + .addNode("step_1", step1) + .addNode("human_feedback", humanFeedback) // 使用可中断节点 + .addNode("step_3", step3) + .addEdge(START, "step_1") + .addEdge("step_1", "human_feedback") + .addConditionalEdges("human_feedback", evalHumanFeedback, + Map.of("back", "step_1", "next", "step_3", "unknown", "human_feedback")) + .addEdge("step_3", END); + + // 配置内存保存器(用于状态持久化) + var saver = new MemorySaver(); + + var compileConfig = CompileConfig.builder() + .saverConfig(SaverConfig.builder() + .register(saver) + .build()) + // 不再需要 interruptBefore 配置,中断由 InterruptableAction 控制 + .build(); + + return builder.compile(compileConfig); + } + + /** + * 执行 Graph 直到中断(InterruptionMetadata 模式) + * 检查流式输出中的 InterruptionMetadata + */ + public static InterruptionMetadata executeUntilInterruptWithMetadata(CompiledGraph graph) { + // 初始输入 + Map initialInput = Map.of("messages", "Step 0"); + + // 配置线程 ID + var invokeConfig = RunnableConfig.builder() + .threadId("Thread1") + .build(); + + // 用于保存最后一个输出 + AtomicReference lastOutputRef = new AtomicReference<>(); + + // 运行 Graph 直到第一个中断点 + graph.stream(initialInput, invokeConfig) + .doOnNext(event -> { + System.out.println("节点输出: " + event); + lastOutputRef.set(event); + }) + .doOnError(error -> System.err.println("流错误: " + error.getMessage())) + .doOnComplete(() -> System.out.println("流完成")) + .blockLast(); + + // 检查最后一个输出是否是 InterruptionMetadata + NodeOutput lastOutput = lastOutputRef.get(); + if (lastOutput instanceof InterruptionMetadata) { + System.out.println("\n检测到中断: " + lastOutput); + return (InterruptionMetadata) lastOutput; + } + + return null; + } + + /** + * 等待用户输入并更新状态(InterruptionMetadata 模式) + */ + public static RunnableConfig waitUserInputAndUpdateStateWithMetadata(CompiledGraph graph, InterruptionMetadata interruption) throws Exception { + var invokeConfig = RunnableConfig.builder() + .threadId("Thread1") + .build(); + + // 检查当前状态 + System.out.printf("\n--State before update--\n%s\n", graph.getState(invokeConfig)); + + // 模拟用户输入 + var userInput = "back"; // "back" 表示返回上一个节点 + System.out.printf("\n--User Input--\n用户选择: '%s'\n\n", userInput); + + // 更新状态:添加 human_feedback + // 使用 updateState 更新状态,传入中断时的节点 ID + var updatedConfig = graph.updateState(invokeConfig, Map.of("human_feedback", userInput), interruption.node()); + + // 检查更新后的状态 + System.out.printf("--State after update--\n%s\n", graph.getState(updatedConfig)); + + return updatedConfig; + } + + /** + * 继续执行 Graph(InterruptionMetadata 模式) + * 使用 HUMAN_FEEDBACK_METADATA_KEY 来恢复执行 + */ + public static void continueExecutionWithMetadata(CompiledGraph graph, RunnableConfig updatedConfig) { + // 创建恢复配置,添加 HUMAN_FEEDBACK_METADATA_KEY + RunnableConfig resumeConfig = RunnableConfig.builder(updatedConfig) + .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, "placeholder") + .build(); + + System.out.println("\n--继续执行 Graph--"); + + // 继续执行 Graph(input 为 null,使用之前的状态) + graph.stream(null, resumeConfig) + .doOnNext(event -> System.out.println("节点输出: " + event)) + .doOnError(error -> System.err.println("流错误: " + error.getMessage())) + .doOnComplete(() -> System.out.println("流完成")) + .blockLast(); + } + + // ==================== 模式二:interruptBefore 模式 ==================== + + /** + * 定义带中断的 Graph(interruptBefore 模式) + * 使用 interruptBefore 配置在指定节点前中断 + */ + public static CompiledGraph createGraphWithInterruptBefore() throws GraphStateException { + // 定义节点 + var step1 = node_async(state -> { + return Map.of("messages", "Step 1"); + }); + + var humanFeedback = node_async(state -> { + return Map.of(); // 等待用户输入,不修改状态 + }); + + var step3 = node_async(state -> { + return Map.of("messages", "Step 3"); + }); + + // 定义条件边 + var evalHumanFeedback = edge_async(state -> { + var feedback = (String) state.value("human_feedback").orElse("unknown"); + return (feedback.equals("next") || feedback.equals("back")) ? feedback : "unknown"; + }); + + // 配置 KeyStrategyFactory + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap keyStrategyHashMap = new HashMap<>(); + keyStrategyHashMap.put("messages", new AppendStrategy()); + keyStrategyHashMap.put("human_feedback", new ReplaceStrategy()); + return keyStrategyHashMap; + }; + + // 构建 Graph + StateGraph builder = new StateGraph(keyStrategyFactory) + .addNode("step_1", step1) + .addNode("human_feedback", humanFeedback) + .addNode("step_3", step3) + .addEdge(START, "step_1") + .addEdge("step_1", "human_feedback") + .addConditionalEdges("human_feedback", evalHumanFeedback, + Map.of("back", "step_1", "next", "step_3", "unknown", "human_feedback")) + .addEdge("step_3", END); + + // 配置内存保存器和中断点 + var saver = new MemorySaver(); + + var compileConfig = CompileConfig.builder() + .saverConfig(SaverConfig.builder() + .register(saver) + .build()) + .interruptBefore("human_feedback") // 在 human_feedback 节点前中断 + .build(); + + return builder.compile(compileConfig); + } + + /** + * 执行 Graph 直到中断(interruptBefore 模式) + */ + public static void executeUntilInterruptWithInterruptBefore(CompiledGraph graph) { + // 初始输入 + Map initialInput = Map.of("messages", "Step 0"); + + // 配置线程 ID + var invokeConfig = RunnableConfig.builder() + .threadId("Thread1") + .build(); + + // 运行 Graph 直到第一个中断点 + graph.stream(initialInput, invokeConfig) + .doOnNext(event -> System.out.println(event)) + .doOnError(error -> System.err.println("流错误: " + error.getMessage())) + .doOnComplete(() -> System.out.println("流完成")) + .blockLast(); + } + + /** + * 等待用户输入并更新状态(interruptBefore 模式) + */ + public static RunnableConfig waitUserInputAndUpdateStateWithInterruptBefore(CompiledGraph graph) throws Exception { + var invokeConfig = RunnableConfig.builder() + .threadId("Thread1") + .build(); + + // 检查当前状态 + System.out.printf("--State before update--\n%s\n", graph.getState(invokeConfig)); + + // 模拟用户输入 + var userInput = "back"; // "back" 表示返回上一个节点 + System.out.printf("\n--User Input--\n用户选择: '%s'\n\n", userInput); + + // 更新状态(模拟 human_feedback 节点的输出) + // 注意:interruptBefore 模式下,传入 null 作为节点 ID + var updateConfig = graph.updateState(invokeConfig, Map.of("human_feedback", userInput), null); + + // 检查更新后的状态 + System.out.printf("--State after update--\n%s\n", graph.getState(updateConfig)); + + return updateConfig; + } + + /** + * 继续执行 Graph(interruptBefore 模式) + */ + public static void continueExecutionWithInterruptBefore(CompiledGraph graph, RunnableConfig updateConfig) { + // 添加恢复执行的元数据标记 + RunnableConfig resumeConfig = RunnableConfig.builder(updateConfig) + .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, "placeholder") + .build(); + + // 继续执行 Graph(input 为 null,使用之前的状态) + graph.stream(null, resumeConfig) + .doOnNext(event -> System.out.println(event)) + .doOnError(error -> System.err.println("流错误: " + error.getMessage())) + .doOnComplete(() -> System.out.println("流完成")) + .blockLast(); + } + + /** + * 第二次等待用户输入(interruptBefore 模式) + */ + public static RunnableConfig waitUserInputSecondTime(CompiledGraph graph, RunnableConfig invokeConfig) throws Exception { + var userInput = "next"; // "next" 表示继续下一个节点 + System.out.printf("\n--User Input--\n用户选择: '%s'\n", userInput); + + // 更新状态 + var updateConfig = graph.updateState(invokeConfig, Map.of("human_feedback", userInput), null); + + System.out.printf("\ngetNext()\n\twith invokeConfig:[%s]\n\twith updateConfig:[%s]\n", + graph.getState(invokeConfig).next(), + graph.getState(updateConfig).next()); + + return updateConfig; + } + + /** + * 继续执行直到完成(interruptBefore 模式) + */ + public static void continueExecutionUntilComplete(CompiledGraph graph, RunnableConfig updateConfig) { + // 添加恢复执行的元数据标记 + RunnableConfig resumeConfig = RunnableConfig.builder(updateConfig) + .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, "placeholder") + .build(); + + // 继续执行 Graph + graph.stream(null, resumeConfig) + .doOnNext(event -> System.out.println(event)) + .doOnError(error -> System.err.println("流错误: " + error.getMessage())) + .doOnComplete(() -> System.out.println("流完成")) + .blockLast(); + } + + // ==================== 可中断的节点动作类 ==================== + + /** + * 可中断的节点动作 + * 实现 InterruptableAction 接口,可以在任意节点中断执行 + */ + public static class InterruptableNodeAction implements AsyncNodeActionWithConfig, InterruptableAction { + private final String nodeId; + private final String message; + + public InterruptableNodeAction(String nodeId, String message) { + this.nodeId = nodeId; + this.message = message; + } + + @Override + public CompletableFuture> apply(OverAllState state, RunnableConfig config) { + // 正常节点逻辑:更新状态 + return CompletableFuture.completedFuture(Map.of("messages", message)); + } + + @Override + public Optional interrupt(String nodeId, OverAllState state, RunnableConfig config) { + // 检查是否需要中断 + // 如果状态中没有 human_feedback,则中断等待用户输入 + Optional humanFeedback = state.value("human_feedback"); + + if (humanFeedback.isEmpty()) { + // 返回 InterruptionMetadata 来中断执行 + InterruptionMetadata interruption = InterruptionMetadata.builder(nodeId, state) + .addMetadata("message", "等待用户输入...") + .addMetadata("node", nodeId) + // 如果要做工具确认的话,可以在这里添加 toolFeedbacks,具体可参考 HumanInTheLoopHook 实现 + //.toolFeedbacks(List.of(InterruptionMetadata.ToolFeedback.builder().description("").build())) + .build(); + + return Optional.of(interruption); + } + + // 如果已经有 human_feedback,继续执行 + return Optional.empty(); + } + } + + // ==================== 主方法 ==================== + + public static void main(String[] args) throws Exception { + System.out.println("========================================"); + System.out.println("人类反馈(Human-in-the-Loop)示例"); + System.out.println("========================================\n"); + + // ========== 模式一:InterruptionMetadata 模式 ========== + System.out.println("=== 模式一:InterruptionMetadata 模式 ==="); + System.out.println("演示如何在任意节点实现 InterruptableAction,通过返回 InterruptionMetadata 实现中断\n"); + + CompiledGraph graph1 = createGraphWithInterruptableAction(); + + // 执行直到中断 + InterruptionMetadata interruption = executeUntilInterruptWithMetadata(graph1); + + if (interruption != null) { + // 等待用户输入并更新状态 + RunnableConfig updatedConfig = waitUserInputAndUpdateStateWithMetadata(graph1, interruption); + + // 继续执行 + continueExecutionWithMetadata(graph1, updatedConfig); + } + + System.out.println("\n模式一示例执行完成\n"); + + // ========== 模式二:interruptBefore 模式 ========== + System.out.println("=== 模式二:interruptBefore 模式 ==="); + System.out.println("演示如何使用 interruptBefore 配置在指定节点前中断\n"); + + CompiledGraph graph2 = createGraphWithInterruptBefore(); + + // 执行直到中断 + executeUntilInterruptWithInterruptBefore(graph2); + + // 等待用户输入并更新状态 + RunnableConfig updateConfig1 = waitUserInputAndUpdateStateWithInterruptBefore(graph2); + + // 继续执行 + continueExecutionWithInterruptBefore(graph2, updateConfig1); + + // 第二次等待用户输入 + var invokeConfig = RunnableConfig.builder() + .threadId("Thread1") + .build(); + RunnableConfig updateConfig2 = waitUserInputSecondTime(graph2, invokeConfig); + + // 继续执行直到完成 + continueExecutionUntilComplete(graph2, updateConfig2); + + System.out.println("\n模式二示例执行完成"); + System.out.println("\n========================================"); + System.out.println("所有示例执行完成"); + System.out.println("========================================"); + } +} diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/LlmStreamingSpringAiExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/LlmStreamingSpringAiExample.java new file mode 100644 index 00000000..25dd6a21 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/LlmStreamingSpringAiExample.java @@ -0,0 +1,120 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.action.NodeAction; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatResponse; + +import java.util.Map; + +import reactor.core.publisher.Flux; + +/** + * Spring AI Alibaba LLM 流式集成示例 + * 演示如何在 Spring AI Alibaba Graph 中使用 LLM 流式输出功能 + */ +public class LlmStreamingSpringAiExample { + + /** + * 使用流式 ChatClient + */ + public static void useStreamingChatClient(ChatClient chatClient) { + // 使用流式输出 + Flux flux = chatClient.prompt() + .user("tell me a joke") + .stream() + .chatResponse(); + + // 订阅流式响应 + flux.subscribe( + response -> { + String content = response.getResult().getOutput().getText(); + System.out.print(content); + }, + error -> System.err.println("Error: " + error.getMessage()), + () -> System.out.println("\nStream completed") + ); + } + + /** + * 使用 Reactor 的阻塞式处理 + */ + public static void useBlockingStreaming(ChatClient chatClient) { + Flux flux = chatClient.prompt() + .user("tell me a joke") + .stream() + .chatResponse(); + + // 使用 Reactor 的阻塞式处理 + flux.collectList().block().forEach(response -> { + System.out.println("Received: " + response.getResult().getOutput().getText()); + }); + } + + public static void main(String[] args) { + System.out.println("=== Spring AI Alibaba LLM 流式集成示例 ===\n"); + + try { + // 示例 1: 使用流式 ChatClient(需要 ChatClient) + System.out.println("示例 1: 使用流式 ChatClient"); + System.out.println("注意: 此示例需要 ChatClient,跳过执行"); + // useStreamingChatClient(chatClient); + System.out.println(); + + // 示例 2: 使用 Reactor 的阻塞式处理(需要 ChatClient) + System.out.println("示例 2: 使用 Reactor 的阻塞式处理"); + System.out.println("注意: 此示例需要 ChatClient,跳过执行"); + // useBlockingStreaming(chatClient); + System.out.println(); + + System.out.println("所有示例执行完成"); + System.out.println("提示: 请配置 ChatClient 后运行完整示例"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 在 Graph 节点中使用流式输出 + */ + public static class StreamingAgentNode implements NodeAction { + + private final ChatClient chatClient; + + public StreamingAgentNode(ChatClient.Builder builder) { + this.chatClient = builder.build(); + } + + @Override + public Map apply(OverAllState state) { + String userMessage = (String) state.value("query").orElse("Hello"); + + // 使用流式输出 + Flux contentFlux = chatClient.prompt() + .user(userMessage) + .stream() + .content(); + + return Map.of("answer", contentFlux); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/LongTimeRunningTaskExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/LongTimeRunningTaskExample.java new file mode 100644 index 00000000..3e3ac40f --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/LongTimeRunningTaskExample.java @@ -0,0 +1,172 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 持久化执行示例 + * 演示长时间运行任务的持久化执行 + */ +public class LongTimeRunningTaskExample { + + /** + * 示例: 长时间运行的数据处理任务 + */ + public static void longRunningDataProcessingTask() throws GraphStateException { + // 定义状态 + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("items", new ReplaceStrategy()); + keyStrategyMap.put("processedCount", new ReplaceStrategy()); + keyStrategyMap.put("results", new AppendStrategy()); + return keyStrategyMap; + }; + + // 处理数据的节点 + var processData = node_async(state -> { + List items = (List) state.value("items").orElse(List.of()); + int processedCount = (int) state.value("processedCount").orElse(0); + + // 批量处理(例如每次处理 100 个) + int batchSize = 100; + int start = processedCount; + int end = Math.min(start + batchSize, items.size()); + + List batch = items.subList(start, end); + List processedResults = batch.stream() + .map(item -> "Processed: " + item) + .collect(Collectors.toList()); + + return Map.of( + "processedCount", end, + "results", processedResults + ); + }); + + // 检查是否完成 + var checkComplete = edge_async(state -> { + int processedCount = (int) state.value("processedCount").orElse(0); + List items = (List) state.value("items").orElse(List.of()); + + return processedCount >= items.size() ? END : "process_data"; + }); + + // 创建图 + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("process_data", processData) + .addEdge(START, "process_data") + .addConditionalEdges("process_data", checkComplete, + Map.of(END, END, "process_data", "process_data")); + + // 配置持久化 + SaverConfig saverConfig = SaverConfig.builder() + .register(new MemorySaver()) + .build(); + + CompiledGraph graph = stateGraph.compile( + CompileConfig.builder() + .saverConfig(saverConfig) + .build() + ); + + // 执行长时间运行的任务 + RunnableConfig config = RunnableConfig.builder() + .threadId("long-running-task-" + UUID.randomUUID()) + .build(); + + // 创建大量数据 + List largeDataSet = IntStream.range(0, 10000) + .mapToObj(i -> "Item-" + i) + .collect(Collectors.toList()); + + // 执行(可能会被中断,但可以恢复) + graph.invoke(Map.of( + "items", largeDataSet, + "processedCount", 0 + ), config); + + System.out.println("Long-running task example executed"); + } + + /** + * 示例: 从错误中恢复 + */ + public static void errorRecoveryExample(CompiledGraph graph) { + String threadId = "error-recovery-thread"; + RunnableConfig config = RunnableConfig.builder() + .threadId(threadId) + .build(); + + try { + // 第一次执行可能会失败 + graph.invoke(Map.of("data", "test"), config); + } + catch (Exception e) { + System.err.println("第一次执行失败,准备重试: " + e.getMessage()); + + // 使用相同的 threadId 重新执行,将从检查点恢复 + // 传入 null 作为输入,表示从上次状态继续 + graph.invoke(Map.of(), config); + } + } + + public static void main(String[] args) { + System.out.println("=== 持久化执行示例 ===\n"); + + try { + // 示例 1: 长时间运行的数据处理任务 + System.out.println("示例 1: 长时间运行的数据处理任务"); + longRunningDataProcessingTask(); + System.out.println(); + + // 示例 2: 从错误中恢复(需要 CompiledGraph) + System.out.println("示例 2: 从错误中恢复"); + System.out.println("注意: 此示例需要 CompiledGraph,跳过执行"); + // errorRecoveryExample(graph); + System.out.println(); + + System.out.println("所有示例执行完成"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/McpNodeExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/McpNodeExample.java new file mode 100644 index 00000000..0e8c196b --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/McpNodeExample.java @@ -0,0 +1,92 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.action.NodeAction; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.tool.ToolCallback; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import reactor.core.publisher.Flux; + +/** + * MCP 节点示例 + * 演示如何为指定节点分配 MCP 工具 + */ +public class McpNodeExample { + + /** + * 配置 MCP 节点 + */ + public static void configureMcpNode(ChatClient.Builder chatClientBuilder, Set toolCallbacks) { + McpNode mcpNode = new McpNode(chatClientBuilder, toolCallbacks); + System.out.println("MCP node configured successfully"); + } + + public static void main(String[] args) { + System.out.println("=== MCP 节点示例 ===\n"); + + try { + // 示例: 配置 MCP 节点(需要 ChatClient 和 ToolCallbacks) + System.out.println("示例: 配置 MCP 节点"); + System.out.println("注意: 此示例需要 ChatClient 和 ToolCallbacks,跳过执行"); + // configureMcpNode(ChatClient.builder(...), toolCallbacks); + System.out.println(); + + System.out.println("所有示例执行完成"); + System.out.println("提示: 请配置 ChatClient 和 ToolCallbacks 后运行完整示例"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * MCP 节点实现 + */ + public static class McpNode implements NodeAction { + + private static final String NODENAME = "mcp-node"; + + private final ChatClient chatClient; + + public McpNode(ChatClient.Builder chatClientBuilder, Set toolCallbacks) { + // 为节点配置 MCP 工具 + this.chatClient = chatClientBuilder + .defaultToolCallbacks(toolCallbacks.toArray(ToolCallback[]::new)) + .build(); + } + + @Override + public Map apply(OverAllState state) { + String query = state.value("query", ""); + Flux streamResult = chatClient.prompt(query).stream().content(); + String result = streamResult.reduce("", (acc, item) -> acc + item).block(); + + HashMap resultMap = new HashMap<>(); + resultMap.put("mcpcontent", result); + + return resultMap; + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/MultiAgentSupervisorExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/MultiAgentSupervisorExample.java new file mode 100644 index 00000000..9c066040 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/MultiAgentSupervisorExample.java @@ -0,0 +1,482 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.function.FunctionToolCallback; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * Multi-Agent Supervisor 示例 + * + * 演示如何使用 LLM 来协调不同的 agents。 + * 创建一个 agent 组,其中包含一个 supervisor agent 来帮助委派任务。 + * + * 架构: + * - Supervisor Agent: 负责路由到不同的 worker agents + * - Researcher Agent: 负责研究任务,使用搜索工具 + * - Coder Agent: 负责代码执行任务,使用代码执行工具 + */ +public class MultiAgentSupervisorExample { + + private final ChatModel chatModel; + private final ChatModel chatModelWithTool; + + public MultiAgentSupervisorExample(ChatModel chatModel, ChatModel chatModelWithTool) { + this.chatModel = chatModel; + this.chatModelWithTool = chatModelWithTool; + } + + /** + * Main 方法 + */ + public static void main(String[] args) { + System.out.println("=== Multi-Agent Supervisor 示例 ===\n"); + + // 检查环境变量 + String apiKey = System.getenv("ALI_AI_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + System.err.println("错误:请先配置 AI_DASHSCOPE_API_KEY 环境变量"); + System.err.println("示例需要 DashScope API Key 才能运行"); + return; + } + + try { + // 创建 DashScope API 实例 + DashScopeApi dashScopeApi = DashScopeApi.builder() + .apiKey(apiKey) + .build(); + + // 创建 ChatModel(用于 Supervisor) + ChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建 ChatModel(用于 Worker Agents,可以使用相同的模型) + ChatModel chatModelWithTool = DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .build(); + + // 创建示例实例 + MultiAgentSupervisorExample example = new MultiAgentSupervisorExample( + chatModel, chatModelWithTool); + + // 创建 Graph + System.out.println("创建 Multi-Agent Supervisor Graph..."); + CompiledGraph graph = example.createSupervisorGraph(); + System.out.println("Graph 创建完成\n"); + + // 执行示例 1: Supervisor -> Coder + example.executeGraphWithCoder(graph); + + // 执行示例 2: Supervisor -> Researcher + example.executeGraphWithResearcher(graph); + + System.out.println("\n所有示例执行完成"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 创建 Multi-Agent Supervisor Graph + */ + public CompiledGraph createSupervisorGraph() throws GraphStateException { + // 定义状态管理策略 + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("messages", new AppendStrategy()); + strategies.put("next", new ReplaceStrategy()); + return strategies; + }; + + // 创建 agents + String[] members = {"researcher", "coder"}; + SupervisorNode supervisor = new SupervisorNode(chatModel, members); + ResearcherNode researcher = new ResearcherNode(chatModelWithTool); + CoderNode coder = new CoderNode(chatModelWithTool); + + // 构建 StateGraph + StateGraph workflow = new StateGraph(keyStrategyFactory) + .addNode("supervisor", node_async(supervisor)) + .addNode("researcher", node_async(researcher)) + .addNode("coder", node_async(coder)) + .addEdge(START, "supervisor") + .addConditionalEdges( + "supervisor", + edge_async(state -> { + String next = (String) state.value("next").orElse("FINISH"); + return next; + }), + Map.of( + "FINISH", END, + "researcher", "researcher", + "coder", "coder" + ) + ) + .addEdge("researcher", "supervisor") + .addEdge("coder", "supervisor"); + + return workflow.compile(); + } + + /** + * 执行 Graph(Supervisor -> Coder) + */ + public void executeGraphWithCoder(CompiledGraph graph) { + System.out.println("\n=== 执行 Graph (Supervisor -> Coder) ==="); + + Map input = Map.of( + "messages", List.of( + Map.of("role", "user", "content", "1 + 1 的结果是多少?") + ) + ); + + RunnableConfig config = RunnableConfig.builder() + .threadId("supervisor-coder-thread") + .build(); + + graph.stream(input, config) + .doOnNext(event -> System.out.println("节点: " + event.node() + ", 状态: " + event.state())) + .doOnError(error -> System.err.println("流错误: " + error.getMessage())) + .doOnComplete(() -> System.out.println("流完成")) + .blockLast(); + } + + /** + * 执行 Graph(Supervisor -> Researcher) + */ + public void executeGraphWithResearcher(CompiledGraph graph) { + System.out.println("\n=== 执行 Graph (Supervisor -> Researcher) ==="); + + Map input = Map.of( + "messages", List.of( + Map.of("role", "user", "content", "下一届冬奥会在哪里举行?") + ) + ); + + RunnableConfig config = RunnableConfig.builder() + .threadId("supervisor-researcher-thread") + .build(); + + graph.stream(input, config) + .doOnNext(event -> System.out.println("节点: " + event.node() + ", 状态: " + event.state())) + .doOnError(error -> System.err.println("流错误: " + error.getMessage())) + .doOnComplete(() -> System.out.println("流完成")) + .blockLast(); + } + + /** + * 搜索工具(模拟实现) + */ + public static class SearchTool implements BiFunction { + + public static final String DESCRIPTION = """ + 使用此工具在互联网上执行搜索。 + + Usage: + - query 参数是要搜索的查询字符串 + - 工具会执行搜索并返回搜索结果 + - 这是一个模拟实现,返回固定的搜索结果 + """; + + @Override + public String apply(SearchRequest request, ToolContext toolContext) { + System.out.println("搜索查询: '" + request.query + "'"); + // 模拟搜索结果 + return "下一届冬奥会将在意大利的科尔蒂纳举行,时间是2026年"; + } + + /** + * 搜索请求结构 + */ + public static class SearchRequest { + @JsonProperty(required = true) + @JsonPropertyDescription("要搜索的查询字符串") + public String query; + + public SearchRequest() { + } + + public SearchRequest(String query) { + this.query = query; + } + } + } + + /** + * 代码执行工具(模拟实现) + */ + public static class CoderTool implements BiFunction { + + public static final String DESCRIPTION = """ + 使用此工具执行 Java 代码并进行数学计算。 + + Usage: + - request 参数是要执行的代码请求 + - 如果你想查看某个值的输出,应该使用 `System.out.println(...);` 打印出来 + - 这对用户可见 + - 这是一个模拟实现,返回固定的执行结果 + """; + + @Override + public String apply(CodeRequest request, ToolContext toolContext) { + System.out.println("代码执行请求: '" + request.request + "'"); + // 模拟代码执行结果 + return "2"; + } + + /** + * 代码执行请求结构 + */ + public static class CodeRequest { + @JsonProperty(required = true) + @JsonPropertyDescription("要执行的代码请求") + public String request; + + public CodeRequest() { + } + + public CodeRequest(String request) { + this.request = request; + } + } + } + + /** + * Supervisor Agent Node + * 负责决定将任务路由到哪个 worker + */ + public static class SupervisorNode implements NodeAction { + private final ChatClient chatClient; + private final String[] members; + + public SupervisorNode(ChatModel model, String[] members) { + this.chatClient = ChatClient.builder(model).build(); + this.members = members; + } + + @Override + public Map apply(OverAllState state) throws Exception { + // 获取最后一条消息 + List messages = (List) state.value("messages").orElse(List.of()); + if (messages.isEmpty()) { + throw new IllegalStateException("No messages in state"); + } + + // 获取最后一条消息的文本内容 + String lastMessageText = extractTextFromMessage(messages.get(messages.size() - 1)); + + // 构建系统提示 + String membersList = String.join(", ", members); + String systemPrompt = String.format( + "你是一个 supervisor,负责管理以下 workers 之间的对话:%s。\n" + + "根据以下用户请求,响应应该由哪个 worker 来处理。\n" + + "每个 worker 将执行任务并返回结果和状态。\n" + + "当任务完成时,响应 FINISH。\n" + + "只返回 worker 名称(%s)或 FINISH,不要返回其他内容。", + membersList, membersList + ); + + // 调用 LLM 决定路由 + String result = chatClient.prompt() + .system(systemPrompt) + .user("用户消息: " + lastMessageText) + .call() + .content(); + + // 清理结果,确保只返回 worker 名称或 FINISH + String next = normalizeRoute(result, members); + + return Map.of("next", next); + } + + /** + * 规范化路由结果 + */ + private String normalizeRoute(String result, String[] members) { + if (result == null || result.trim().isEmpty()) { + return "FINISH"; + } + + String normalized = result.trim().toLowerCase(); + + // 检查是否是 FINISH + if (normalized.equals("finish") || normalized.contains("finish")) { + return "FINISH"; + } + + // 检查是否匹配任何成员 + for (String member : members) { + if (normalized.equals(member.toLowerCase()) || + normalized.contains(member.toLowerCase())) { + return member; + } + } + + // 如果无法确定,根据消息内容推断 + // 这里可以根据实际需求添加更智能的路由逻辑 + // 默认返回第一个 worker + return members.length > 0 ? members[0] : "FINISH"; + } + + private String extractTextFromMessage(Object message) { + if (message instanceof Map) { + Map msgMap = (Map) message; + Object content = msgMap.get("content"); + if (content != null) { + return content.toString(); + } + } + return message.toString(); + } + } + + /** + * Researcher Agent Node + * 负责执行研究任务 + */ + public static class ResearcherNode implements NodeAction { + private final ChatClient chatClient; + + public ResearcherNode(ChatModel model) { + ToolCallback searchTool = FunctionToolCallback.builder("search", new SearchTool()) + .description(SearchTool.DESCRIPTION) + .inputType(SearchTool.SearchRequest.class) + .build(); + + this.chatClient = ChatClient.builder(model) + .defaultTools(searchTool) + .build(); + } + + @Override + public Map apply(OverAllState state) throws Exception { + // 获取最后一条消息 + List messages = (List) state.value("messages").orElse(List.of()); + if (messages.isEmpty()) { + throw new IllegalStateException("No messages in state"); + } + + String lastMessageText = extractTextFromMessage(messages.get(messages.size() - 1)); + + // 使用 ChatClient 调用 LLM,LLM 可能会调用搜索工具 + String result = chatClient.prompt() + .user(lastMessageText) + .call() + .content(); + + // 返回结果消息 + return Map.of("messages", List.of( + Map.of("role", "assistant", "content", result) + )); + } + + private String extractTextFromMessage(Object message) { + if (message instanceof Map) { + Map msgMap = (Map) message; + Object content = msgMap.get("content"); + if (content != null) { + return content.toString(); + } + } + return message.toString(); + } + } + + /** + * Coder Agent Node + * 负责执行代码任务 + */ + public static class CoderNode implements NodeAction { + private final ChatClient chatClient; + + public CoderNode(ChatModel model) { + ToolCallback coderTool = FunctionToolCallback.builder("executeCode", new CoderTool()) + .description(CoderTool.DESCRIPTION) + .inputType(CoderTool.CodeRequest.class) + .build(); + + this.chatClient = ChatClient.builder(model) + .defaultTools(coderTool) + .build(); + } + + @Override + public Map apply(OverAllState state) throws Exception { + // 获取最后一条消息 + List messages = (List) state.value("messages").orElse(List.of()); + if (messages.isEmpty()) { + throw new IllegalStateException("No messages in state"); + } + + String lastMessageText = extractTextFromMessage(messages.get(messages.size() - 1)); + + // 使用 ChatClient 调用 LLM,LLM 可能会调用代码执行工具 + String result = chatClient.prompt() + .user(lastMessageText) + .call() + .content(); + + // 返回结果消息 + return Map.of("messages", List.of( + Map.of("role", "assistant", "content", result) + )); + } + + private String extractTextFromMessage(Object message) { + if (message instanceof Map) { + Map msgMap = (Map) message; + Object content = msgMap.get("content"); + if (content != null) { + return content.toString(); + } + } + return message.toString(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/ParallelBranchExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/ParallelBranchExample.java new file mode 100644 index 00000000..af674525 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/ParallelBranchExample.java @@ -0,0 +1,168 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.action.AsyncNodeAction; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 创建并行节点执行分支示例 + * 演示如何创建并行分支以加速图执行 + */ +public class ParallelBranchExample { + + /** + * 创建节点的辅助方法 + */ + private static AsyncNodeAction makeNode(String message) { + return node_async(state -> Map.of("messages", List.of(message))); + } + + /** + * 定义带并行分支的 Graph + */ + public static CompiledGraph createParallelBranchGraph() throws GraphStateException { + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap keyStrategyHashMap = new HashMap<>(); + keyStrategyHashMap.put("messages", new AppendStrategy()); + return keyStrategyHashMap; + }; + + // 构建并行 Graph + StateGraph workflow = new StateGraph(keyStrategyFactory) + .addNode("A", makeNode("A")) + .addNode("A1", makeNode("A1")) + .addNode("A2", makeNode("A2")) + .addNode("A3", makeNode("A3")) + .addNode("B", makeNode("B")) + .addNode("C", makeNode("C")) + .addEdge("A", "A1") // A 到 A1 + .addEdge("A", "A2") // A 到 A2(并行) + .addEdge("A", "A3") // A 到 A3(并行) + .addEdge("A1", "B") // A1 汇聚到 B + .addEdge("A2", "B") // A2 汇聚到 B + .addEdge("A3", "B") // A3 汇聚到 B + .addEdge("B", "C") + .addEdge(START, "A") + .addEdge("C", END); + + return workflow.compile(); + } + + /** + * 执行并行 Graph + */ + public static void executeParallelGraph(CompiledGraph compiledGraph) { + // 执行 Graph + compiledGraph.stream(Map.of()) + .doOnNext(step -> System.out.println(step)) + .doOnError(error -> System.err.println("流错误: " + error.getMessage())) + .doOnComplete(() -> System.out.println("流完成")) + .blockLast(); + } + + /** + * 使用编译的子图作为并行节点 + */ + public static CompiledGraph useCompiledSubgraphAsParallelNode() throws GraphStateException { + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap keyStrategyHashMap = new HashMap<>(); + keyStrategyHashMap.put("messages", new AppendStrategy()); + return keyStrategyHashMap; + }; + + // 创建子图 A3 + StateGraph subgraphA3Builder = new StateGraph(keyStrategyFactory) + .addNode("A3.1", makeNode("A3.1")) + .addNode("A3.2", makeNode("A3.2")) + .addEdge(START, "A3.1") + .addEdge("A3.1", "A3.2") + .addEdge("A3.2", END); + + CompiledGraph subgraphA3 = subgraphA3Builder.compile(); + + // 创建子图 A1 + StateGraph subgraphA1Builder = new StateGraph(keyStrategyFactory) + .addNode("A1.1", makeNode("A1.1")) + .addNode("A1.2", makeNode("A1.2")) + .addEdge(START, "A1.1") + .addEdge("A1.1", "A1.2") + .addEdge("A1.2", END); + + CompiledGraph subgraphA1 = subgraphA1Builder.compile(); + + // 主图:混合使用节点和子图 + StateGraph workflow = new StateGraph(keyStrategyFactory) + .addNode("A", makeNode("A")) + .addNode("A1", node_async(state -> subgraphA1.invoke(state.data()).orElseThrow().data())) + .addNode("A2", makeNode("A2")) + .addNode("A3", node_async(state -> subgraphA3.invoke(state.data()).orElseThrow().data())) + .addNode("B", makeNode("B")) + .addEdge("A", "A1") + .addEdge("A", "A2") + .addEdge("A", "A3") + .addEdge("A1", "B") + .addEdge("A2", "B") + .addEdge("A3", "B") + .addEdge(START, "A") + .addEdge("B", END); + + return workflow.compile(); + } + + public static void main(String[] args) { + System.out.println("=== 创建并行节点执行分支示例 ===\n"); + + try { + // 示例 1: 定义带并行分支的 Graph + System.out.println("示例 1: 定义带并行分支的 Graph"); + CompiledGraph graph = createParallelBranchGraph(); + System.out.println("并行分支图创建完成"); + System.out.println(); + + // 示例 2: 执行并行 Graph + System.out.println("示例 2: 执行并行 Graph"); + executeParallelGraph(graph); + System.out.println(); + + // 示例 3: 使用编译的子图作为并行节点 + System.out.println("示例 3: 使用编译的子图作为并行节点"); + CompiledGraph subgraphGraph = useCompiledSubgraphAsParallelNode(); + System.out.println("子图作为并行节点示例创建完成"); + System.out.println(); + + System.out.println("所有示例执行完成"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/ParallelStreamingExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/ParallelStreamingExample.java new file mode 100644 index 00000000..8e0ceca2 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/ParallelStreamingExample.java @@ -0,0 +1,251 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.action.AsyncNodeAction; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.streaming.StreamingOutput; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; + +import reactor.core.publisher.Flux; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; + +/** + * 并行流式输出示例 + * 演示如何在并行分支中使用 Flux 实现流式输出 + * 每个并行节点可以独立产生流式输出,并保持各自的节点 ID + */ +public class ParallelStreamingExample { + + /** + * 示例 1: 并行节点流式输出 - 每个节点保持独立的节点 ID + * + * 演示如何创建多个并行节点,每个节点返回 Flux 流式输出 + * 流式输出会保持各自的节点 ID,便于区分不同节点的输出 + */ + public static void parallelStreamingWithNodeIdPreservation() throws GraphStateException { + // 定义状态策略 + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("messages", new AppendStrategy()); + keyStrategyMap.put("parallel_results", new AppendStrategy()); + return keyStrategyMap; + }; + + // 并行节点 1 - 返回 Flux 流式输出 + AsyncNodeAction node1 = state -> { + System.out.println("Node1 executing on thread: " + Thread.currentThread().getName()); + + // 创建流式数据 + Flux stream1 = Flux.just("节点1-块1", "节点1-块2", "节点1-块3") + .delayElements(Duration.ofMillis(50)) + .doOnNext(chunk -> + System.out.println("Node1 streaming emitting on thread: " + Thread.currentThread().getName()) + ); + + return CompletableFuture.completedFuture(Map.of("stream1", stream1)); + }; + + // 并行节点 2 - 返回 Flux 流式输出 + AsyncNodeAction node2 = state -> { + System.out.println("Node2 executing on thread: " + Thread.currentThread().getName()); + + // 创建流式数据(延迟时间不同,模拟不同的处理速度) + Flux stream2 = Flux.just("节点2-块1", "节点2-块2", "节点2-块3") + .delayElements(Duration.ofMillis(75)) + .doOnNext(chunk -> + System.out.println("Node2 streaming emitting on thread: " + Thread.currentThread().getName()) + ); + + return CompletableFuture.completedFuture(Map.of("stream2", stream2)); + }; + + // 合并节点 - 接收并行节点的结果 + AsyncNodeAction mergeNode = state -> { + System.out.println("\n合并节点接收到状态: " + state.data()); + return CompletableFuture.completedFuture( + Map.of("messages", "所有并行节点已完成,结果已合并") + ); + }; + + // 构建图:两个并行节点从 START 开始,都汇聚到 merge 节点 + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("node1", node1) + .addNode("node2", node2) + .addNode("merge", mergeNode) + .addEdge(START, "node1") // 并行分支 1 + .addEdge(START, "node2") // 并行分支 2 + .addEdge("node1", "merge") // 汇聚到合并节点 + .addEdge("node2", "merge") // 汇聚到合并节点 + .addEdge("merge", END); + + // 编译图 + CompiledGraph graph = stateGraph.compile( + CompileConfig.builder() + .build() + ); + + // 创建配置 + RunnableConfig config = RunnableConfig.builder() + .threadId("parallel_streaming_thread") + .build(); + + // 跟踪每个节点产生的流式输出数量 + Map nodeStreamCounts = new HashMap<>(); + AtomicInteger totalChunks = new AtomicInteger(0); + + System.out.println("开始并行流式输出...\n"); + + // 执行流式图并处理输出 + graph.stream(Map.of("input", "test"), config) + .doOnNext(output -> { + if (output instanceof StreamingOutput streamingOutput) { + // 处理流式输出 + String nodeId = streamingOutput.node(); + String chunk = streamingOutput.chunk(); + + // 统计每个节点的流式输出 + nodeStreamCounts.merge(nodeId, 1, Integer::sum); + totalChunks.incrementAndGet(); + + // 实时打印流式内容,显示节点 ID + System.out.println("[流式输出] 节点: " + nodeId + + ", 内容: " + chunk); + } + else { + // 处理普通节点输出 + String nodeId = output.node(); + Map state = output.state().data(); + System.out.println("\n[节点完成] " + nodeId + + ", 状态: " + state); + } + }) + .doOnComplete(() -> { + System.out.println("\n=== 并行流式输出完成 ==="); + System.out.println("总流式块数: " + totalChunks.get()); + System.out.println("各节点流式输出统计: " + nodeStreamCounts); + }) + .doOnError(error -> { + System.err.println("流式输出错误: " + error.getMessage()); + error.printStackTrace(); + }) + .blockLast(); // 阻塞等待流完成 + } + + /** + * 示例 2: 单个节点的流式输出 + * + * 演示单个节点使用 Flux 产生流式输出 + */ + public static void singleNodeStreaming() throws GraphStateException { + // 定义状态策略 + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("messages", new AppendStrategy()); + keyStrategyMap.put("stream_result", new AppendStrategy()); + return keyStrategyMap; + }; + + // 单个流式节点 + AsyncNodeAction streamingNode = state -> { + // 创建流式数据 + Flux dataStream = Flux.just("块1", "块2", "块3", "块4", "块5") + .delayElements(Duration.ofMillis(100)); + + + return CompletableFuture.completedFuture(Map.of("stream_output", dataStream)); + }; + + // 构建图 + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("streaming_node", streamingNode) + .addEdge(START, "streaming_node") + .addEdge("streaming_node", END); + + // 编译图 + CompiledGraph graph = stateGraph.compile( + CompileConfig.builder() + .build() + ); + + // 创建配置 + RunnableConfig config = RunnableConfig.builder() + .threadId("single_streaming_thread") + .build(); + + System.out.println("开始单节点流式输出...\n"); + + AtomicInteger streamCount = new AtomicInteger(0); + String[] lastNodeId = new String[1]; + + // 执行流式图 + graph.stream(Map.of("input", "test"), config) + .filter(output -> output instanceof StreamingOutput) + .map(output -> (StreamingOutput) output) + .doOnNext(streamingOutput -> { + streamCount.incrementAndGet(); + lastNodeId[0] = streamingOutput.node(); + System.out.println("[流式输出] 节点: " + streamingOutput.node() + + ", 内容: " + streamingOutput.chunk()); + }) + .doOnComplete(() -> { + System.out.println("\n=== 单节点流式输出完成 ==="); + System.out.println("节点 ID: " + lastNodeId[0]); + System.out.println("流式块数: " + streamCount.get()); + }) + .doOnError(error -> { + System.err.println("流式输出错误: " + error.getMessage()); + }) + .blockLast(); + } + + public static void main(String[] args) { + System.out.println("=== 并行流式输出示例 ===\n"); + + try { + // 示例 1: 并行节点流式输出 +// System.out.println("示例 1: 并行节点流式输出(保持节点 ID)"); +// parallelStreamingWithNodeIdPreservation(); +// System.out.println(); + + // 示例 2: 单个节点流式输出 + System.out.println("示例 2: 单个节点流式输出"); + singleNodeStreaming(); + System.out.println(); + + System.out.println("所有示例执行完成"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/PersistenceExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/PersistenceExample.java new file mode 100644 index 00000000..cf201708 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/PersistenceExample.java @@ -0,0 +1,251 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.StateSnapshot; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import org.springframework.ai.chat.client.ChatClient; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 为图添加持久化(记忆)示例 + * 演示如何使用 Checkpointer 为 StateGraph 提供持久化记忆 + */ +public class PersistenceExample { + + /** + * 不使用 Checkpointer 的示例 + */ + public static CompiledGraph createGraphWithoutCheckpointer(ChatClient.Builder chatClientBuilder) throws GraphStateException { + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("messages", new AppendStrategy()); + strategies.put("user_name", new ReplaceStrategy()); + strategies.put("context", new ReplaceStrategy()); + return strategies; + }; + + StateGraph workflow = new StateGraph(keyStrategyFactory) + .addNode("agent", node_async(state -> { + List messages = (List) state.value("messages").orElse(List.of()); + String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1); + return Map.of("messages", "Response to: " + lastMessage); + })) + .addEdge(START, "agent") + .addEdge("agent", END); + + return workflow.compile(); + } + + /** + * 添加持久化(记忆) + */ + public static CompiledGraph createGraphWithCheckpointer(ChatClient.Builder chatClientBuilder) throws GraphStateException { + // 创建 Checkpointer + var checkpointer = new MemorySaver(); + + // 配置持久化 + var compileConfig = CompileConfig.builder() + .saverConfig(SaverConfig.builder() + .register(checkpointer) + .build()) + .build(); + + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("messages", new AppendStrategy()); + return strategies; + }; + + StateGraph workflow = new StateGraph(keyStrategyFactory) + .addNode("agent", node_async(state -> { + List messages = (List) state.value("messages").orElse(List.of()); + String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1); + return Map.of("messages", "Response to: " + lastMessage); + })) + .addEdge(START, "agent") + .addEdge("agent", END); + + // 编译带持久化的 Graph + return workflow.compile(compileConfig); + } + + /** + * 测试带持久化的 Graph + */ + public static void testGraphWithPersistence(CompiledGraph persistentGraph) { + // 创建运行配置(使用 threadId 标识会话) + var config = RunnableConfig.builder() + .threadId("user-alice-session") + .build(); + + // 第一次调用 - 介绍自己 + System.out.println("=== First call with persistence - Introduction ==="); + var result1 = persistentGraph.invoke( + Map.of("messages", List.of("Hi, I'm Alice, nice to meet you")), + config + ); + + List messages1 = (List) result1.get().data().get("messages"); + System.out.println("Response: " + messages1.get(messages1.size() - 1)); + + // 第二次调用 - 询问名字(有持久化,可以记住) + System.out.println("=== Second call with persistence - Ask name ==="); + var result2 = persistentGraph.invoke( + Map.of("messages", List.of("What's my name?")), + config + ); + + List messages2 = (List) result2.get().data().get("messages"); + System.out.println("Response: " + messages2.get(messages2.size() - 1)); + } + + /** + * 多会话隔离 + */ + public static void multiSessionIsolation(CompiledGraph persistentGraph) { + // Alice 的会话 + var aliceConfig = RunnableConfig.builder() + .threadId("user-alice") + .build(); + + persistentGraph.invoke(Map.of("messages", List.of("Hi, I'm Alice")), aliceConfig); + + // Bob 的会话 + var bobConfig = RunnableConfig.builder() + .threadId("user-bob") + .build(); + + persistentGraph.invoke(Map.of("messages", List.of("Hi, I'm Bob")), bobConfig); + + // Alice 询问名字 - 能记住 + var aliceResult = persistentGraph.invoke( + Map.of("messages", List.of("What's my name?")), + aliceConfig + ); + System.out.println("Alice: " + aliceResult.get().data().get("messages")); + + // Bob 询问名字 - 也能记住 + var bobResult = persistentGraph.invoke( + Map.of("messages", List.of("What's my name?")), + bobConfig + ); + System.out.println("Bob: " + bobResult.get().data().get("messages")); + } + + /** + * 获取当前状态 + */ + public static void getCurrentState(CompiledGraph graph) { + RunnableConfig config = RunnableConfig.builder() + .threadId("user-alice") + .build(); + + StateSnapshot snapshot = graph.getState(config); + + System.out.println("Current node: " + snapshot.node()); + System.out.println("Current state: " + snapshot.state()); + System.out.println("Next node: " + snapshot.next()); + System.out.println("Checkpoint ID: " + snapshot.config().checkPointId().orElse("N/A")); + } + + /** + * 获取状态历史 + */ + public static void getStateHistory(CompiledGraph graph) { + RunnableConfig config = RunnableConfig.builder() + .threadId("user-alice") + .build(); + + // 获取所有历史状态 + List history = (List) graph.getStateHistory(config); + + System.out.println("=== State History ==="); + for (int i = 0; i < history.size(); i++) { + StateSnapshot h = history.get(i); + System.out.println("Step " + i + ": node=" + h.node() + + ", messages count=" + ((List) h.state().data().get("messages")).size()); + } + } + + public static void main(String[] args) { + System.out.println("=== 持久化示例 ===\n"); + + try { + // 示例 1: 不使用 Checkpointer 的示例(需要 ChatClient) + System.out.println("示例 1: 不使用 Checkpointer 的示例"); + System.out.println("注意: 此示例需要 ChatClient,跳过执行"); + // CompiledGraph graphWithoutCheckpointer = createGraphWithoutCheckpointer(ChatClient.builder(...)); + System.out.println(); + + // 示例 2: 添加持久化(需要 ChatClient) + System.out.println("示例 2: 添加持久化"); + System.out.println("注意: 此示例需要 ChatClient,跳过执行"); + // CompiledGraph persistentGraph = createGraphWithCheckpointer(ChatClient.builder(...)); + System.out.println(); + + // 示例 3: 测试带持久化的 Graph(需要 CompiledGraph) + System.out.println("示例 3: 测试带持久化的 Graph"); + System.out.println("注意: 此示例需要 CompiledGraph,跳过执行"); + // testGraphWithPersistence(persistentGraph); + System.out.println(); + + // 示例 4: 多会话隔离(需要 CompiledGraph) + System.out.println("示例 4: 多会话隔离"); + System.out.println("注意: 此示例需要 CompiledGraph,跳过执行"); + // multiSessionIsolation(persistentGraph); + System.out.println(); + + // 示例 5: 获取当前状态(需要 CompiledGraph) + System.out.println("示例 5: 获取当前状态"); + System.out.println("注意: 此示例需要 CompiledGraph,跳过执行"); + // getCurrentState(persistentGraph); + System.out.println(); + + // 示例 6: 获取状态历史(需要 CompiledGraph) + System.out.println("示例 6: 获取状态历史"); + System.out.println("注意: 此示例需要 CompiledGraph,跳过执行"); + // getStateHistory(persistentGraph); + System.out.println(); + + System.out.println("所有示例执行完成"); + System.out.println("提示: 请配置 ChatClient 后运行完整示例"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/PlantUmlExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/PlantUmlExample.java new file mode 100644 index 00000000..da0d2f0a --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/PlantUmlExample.java @@ -0,0 +1,96 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.GraphRepresentation; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import java.util.HashMap; +import java.util.Map; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * PlantUML 图表可视化示例 + * 演示如何使用 PlantUML 可视化 Spring AI Alibaba Graph 工作流结构 + */ +public class PlantUmlExample { + + /** + * 从 Graph 生成 PlantUML + */ + public static void generatePlantUmlFromGraph() throws GraphStateException { + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("result", new ReplaceStrategy()); + return strategies; + }; + + // 构建一个简单的 Graph + StateGraph graph = new StateGraph(keyStrategyFactory) + .addNode("step1", node_async(state -> Map.of("result", "Step 1"))) + .addNode("step2", node_async(state -> Map.of("result", "Step 2"))) + .addNode("step3", node_async(state -> Map.of("result", "Step 3"))) + .addEdge(START, "step1") + .addEdge("step1", "step2") + .addEdge("step2", "step3") + .addEdge("step3", END); + + CompiledGraph compiledGraph = graph.compile(); + + // 生成 PlantUML 表示 + GraphRepresentation representation = compiledGraph.getGraph( + GraphRepresentation.Type.PLANTUML, + "My Workflow" + ); + + // 显示 PlantUML 代码 + System.out.println("PlantUML representation:"); + System.out.println(representation.content()); + } + + /** + * 简单 PlantUML 代码示例 + */ + public static void simplePlantUmlExample() { + String code = """ + @startuml + title Spring AI Alibaba Graph + START --> NodeA + NodeA --> NodeB + NodeB --> END + @enduml + """; + + System.out.println("Simple PlantUML code:"); + System.out.println(code); + } + + public static void main(String[] args) throws GraphStateException { + System.out.println("=== PlantUML 图表可视化示例 ==="); + simplePlantUmlExample(); + generatePlantUmlFromGraph(); + System.out.println("所有示例执行完成"); + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphAsCompiledGraphExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphAsCompiledGraphExample.java new file mode 100644 index 00000000..9f4c09a5 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphAsCompiledGraphExample.java @@ -0,0 +1,172 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import java.util.HashMap; +import java.util.Map; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 子图作为 CompiledGraph 示例 + * 演示如何使用已编译的 Graph 作为子图 + */ +public class SubgraphAsCompiledGraphExample { + + /** + * 创建并编译子图 + */ + public static CompiledGraph createAndCompileSubGraph() throws GraphStateException { + KeyStrategyFactory subKeyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("input", new ReplaceStrategy()); + strategies.put("output", new ReplaceStrategy()); + return strategies; + }; + + // 定义并编译子图 + StateGraph subGraphDef = new StateGraph(subKeyFactory) + .addNode("process", node_async(state -> { + String input = (String) state.value("input").orElse(""); + String output = "Processed: " + input.toUpperCase(); + return Map.of("output", output); + })) + .addEdge(START, "process") + .addEdge("process", END); + + // 编译子图 + return subGraphDef.compile(); + } + + /** + * 在父图中使用 + */ + public static CompiledGraph useInParentGraph(CompiledGraph compiledSubGraph) throws GraphStateException { + KeyStrategyFactory parentKeyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("data", new ReplaceStrategy()); + strategies.put("result", new ReplaceStrategy()); + return strategies; + }; + + StateGraph parentGraph = new StateGraph(parentKeyFactory) + .addNode("prepare", node_async(state -> + Map.of("data", "hello world"))) + .addNode("subgraph", compiledSubGraph) + .addNode("finalize", node_async(state -> { + String result = (String) state.value("result").orElse(""); + return Map.of("final", "Done: " + result); + })) + .addEdge(START, "prepare") + .addEdge("prepare", "subgraph") + .addEdge("subgraph", "finalize") + .addEdge("finalize", END); + + return parentGraph.compile(); + } + + /** + * 多个子图复用 + */ + public static CompiledGraph reuseMultipleSubGraphs(CompiledGraph dataProcessor) throws GraphStateException { + KeyStrategyFactory keyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("data", new ReplaceStrategy()); + strategies.put("result", new ReplaceStrategy()); + return strategies; + }; + + // 在多个节点中复用 + StateGraph mainGraph = new StateGraph(keyFactory) + .addNode("process1", dataProcessor) + .addNode("process2", dataProcessor) + .addNode("process3", dataProcessor) + .addEdge(START, "process1") + .addEdge("process1", "process2") + .addEdge("process2", "process3") + .addEdge("process3", END); + + return mainGraph.compile(); + } + + public static void main(String[] args) { + System.out.println("=== 子图作为 CompiledGraph 示例 ===\n"); + + try { + // 示例 1: 创建并编译子图 + System.out.println("示例 1: 创建并编译子图"); + CompiledGraph subGraph = createAndCompileSubGraph(); + System.out.println("子图创建完成"); + System.out.println(); + + // 示例 2: 在父图中使用 + System.out.println("示例 2: 在父图中使用"); + CompiledGraph parentGraph = useInParentGraph(subGraph); + System.out.println("父图创建完成"); + System.out.println(); + + // 示例 3: 多个子图复用 + System.out.println("示例 3: 多个子图复用"); + CompiledGraph reusedGraph = reuseMultipleSubGraphs(subGraph); + System.out.println("多子图复用示例创建完成"); + System.out.println(); + + System.out.println("所有示例执行完成"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 在节点中使用 CompiledGraph + */ + public static class CompiledSubGraphNode implements NodeAction { + + private final CompiledGraph compiledGraph; + + public CompiledSubGraphNode(CompiledGraph compiledGraph) { + this.compiledGraph = compiledGraph; + } + + @Override + public Map apply(OverAllState state) { + // 从父状态提取输入 + String input = (String) state.value("data").orElse(""); + + // 执行编译好的子图 + Map subInput = Map.of("input", input); + OverAllState subResult = compiledGraph.invoke(subInput).orElseThrow(); + + // 提取子图输出 + String output = (String) subResult.value("output").orElse(""); + return Map.of("result", output); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphAsNodeActionExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphAsNodeActionExample.java new file mode 100644 index 00000000..9a178145 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphAsNodeActionExample.java @@ -0,0 +1,160 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 子图作为节点操作示例 + * 演示如何将子图作为 NodeAction 在父图中使用 + */ +public class SubgraphAsNodeActionExample { + + /** + * 定义子图 + */ + public static CompiledGraph createSubGraph(KeyStrategyFactory keyStrategyFactory) throws GraphStateException { + StateGraph subGraph = new StateGraph(keyStrategyFactory) + .addNode("substep1", node_async(state -> { + String input = (String) state.value("input").orElse(""); + return Map.of("result", "SubStep1:" + input); + })) + .addNode("substep2", node_async(state -> { + String prev = (String) state.value("result").orElse(""); + return Map.of("result", prev + "->SubStep2"); + })) + .addEdge(START, "substep1") + .addEdge("substep1", "substep2") + .addEdge("substep2", END); + + return subGraph.compile(); + } + + /** + * 在父图中使用 + */ + public static CompiledGraph useInParentGraph(KeyStrategyFactory keyStrategyFactory, CompiledGraph subGraph) throws GraphStateException { + SubGraphNode subGraphNode = new SubGraphNode(subGraph); + + StateGraph parentGraph = new StateGraph(keyStrategyFactory) + .addNode("prepare", node_async(state -> { + return Map.of("data", "Input Data"); + })) + .addNode("process", node_async(subGraphNode)) // 使用子图作为节点 + .addNode("finalize", node_async(state -> { + String processed = (String) state.value("processed").orElse(""); + return Map.of("final", "Final:" + processed); + })) + .addEdge(START, "prepare") + .addEdge("prepare", "process") + .addEdge("process", "finalize") + .addEdge("finalize", END); + + return parentGraph.compile(); + } + + public static void main(String[] args) throws GraphStateException { + System.out.println("=== 子图作为节点操作示例 ==="); + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("data", new ReplaceStrategy()); + strategies.put("input", new ReplaceStrategy()); + strategies.put("result", new ReplaceStrategy()); + strategies.put("processed", new ReplaceStrategy()); + strategies.put("final", new ReplaceStrategy()); + return strategies; + }; + + CompiledGraph subGraph = createSubGraph(keyStrategyFactory); + CompiledGraph parentGraph = useInParentGraph(keyStrategyFactory, subGraph); + System.out.println("所有示例执行完成"); + } + + /** + * 将子图包装为 NodeAction + */ + public static class SubGraphNode implements NodeAction { + + private final CompiledGraph subGraph; + + public SubGraphNode(CompiledGraph subGraph) { + this.subGraph = subGraph; + } + + @Override + public Map apply(OverAllState state) { + // 从父状态提取子图需要的数据 + String input = (String) state.value("data").orElse(""); + + // 执行子图 + Map subInput = Map.of("input", input); + Optional subResult = subGraph.invoke(subInput); + + // 返回结果给父图 + String result = (String) subResult.get().value("result").orElse(""); + return Map.of("processed", result); + } + } + + /** + * 可配置的子图节点 + */ + public static class ConfigurableSubGraphNode implements NodeAction { + + private final CompiledGraph subGraph; + private final String inputKey; + private final String outputKey; + + public ConfigurableSubGraphNode( + CompiledGraph subGraph, + String inputKey, + String outputKey + ) { + this.subGraph = subGraph; + this.inputKey = inputKey; + this.outputKey = outputKey; + } + + @Override + public Map apply(OverAllState state) { + // 从父状态读取指定键的数据 + Object input = state.value(inputKey).orElse(null); + + // 执行子图 + OverAllState subResult = subGraph.invoke(Map.of("input", input)).orElseThrow(); + + // 将结果写入指定键 + Object output = subResult.value("result").orElse(null); + return Map.of(outputKey, output); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphAsStateGraphExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphAsStateGraphExample.java new file mode 100644 index 00000000..6701791d --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphAsStateGraphExample.java @@ -0,0 +1,175 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.action.NodeAction; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 子图作为 StateGraph 示例 + * 演示如何将 StateGraph 组合使用 + */ +public class SubgraphAsStateGraphExample { + + /** + * 定义子图 + */ + public static StateGraph createProcessingSubGraph() throws GraphStateException { + KeyStrategyFactory keyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("input", new ReplaceStrategy()); + strategies.put("output", new ReplaceStrategy()); + strategies.put("valid", new ReplaceStrategy()); + return strategies; + }; + + return new StateGraph(keyFactory) + .addNode("validate", node_async(state -> { + String input = (String) state.value("input").orElse(""); + boolean isValid = input != null && !input.isEmpty(); + return Map.of("valid", isValid); + })) + .addNode("transform", node_async(state -> { + String input = (String) state.value("input").orElse(""); + String transformed = input.toUpperCase(); + return Map.of("output", transformed); + })) + .addEdge(START, "validate") + .addConditionalEdges("validate", + edge_async(state -> { + Boolean valid = (Boolean) state.value("valid").orElse(false); + return valid ? "valid" : "invalid"; + }), + Map.of( + "valid", "transform", + "invalid", END + )) + .addEdge("transform", END); + } + + /** + * 在父图中集成子图 - 方式 1: 直接嵌入 + */ + public static StateGraph createParentGraphWithDirectEmbedding() throws GraphStateException { + KeyStrategyFactory keyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("data", new ReplaceStrategy()); + strategies.put("output", new ReplaceStrategy()); + strategies.put("result", new ReplaceStrategy()); + return strategies; + }; + + StateGraph subGraph = createProcessingSubGraph(); + + return new StateGraph(keyFactory) + .addNode("prepare", node_async(state -> { + return Map.of("data", "hello world"); + })) + // 将子图作为节点添加 + .addNode("process", subGraph) + .addNode("finalize", node_async(state -> { + String output = (String) state.value("output").orElse(""); + return Map.of("result", "Final: " + output); + })) + .addEdge(START, "prepare") + .addEdge("prepare", "process") + .addEdge("process", "finalize") + .addEdge("finalize", END); + } + + /** + * 在父图中集成子图 - 方式 2: 使用编译后的子图 + */ + public static StateGraph createParentGraphWithCompiledSubGraph() throws GraphStateException { + KeyStrategyFactory keyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("data", new ReplaceStrategy()); + strategies.put("output", new ReplaceStrategy()); + strategies.put("processed", new ReplaceStrategy()); + return strategies; + }; + + // 先编译子图 + CompiledGraph compiledSubGraph = createProcessingSubGraph().compile(); + + // 在父图中使用 + return new StateGraph(keyFactory) + .addNode("prepare", node_async(state -> { + return Map.of("data", "input"); + })) + .addNode("process", node_async(state -> { + // 手动调用子图 + Map subInput = Map.of( + "input", state.value("data").orElse("") + ); + OverAllState subResult = compiledSubGraph.invoke(subInput).orElseThrow(); + return Map.of("processed", subResult.value("output").orElse("")); + })) + .addEdge(START, "prepare") + .addEdge("prepare", "process") + .addEdge("process", END); + } + + public static void main(String[] args) throws GraphStateException { + System.out.println("=== 子图作为 StateGraph 示例 ==="); + StateGraph parentGraph1 = createParentGraphWithDirectEmbedding(); + StateGraph parentGraph2 = createParentGraphWithCompiledSubGraph(); + System.out.println("所有示例执行完成"); + } + + /** + * 状态隔离示例 + */ + public static class IsolatedSubGraphNode implements NodeAction { + private final CompiledGraph subGraph; + + public IsolatedSubGraphNode(StateGraph subGraphDef) throws GraphStateException { + this.subGraph = subGraphDef.compile(); + } + + @Override + public Map apply(OverAllState parentState) { + // 提取父状态数据 + String input = (String) parentState.value("input").orElse(""); + + // 创建子图独立状态 + Map subState = Map.of("subInput", input); + + // 执行子图 + Optional subResult = subGraph.invoke(subState); + + // 将子图结果映射回父状态 + String output = (String) subResult.get().value("subOutput").orElse(""); + return Map.of("output", output); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphExample.java new file mode 100644 index 00000000..335960ad --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/SubgraphExample.java @@ -0,0 +1,242 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.GraphRepresentation; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.NodeOutput; +import com.alibaba.cloud.ai.graph.OverAllState; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Flux; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 子图示例 + * 演示如何在 Spring AI Alibaba Graph 中使用子图 + */ +public class SubgraphExample { + + /** + * 示例 1: 添加编译的子图作为节点 + */ + public static void addCompiledSubgraphAsNode() throws GraphStateException { + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("sharedData", new ReplaceStrategy()); + keyStrategyMap.put("results", new AppendStrategy()); + return keyStrategyMap; + }; + + // 创建子图 + var childNode1 = node_async(state -> { + String data = (String) state.value("sharedData").orElse(""); + return Map.of("results", List.of("Child processed: " + data)); + }); + + StateGraph childGraph = new StateGraph(keyStrategyFactory) + .addNode("child_node1", childNode1) + .addEdge(START, "child_node1") + .addEdge("child_node1", END); + + CompiledGraph compiledChild = childGraph.compile(); + + // 创建父图 + var parentNode1 = node_async(state -> { + return Map.of("sharedData", "Parent data"); + }); + + StateGraph parentGraph = new StateGraph(keyStrategyFactory) + .addNode("parent_node1", parentNode1) + .addNode("call_child", node_async(state -> { + return compiledChild.invoke(state.data(), + RunnableConfig.builder().build()) + .orElseThrow() + .data(); + })) + .addEdge(START, "parent_node1") + .addEdge("parent_node1", "call_child") + .addEdge("call_child", END); + + CompiledGraph compiledParent = parentGraph.compile(); + System.out.println("Compiled subgraph as node example created"); + } + + /** + * 示例 2: 在节点操作中调用子图 + */ + public static void callSubgraphInNodeAction() throws GraphStateException { + // 父图状态 + KeyStrategyFactory parentKeyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("parentData", new ReplaceStrategy()); + keyStrategyMap.put("processedResult", new ReplaceStrategy()); + return keyStrategyMap; + }; + + // 子图状态(完全不同) + KeyStrategyFactory childKeyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("childInput", new ReplaceStrategy()); + keyStrategyMap.put("childOutput", new ReplaceStrategy()); + return keyStrategyMap; + }; + + // 创建子图 + var childProcessor = node_async(state -> { + String input = (String) state.value("childInput").orElse(""); + String output = "Processed: " + input; + return Map.of("childOutput", output); + }); + + StateGraph childGraph = new StateGraph(childKeyStrategyFactory) + .addNode("processor", childProcessor) + .addEdge(START, "processor") + .addEdge("processor", END); + + CompiledGraph compiledChild = childGraph.compile(); + + // 父图中的转换节点 + var transformAndCallChild = node_async(state -> { + // 1. 从父状态提取数据 + String parentData = (String) state.value("parentData").orElse(""); + + // 2. 转换为子图输入 + Map childInput = Map.of("childInput", parentData); + + // 3. 调用子图 + OverAllState childResult = compiledChild.invoke( + childInput, + RunnableConfig.builder().build() + ).orElseThrow(); + + // 4. 转换子图输出回父状态 + String childOutput = (String) childResult.value("childOutput").orElse(""); + return Map.of("processedResult", childOutput); + }); + + // 创建父图 + StateGraph parentGraph = new StateGraph(parentKeyStrategyFactory) + .addNode("call_child_with_transform", transformAndCallChild) + .addEdge(START, "call_child_with_transform") + .addEdge("call_child_with_transform", END); + + CompiledGraph compiledParent = parentGraph.compile(); + System.out.println("Call subgraph in node action example created"); + } + + /** + * 示例 3: 可视化子图 + */ + public static void visualizeSubgraph() throws GraphStateException { + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("data", new ReplaceStrategy()); + return keyStrategyMap; + }; + + StateGraph stateGraph = new StateGraph(keyStrategyFactory) + .addNode("node1", node_async(state -> Map.of("data", "processed"))) + .addNode("node2", node_async(state -> Map.of("data", "finalized"))) + .addEdge(START, "node1") + .addEdge("node1", "node2") + .addEdge("node2", END); + + // 获取 PlantUML 表示 + GraphRepresentation representation = stateGraph.getGraph( + GraphRepresentation.Type.PLANTUML, + "My Graph" + ); + + System.out.println("PlantUML representation:"); + System.out.println(representation.content()); + } + + /** + * 示例 4: 流式处理子图 + */ + public static void streamSubgraph() throws GraphStateException { + KeyStrategyFactory keyStrategyFactory = () -> { + Map keyStrategyMap = new HashMap<>(); + keyStrategyMap.put("data", new ReplaceStrategy()); + return keyStrategyMap; + }; + + StateGraph childGraph = new StateGraph(keyStrategyFactory) + .addNode("process", node_async(state -> Map.of("data", "processed"))) + .addEdge(START, "process") + .addEdge("process", END); + + CompiledGraph compiledChild = childGraph.compile(); + + // 执行父图并获取流式输出 + Flux stream = compiledChild.stream( + Map.of("data", "input"), + RunnableConfig.builder().threadId("parent-thread").build() + ); + + // 处理流式输出 + stream.subscribe(output -> { + System.out.println("Subgraph output: " + output); + }); + } + + public static void main(String[] args) { + System.out.println("=== 子图示例 ===\n"); + + try { + // 示例 1: 添加编译的子图作为节点 + System.out.println("示例 1: 添加编译的子图作为节点"); + addCompiledSubgraphAsNode(); + System.out.println(); + + // 示例 2: 在节点操作中调用子图 + System.out.println("示例 2: 在节点操作中调用子图"); + callSubgraphInNodeAction(); + System.out.println(); + + // 示例 3: 可视化子图 + System.out.println("示例 3: 可视化子图"); + visualizeSubgraph(); + System.out.println(); + + // 示例 4: 流式处理子图 + System.out.println("示例 4: 流式处理子图"); + streamSubgraph(); + System.out.println(); + + System.out.println("所有示例执行完成"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/TimeTravelExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/TimeTravelExample.java new file mode 100644 index 00000000..766f6756 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/TimeTravelExample.java @@ -0,0 +1,278 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.StateSnapshot; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 时光旅行示例 + * 演示如何查看和恢复 Graph 执行的历史状态 + */ +public class TimeTravelExample { + + /** + * 配置 Checkpoint + */ + public static CompiledGraph configureCheckpoint(StateGraph stateGraph) throws GraphStateException { + // 创建 Checkpointer + var checkpointer = new MemorySaver(); + + // 配置持久化 + var compileConfig = CompileConfig.builder() + .saverConfig(SaverConfig.builder() + .register(checkpointer) + .build()) + .build(); + + return stateGraph.compile(compileConfig); + } + + /** + * 执行 Graph 并生成历史 + */ + public static void executeGraphAndGenerateHistory(CompiledGraph graph) { + // 配置线程 ID + var config = RunnableConfig.builder() + .threadId("conversation-1") + .build(); + + // 执行 Graph + Map input = Map.of("query", "Hello"); + graph.invoke(input, config); + + // 再次执行 + graph.invoke(Map.of("query", "Follow-up question"), config); + } + + /** + * 查看状态历史 + */ + public static void viewStateHistory(CompiledGraph graph) { + var config = RunnableConfig.builder() + .threadId("conversation-1") + .build(); + + // 获取所有历史状态 + List history = (List) graph.getStateHistory(config); + + System.out.println("State history:"); + for (int i = 0; i < history.size(); i++) { + StateSnapshot snapshot = history.get(i); + System.out.printf("Step %d: %s\n", i, snapshot.state()); + System.out.printf(" Checkpoint ID: %s\n", snapshot.config().checkPointId().orElse("N/A")); + System.out.printf(" Node: %s\n", snapshot.node()); + } + } + + /** + * 回溯到历史状态 + */ + public static void travelBackToHistory(CompiledGraph graph) { + var config = RunnableConfig.builder() + .threadId("conversation-1") + .build(); + + // 获取所有历史状态 + List history = (List) graph.getStateHistory(config); + + // 获取特定的历史状态 + StateSnapshot historicalSnapshot = history.get(1); + + // 使用历史状态的 checkpoint ID 创建新配置 + var historicalConfig = RunnableConfig.builder() + .threadId("conversation-1") + .checkPointId(historicalSnapshot.config().checkPointId().orElse(null)) + .build(); + + // 从历史状态继续执行 + graph.invoke( + Map.of("query", "New question from historical state"), + historicalConfig + ); + } + + /** + * 分支创建 + */ + public static void createBranch(CompiledGraph graph) { + var config = RunnableConfig.builder() + .threadId("conversation-1") + .build(); + + // 获取所有历史状态 + List history = (List) graph.getStateHistory(config); + + // 获取特定的历史状态 + StateSnapshot historicalSnapshot = history.get(1); + + // 从历史状态创建新分支 + var branchConfig = RunnableConfig.builder() + .threadId("conversation-1-branch") // 新的线程 ID + .checkPointId(historicalSnapshot.config().checkPointId().orElse(null)) + .build(); + + // 在新分支上执行 + graph.invoke( + Map.of("query", "Alternative path"), + branchConfig + ); + } + + /** + * 完整示例 + */ + public static void completeExample() throws GraphStateException { + // 构建 Graph + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("messages", new AppendStrategy()); + strategies.put("step", new ReplaceStrategy()); + return strategies; + }; + + StateGraph builder = new StateGraph(keyStrategyFactory) + .addNode("step1", node_async(state -> + Map.of("messages", "Step 1", "step", 1))) + .addNode("step2", node_async(state -> + Map.of("messages", "Step 2", "step", 2))) + .addNode("step3", node_async(state -> + Map.of("messages", "Step 3", "step", 3))) + .addEdge(START, "step1") + .addEdge("step1", "step2") + .addEdge("step2", "step3") + .addEdge("step3", END); + + // 配置持久化 + var checkpointer = new MemorySaver(); + var compileConfig = CompileConfig.builder() + .saverConfig(SaverConfig.builder() + .register(checkpointer) + .build()) + .build(); + + CompiledGraph graph = builder.compile(compileConfig); + + // 执行 + var config = RunnableConfig.builder() + .threadId("demo") + .build(); + + graph.invoke(Map.of(), config); + + // 查看历史 + List history = (List) graph.getStateHistory(config); + history.forEach(snapshot -> { + System.out.println("State: " + snapshot.state()); + System.out.println("Node: " + snapshot.node()); + System.out.println("---"); + }); + + // 回溯到 step1 + StateSnapshot step1Snapshot = history.stream() + .filter(s -> "step1".equals(s.node())) + .findFirst() + .orElseThrow(); + + var replayConfig = RunnableConfig.builder() + .threadId("demo") + .checkPointId(step1Snapshot.config().checkPointId().orElse(null)) + .build(); + + // 从 step1 重新执行 + graph.invoke(Map.of(), replayConfig); + } + + public static void main(String[] args) { + System.out.println("=== 时光旅行示例 ===\n"); + + try { + // 示例 1: 配置 Checkpoint + System.out.println("示例 1: 配置 Checkpoint"); + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("messages", new AppendStrategy()); + strategies.put("step", new ReplaceStrategy()); + return strategies; + }; + + StateGraph builder = new StateGraph(keyStrategyFactory) + .addNode("step1", node_async(state -> Map.of("messages", "Step 1", "step", 1))) + .addNode("step2", node_async(state -> Map.of("messages", "Step 2", "step", 2))) + .addNode("step3", node_async(state -> Map.of("messages", "Step 3", "step", 3))) + .addEdge(START, "step1") + .addEdge("step1", "step2") + .addEdge("step2", "step3") + .addEdge("step3", END); + + CompiledGraph graph = configureCheckpoint(builder); + System.out.println("Checkpoint 配置完成"); + System.out.println(); + + // 示例 2: 执行 Graph 并生成历史 + System.out.println("示例 2: 执行 Graph 并生成历史"); + executeGraphAndGenerateHistory(graph); + System.out.println(); + + // 示例 3: 查看状态历史 + System.out.println("示例 3: 查看状态历史"); + viewStateHistory(graph); + System.out.println(); + + // 示例 4: 回溯到历史状态 + System.out.println("示例 4: 回溯到历史状态"); + System.out.println("注意: 此示例需要有效的历史状态,跳过执行"); + // travelBackToHistory(graph); + System.out.println(); + + // 示例 5: 分支创建 + System.out.println("示例 5: 分支创建"); + System.out.println("注意: 此示例需要有效的历史状态,跳过执行"); + // createBranch(graph); + System.out.println(); + + // 示例 6: 完整示例 + System.out.println("示例 6: 完整示例"); + completeExample(); + System.out.println(); + + System.out.println("所有示例执行完成"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/TimeTravelRedisExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/TimeTravelRedisExample.java new file mode 100644 index 00000000..8ab13fd0 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/TimeTravelRedisExample.java @@ -0,0 +1,308 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.redis.RedisSaver; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.StateSnapshot; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * Redis 时光旅行示例 + * 演示如何使用 Redis 持久化查看和恢复 Graph 执行的历史状态 + */ +public class TimeTravelRedisExample { + + /** + * 配置 Checkpoint + */ + public static CompiledGraph configureCheckpoint(StateGraph stateGraph, RedissonClient redisson) throws GraphStateException { + // 创建 Checkpointer + var checkpointer = RedisSaver.builder().redisson(redisson).build(); + + // 配置持久化 + var compileConfig = CompileConfig.builder() + .saverConfig(SaverConfig.builder() + .register(checkpointer) + .build()) + .build(); + + return stateGraph.compile(compileConfig); + } + + /** + * 执行 Graph 并生成历史 + */ + public static void executeGraphAndGenerateHistory(CompiledGraph graph) { + // 配置线程 ID + var config = RunnableConfig.builder() + .threadId("conversation-redis-1") + .build(); + + // 执行 Graph + Map input = Map.of("query", "Hello"); + graph.invoke(input, config); + + // 再次执行 + graph.invoke(Map.of("query", "Follow-up question"), config); + } + + /** + * 查看状态历史 + */ + public static void viewStateHistory(CompiledGraph graph) { + var config = RunnableConfig.builder() + .threadId("conversation-redis-1") + .build(); + + // 获取所有历史状态 + List history = (List) graph.getStateHistory(config); + + System.out.println("State history:"); + for (int i = 0; i < history.size(); i++) { + StateSnapshot snapshot = history.get(i); + System.out.printf("Step %d: %s\n", i, snapshot.state()); + System.out.printf(" Checkpoint ID: %s\n", snapshot.config().checkPointId().orElse("N/A")); + System.out.printf(" Node: %s\n", snapshot.node()); + } + } + + /** + * 回溯到历史状态 + */ + public static void travelBackToHistory(CompiledGraph graph) { + var config = RunnableConfig.builder() + .threadId("conversation-redis-1") + .build(); + + // 获取所有历史状态 + List history = (List) graph.getStateHistory(config); + + if (history.size() < 2) { + System.out.println("历史记录不足,无法回溯"); + return; + } + + // 获取特定的历史状态 (例如第二个状态) + StateSnapshot historicalSnapshot = history.get(1); + + // 使用历史状态的 checkpoint ID 创建新配置 + var historicalConfig = RunnableConfig.builder() + .threadId("conversation-redis-1") + .checkPointId(historicalSnapshot.config().checkPointId().orElse(null)) + .build(); + + // 从历史状态继续执行 + graph.invoke( + Map.of("query", "New question from historical state"), + historicalConfig + ); + } + + /** + * 分支创建 + */ + public static void createBranch(CompiledGraph graph) { + var config = RunnableConfig.builder() + .threadId("conversation-redis-1") + .build(); + + // 获取所有历史状态 + List history = (List) graph.getStateHistory(config); + + if (history.size() < 2) { + System.out.println("历史记录不足,无法创建分支"); + return; + } + + // 获取特定的历史状态 + StateSnapshot historicalSnapshot = history.get(1); + + // 从历史状态创建新分支 + var branchConfig = RunnableConfig.builder() + .threadId("conversation-redis-1-branch") // 新的线程 ID + .checkPointId(historicalSnapshot.config().checkPointId().orElse(null)) + .build(); + + // 在新分支上执行 + graph.invoke( + Map.of("query", "Alternative path"), + branchConfig + ); + } + + /** + * 完整示例 + */ + public static void completeExample(RedissonClient redisson) throws Exception { + // 构建 Graph + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("messages", new AppendStrategy()); + strategies.put("step", new ReplaceStrategy()); + return strategies; + }; + + StateGraph builder = new StateGraph(keyStrategyFactory) + .addNode("step1", node_async(state -> + Map.of("messages", "Step 1", "step", 1))) + .addNode("step2", node_async(state -> + Map.of("messages", "Step 2", "step", 2))) + .addNode("step3", node_async(state -> + Map.of("messages", "Step 3", "step", 3))) + .addEdge(START, "step1") + .addEdge("step1", "step2") + .addEdge("step2", "step3") + .addEdge("step3", END); + + // 配置持久化 + var checkpointer = RedisSaver.builder().redisson(redisson).build(); + var compileConfig = CompileConfig.builder() + .saverConfig(SaverConfig.builder() + .register(checkpointer) + .build()) + .build(); + + CompiledGraph graph = builder.compile(compileConfig); + + // 执行 + var config = RunnableConfig.builder() + .threadId("demo-redis") + .build(); + + // 清理之前的状态(如果存在) + checkpointer.release(config); + + graph.invoke(Map.of(), config); + + // 查看历史 + List history = (List) graph.getStateHistory(config); + history.forEach(snapshot -> { + System.out.println("State: " + snapshot.state()); + System.out.println("Node: " + snapshot.node()); + System.out.println("---"); + }); + + // 回溯到 step1 + StateSnapshot step1Snapshot = history.stream() + .filter(s -> "step1".equals(s.node())) + .findFirst() + .orElseThrow(); + + var replayConfig = RunnableConfig.builder() + .threadId("demo-redis") + .checkPointId(step1Snapshot.config().checkPointId().orElse(null)) + .build(); + + // 从 step1 重新执行 + graph.invoke(Map.of(), replayConfig); + } + + public static void main(String[] args) { + System.out.println("=== Redis 时光旅行示例 ===\n"); + + // 初始化 Redis 客户端 + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://localhost:6379"); + RedissonClient redisson = Redisson.create(config); + + try { + // 示例 1: 配置 Checkpoint + System.out.println("示例 1: 配置 Checkpoint"); + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap strategies = new HashMap<>(); + strategies.put("messages", new AppendStrategy()); + strategies.put("step", new ReplaceStrategy()); + return strategies; + }; + + StateGraph builder = new StateGraph(keyStrategyFactory) + .addNode("step1", node_async(state -> Map.of("messages", "Step 1", "step", 1))) + .addNode("step2", node_async(state -> Map.of("messages", "Step 2", "step", 2))) + .addNode("step3", node_async(state -> Map.of("messages", "Step 3", "step", 3))) + .addEdge(START, "step1") + .addEdge("step1", "step2") + .addEdge("step2", "step3") + .addEdge("step3", END); + + CompiledGraph graph = configureCheckpoint(builder, redisson); + + // 清理旧数据 + RunnableConfig cleanConfig = RunnableConfig.builder().threadId("conversation-redis-1").build(); + RedisSaver.builder().redisson(redisson).build().release(cleanConfig); + RunnableConfig cleanBranchConfig = RunnableConfig.builder().threadId("conversation-redis-1-branch").build(); + RedisSaver.builder().redisson(redisson).build().release(cleanBranchConfig); + + System.out.println("Checkpoint 配置完成"); + System.out.println(); + + // 示例 2: 执行 Graph 并生成历史 + System.out.println("示例 2: 执行 Graph 并生成历史"); + executeGraphAndGenerateHistory(graph); + System.out.println(); + + // 示例 3: 查看状态历史 + System.out.println("示例 3: 查看状态历史"); + viewStateHistory(graph); + System.out.println(); + + // 示例 4: 回溯到历史状态 + System.out.println("示例 4: 回溯到历史状态"); + travelBackToHistory(graph); + System.out.println(); + + // 示例 5: 分支创建 + System.out.println("示例 5: 分支创建"); + createBranch(graph); + System.out.println(); + + // 示例 6: 完整示例 + System.out.println("示例 6: 完整示例"); + completeExample(redisson); + System.out.println(); + + System.out.println("所有示例执行完成"); + System.out.println("提示: 请配置 Redis 连接后运行完整示例"); + System.out.println("提示: 需要添加 Redisson 依赖: org.redisson:redisson"); + } + catch (Exception e) { + System.err.println("执行示例时出错: " + e.getMessage()); + e.printStackTrace(); + } finally { + redisson.shutdown(); + } + } +} diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/WaitUserInputExample.java b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/WaitUserInputExample.java new file mode 100644 index 00000000..c88ec015 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/java/com/alibaba/cloud/ai/examples/documentation/graph/examples/WaitUserInputExample.java @@ -0,0 +1,166 @@ +/* + * Copyright 2024-2025 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 com.alibaba.cloud.ai.examples.documentation.graph.examples; + +import com.alibaba.cloud.ai.graph.CompileConfig; +import com.alibaba.cloud.ai.graph.CompiledGraph; +import com.alibaba.cloud.ai.graph.KeyStrategy; +import com.alibaba.cloud.ai.graph.KeyStrategyFactory; +import com.alibaba.cloud.ai.graph.RunnableConfig; +import com.alibaba.cloud.ai.graph.StateGraph; +import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig; +import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver; +import com.alibaba.cloud.ai.graph.exception.GraphStateException; +import com.alibaba.cloud.ai.graph.state.strategy.AppendStrategy; +import com.alibaba.cloud.ai.graph.state.strategy.ReplaceStrategy; + +import java.util.HashMap; +import java.util.Map; + +import static com.alibaba.cloud.ai.graph.StateGraph.END; +import static com.alibaba.cloud.ai.graph.StateGraph.START; +import static com.alibaba.cloud.ai.graph.action.AsyncEdgeAction.edge_async; +import static com.alibaba.cloud.ai.graph.action.AsyncNodeAction.node_async; + +/** + * 等待用户输入示例 + * 演示如何实现等待用户输入的交互式工作流 + */ +public class WaitUserInputExample { + + /** + * 定义带中断的 Graph + */ + public static CompiledGraph createGraphWithInterrupt() throws GraphStateException { + // 定义节点 + var step1 = node_async(state -> { + return Map.of("messages", "Step 1"); + }); + + var humanFeedback = node_async(state -> { + return Map.of(); // 等待用户输入,不修改状态 + }); + + var step3 = node_async(state -> { + return Map.of("messages", "Step 3"); + }); + + // 定义条件边 + var evalHumanFeedback = edge_async(state -> { + var feedback = (String) state.value("human_feedback").orElse("unknown"); + return (feedback.equals("next") || feedback.equals("back")) ? feedback : "unknown"; + }); + + // 配置 KeyStrategyFactory + KeyStrategyFactory keyStrategyFactory = () -> { + HashMap keyStrategyHashMap = new HashMap<>(); + keyStrategyHashMap.put("messages", new AppendStrategy()); + keyStrategyHashMap.put("human_feedback", new ReplaceStrategy()); + return keyStrategyHashMap; + }; + + // 构建 Graph + StateGraph builder = new StateGraph(keyStrategyFactory) + .addNode("step_1", step1) + .addNode("human_feedback", humanFeedback) + .addNode("step_3", step3) + .addEdge(START, "step_1") + .addEdge("step_1", "human_feedback") + .addConditionalEdges("human_feedback", evalHumanFeedback, + Map.of("back", "step_1", "next", "step_3", "unknown", "human_feedback")) + .addEdge("step_3", END); + + // 配置内存保存器和中断点 + var saver = new MemorySaver(); + + var compileConfig = CompileConfig.builder() + .saverConfig(SaverConfig.builder() + .register(saver) + .build()) + .interruptBefore("human_feedback") // 在 human_feedback 节点前中断 + .build(); + + return builder.compile(compileConfig); + } + + /** + * 执行 Graph 直到中断 + */ + public static void executeUntilInterrupt(CompiledGraph graph) { + // 初始输入 + Map initialInput = Map.of("messages", "Step 0"); + + // 配置线程 ID + var invokeConfig = RunnableConfig.builder() + .threadId("Thread1") + .build(); + + // 运行 Graph 直到第一个中断点 + graph.stream(initialInput, invokeConfig) + .doOnNext(event -> System.out.println(event)) + .doOnError(error -> System.err.println("流错误: " + error.getMessage())) + .doOnComplete(() -> System.out.println("流完成")) + .blockLast(); + } + + /** + * 等待用户输入并更新状态 + */ + public static void waitUserInputAndUpdateState(CompiledGraph graph) throws Exception { + var invokeConfig = RunnableConfig.builder() + .threadId("Thread1") + .build(); + + // 检查当前状态 + System.out.printf("--State before update--\n%s\n", graph.getState(invokeConfig)); + + // 模拟用户输入 + var userInput = "back"; // "back" 表示返回上一个节点 + System.out.printf("\n--User Input--\n用户选择: '%s'\n\n", userInput); + + // 更新状态(模拟 human_feedback 节点的输出) + var updateConfig = graph.updateState(invokeConfig, Map.of("human_feedback", userInput), null); + + // 检查更新后的状态 + System.out.printf("--State after update--\n%s\n", graph.getState(invokeConfig)); + } + + /** + * 继续执行 Graph + */ + public static void continueExecution(CompiledGraph graph) { + var invokeConfig = RunnableConfig.builder() + .threadId("Thread1") + .build(); + + // 继续执行 Graph + graph.stream(null, invokeConfig) + .doOnNext(event -> System.out.println(event)) + .doOnError(error -> System.err.println("流错误: " + error.getMessage())) + .doOnComplete(() -> System.out.println("流完成")) + .blockLast(); + } + + public static void main(String[] args) throws Exception { + System.out.println("=== 等待用户输入示例 ==="); + CompiledGraph graph = createGraphWithInterrupt(); + executeUntilInterrupt(graph); + waitUserInputAndUpdateState(graph); + continueExecution(graph); + System.out.println("所有示例执行完成"); + } +} + diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/resources/application.yml b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/resources/application.yml new file mode 100644 index 00000000..d295a990 --- /dev/null +++ b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/resources/application.yml @@ -0,0 +1,42 @@ +server: + port: 8080 + +spring: + application: + name: documentation-examples + ai: + # DashScope 配置(阿里云百炼) + dashscope: + api-key: ${DASHSCOPE_API_KEY:your-api-key} + chat: + options: + model: ${DASHSCOPE_MODEL:qwen-plus} + # A2A Nacos 配置 + alibaba: + a2a: + nacos: + server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848} + username: ${NACOS_USERNAME:nacos} + password: ${NACOS_PASSWORD:nacos} + discovery: + enabled: true # 启用 A2A 服务发现,用于发现远程智能体 + registry: + enabled: true # 启用 Nacos Registry,将本地 Agent 注册到 Nacos + server: + version: 1.0.0 + card: + name: data_analysis_agent + description: 专门用于数据分析和统计计算的本地智能体 + provider: + name: Spring AI Alibaba Documentation + organization: Spring AI Alibaba + url: https://sca.aliyun.com/ai/ + contact: + email: dev@alibabacloud.com + +# 日志配置 +logging: + level: + root: INFO + com.alibaba.cloud.ai: DEBUG + org.springframework.ai: DEBUG diff --git a/project/spingai/spring-ai-alibaba-examples/documentation/src/main/resources/images/photo.jpg b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/resources/images/photo.jpg new file mode 100644 index 00000000..504929f7 Binary files /dev/null and b/project/spingai/spring-ai-alibaba-examples/documentation/src/main/resources/images/photo.jpg differ diff --git a/project/spring-boot-web-01/.gitattributes b/project/spring-boot-web-01/.gitattributes new file mode 100644 index 00000000..3b41682a --- /dev/null +++ b/project/spring-boot-web-01/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/project/spring-boot-web-01/.gitignore b/project/spring-boot-web-01/.gitignore new file mode 100644 index 00000000..667aaef0 --- /dev/null +++ b/project/spring-boot-web-01/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/project/spring-boot-web-01/pom.xml b/project/spring-boot-web-01/pom.xml new file mode 100644 index 00000000..0ad71725 --- /dev/null +++ b/project/spring-boot-web-01/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.5 + + + org.cpq + spring-boot-web-01 + 0.0.1-SNAPSHOT + spring-boot-web-01 + spring-boot-web-01 + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/project/spring-boot-web-01/src/main/java/org/cpq/springbootweb01/SpringBootWeb01Application.java b/project/spring-boot-web-01/src/main/java/org/cpq/springbootweb01/SpringBootWeb01Application.java new file mode 100644 index 00000000..5667386a --- /dev/null +++ b/project/spring-boot-web-01/src/main/java/org/cpq/springbootweb01/SpringBootWeb01Application.java @@ -0,0 +1,13 @@ +package org.cpq.springbootweb01; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringBootWeb01Application { + + public static void main(String[] args) { + SpringApplication.run(SpringBootWeb01Application.class, args); + } + +} diff --git a/project/spring-boot-web-01/src/main/java/org/cpq/springbootweb01/completable/CompletableController.java b/project/spring-boot-web-01/src/main/java/org/cpq/springbootweb01/completable/CompletableController.java new file mode 100644 index 00000000..f4d5be55 --- /dev/null +++ b/project/spring-boot-web-01/src/main/java/org/cpq/springbootweb01/completable/CompletableController.java @@ -0,0 +1,73 @@ +package org.cpq.springbootweb01.completable; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@RequestMapping("/c1") +@RestController +public class CompletableController { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final ConcurrentHashMap>> responseMap = new ConcurrentHashMap<>(); + + private static final AtomicLong idGenerator = new AtomicLong(123); // 模拟ID生成 + + @PostMapping("/send") + public Map send(@RequestBody Map body) throws Exception { + System.out.println("发送请求: "); + Map response = sendAndWayResp(body); + System.out.println("收到响应: " + response); + // 继续后续逻辑 + System.out.println("执行后续代码..."); + return response; + } + + @PostMapping("/resp") + public String resp(@RequestBody Map body) throws Exception { + complete(body); + return ""; + } + + private static Map sendAndWayResp(Map param) { + long messageId = idGenerator.getAndIncrement(); + // 将请求存入Map并启动发送线程 + responseMap.put(messageId, new CompletableFuture<>()); + + param.put("messageId", messageId); + System.out.println("发送请求,param: " + param); + + CompletableFuture> future = responseMap.get(messageId); + try { + return future.get(30, TimeUnit.SECONDS); + } catch (Exception ex) { + ex.printStackTrace(); + } + return new HashMap<>(); + } + + private static Map complete(Map body) { + String messageIdStr = body.get("messageId").toString(); + final Long messageId = Long.parseLong(messageIdStr); + responseMap.computeIfPresent(messageId, (id, future) -> { + Object data = body.get("data"); + try { + String content = objectMapper.writeValueAsString(data); + Map map = objectMapper.readValue(content, Map.class); + future.complete(map); + } catch (Exception ex) { + ex.printStackTrace(); + } + return null; // 移除已处理的条目 + }); + return new HashMap<>(); + } + +} diff --git a/project/spring-boot-web-01/src/main/resources/application.properties b/project/spring-boot-web-01/src/main/resources/application.properties new file mode 100644 index 00000000..0af799e5 --- /dev/null +++ b/project/spring-boot-web-01/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=spring-boot-web-01 diff --git a/project/spring-boot-web-01/src/test/java/org/cpq/springbootweb01/SpringBootWeb01ApplicationTests.java b/project/spring-boot-web-01/src/test/java/org/cpq/springbootweb01/SpringBootWeb01ApplicationTests.java new file mode 100644 index 00000000..c00c0cfe --- /dev/null +++ b/project/spring-boot-web-01/src/test/java/org/cpq/springbootweb01/SpringBootWeb01ApplicationTests.java @@ -0,0 +1,13 @@ +package org.cpq.springbootweb01; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SpringBootWeb01ApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/project/thing-model/JavaLinkKitDemo/README.txt b/project/thing-model/JavaLinkKitDemo/README.txt new file mode 100644 index 00000000..8a612c4b --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/README.txt @@ -0,0 +1,42 @@ +阿里云物模型 + +Demo 使用说明 +1.修改 device_id.son文件,填入三元组等信息: + 对于企业实例, 或者2021年07月30日之后(含当日)开通的物联网平台服务下公共实例, + 实例的详情页面下会有实例id,一般格式为iot-*******,请将其填入device_id.json的instanceId字段, + 请参考https://help.aliyun.com/document_detail/147356.htm + + 对于2021年07月30日之前(不含当日)开通的物联网平台服务下公共实例, + 请将开通物联网平台时所选的区域信息,填入device_id.json的region字段 + 具体包括如下选项: + 上海 ------ cn-shanghai + 新加坡 ----- ap-southeast-1 + 日本 ----- ap-northeast-1 + 美西 ----- us-west-1 + 德国 ----- eu-central-1 + 比如上海的region的话,请填入cn-shanghai + 注:北京和深圳地域的用户请在device_id.json中填写instanceId,不要填写region + +修改 com.aliyun.alink.devicesdk.demo.FileUtils.readFile 文件地址 + +2.执行HelloWorld工程的main方法 + +直连设备 +{ + "ProductKey": "ik8nUag4zSA", + "DeviceName": "device01", + "DeviceSecret": "a9b9de11ddf053a89eb38634e088601c" +} + +网关设备 +{ + "ProductKey": "ik8ngaVzfay", + "DeviceName": "gateway_device01", + "DeviceSecret": "98e01f47f0291b17bb2dcacdde9e1dad" +} +网关子设备 + DeviceName DeviceSecret ProductKey +VTnAhZHhxAQshqAKbxQZ d6e623c74e783e9a4bedccc8917e3872 ik8nXZA5lVi +VnwoGiGCCjkvluL8Q34T f301a2b1e68dd82457c94f4ae6786626 ik8nXZA5lVi +fZc5FdunDavRfkwAb93b 6696b0fff86cac716743f35779216d2d ik8nXZA5lVi + diff --git a/project/thing-model/JavaLinkKitDemo/device_id.json b/project/thing-model/JavaLinkKitDemo/device_id.json new file mode 100644 index 00000000..fd3cc883 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/device_id.json @@ -0,0 +1,25 @@ +{ + "productKey": "ik8ngaVzfay", + "deviceName": "gateway_device01", + "productSecret":"", + "deviceSecret": "98e01f47f0291b17bb2dcacdde9e1dad", + "instanceId": "iot-06z00c9jqpd2mgl", + "region": "cn-shanghai", + "subDevice": [ + { + "productKey": "ik8nXZA5lVi", + "deviceName": "VTnAhZHhxAQshqAKbxQZ", + "deviceSecret": "d6e623c74e783e9a4bedccc8917e3872" + }, + { + "productKey": "ik8nXZA5lVi", + "deviceName": "fZc5FdunDavRfkwAb93b", + "deviceSecret": "6696b0fff86cac716743f35779216d2d" + }, + { + "productKey": "ik8nXZA5lVi", + "deviceName": "VnwoGiGCCjkvluL8Q34T", + "deviceSecret": "f301a2b1e68dd82457c94f4ae6786626" + } + ] +} \ No newline at end of file diff --git a/project/thing-model/JavaLinkKitDemo/pom.xml b/project/thing-model/JavaLinkKitDemo/pom.xml new file mode 100644 index 00000000..22ed85d8 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + com.aliyun.alink.devicesdk.demo + JavaLinkKitDemo + 1.0.0 + Aliyun Open API SDK for Java + + Copyright (C) Alibaba Cloud Computing + All rights reserved. + + 版权所有 (C)阿里云计算有限公司 + + http://www.aliyun.com + + + + + + aliyunmaven + aliyun maven + https://maven.aliyun.com/nexus/content/groups/public/ + + + aliyunmavensnapshot + aliyun maven snapshot + https://maven.aliyun.com/nexus/content/repositories/snapshots + + + + + + com.aliyun.alink.linksdk + iot-linkkit-java + 1.2.1.2 + compile + + + com.google.code.gson + gson + 2.8.1 + compile + + + com.alibaba + fastjson + 1.2.83 + compile + + + + + + org.apache.qpid + qpid-jms-client + 0.56.0 + + + + + commons-codec + commons-codec + 1.10 + + + + + + + + org.slf4j + slf4j-api + 1.7.5 + + + + ch.qos.logback + logback-classic + 1.2.3 + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.5.5 + + + + com.aliyun.alink.devicesdk.demo.HelloWorld + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + + \ No newline at end of file diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/AmqpClient.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/AmqpClient.java new file mode 100644 index 00000000..8b9b5543 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/AmqpClient.java @@ -0,0 +1,215 @@ +package com.aliyun.alink.devicesdk.demo; + +import org.apache.commons.codec.binary.Base64; +import org.apache.qpid.jms.JmsConnection; +import org.apache.qpid.jms.JmsConnectionListener; +import org.apache.qpid.jms.message.JmsInboundMessageDispatch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.jms.*; +import javax.naming.Context; +import javax.naming.InitialContext; +import java.net.URI; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class AmqpClient { + private final static Logger logger = LoggerFactory.getLogger(AmqpClient.class); + private static String accessKey = "LTAI5t8jbXq53kbau9JVhmfK"; + private static String accessSecret = "ztmICmTjmRg78ME0LCuUkN8lIrnygI"; + private static String consumerGroupId = "DEFAULT_GROUP"; + + //iotInstanceId:实例ID。若是2021年07月30日之前(不含当日)开通的公共实例,请填空字符串。 + private static String iotInstanceId = "iot-06z00c9jqpd2mgl"; + + //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。 + //建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。 + private static String clientId = "123445hjhhghg"; + + //${YourHost}为接入域名,请参见AMQP客户端接入说明文档。 + private static String host = "iot-06z00c9jqpd2mgl.amqp.iothub.aliyuncs.com"; + + // 指定单个进程启动的连接数 + // 单个连接消费速率有限,请参考使用限制,最大64个连接 + // 连接数和消费速率及rebalance相关,建议每500QPS增加一个连接 + private static int connectionCount = 4; + + //业务处理异步线程池,线程池参数可以根据您的业务特点调整,或者您也可以用其他异步方式处理接收到的消息。 + private final static ExecutorService executorService = new ThreadPoolExecutor( + Runtime.getRuntime().availableProcessors(), + Runtime.getRuntime().availableProcessors() * 2, 60, TimeUnit.SECONDS, + new LinkedBlockingQueue(50000)); + + public static void main(String[] args) throws Exception { + List connections = new ArrayList<>(); + + //参数说明,请参见AMQP客户端接入说明文档。 + for (int i = 0; i < connectionCount; i++) { + long timeStamp = System.currentTimeMillis(); + //签名方法:支持hmacmd5、hmacsha1和hmacsha256。 + String signMethod = "hmacsha1"; + + //userName组装方法,请参见AMQP客户端接入说明文档。 + String userName = clientId + "-" + i + "|authMode=aksign" + + ",signMethod=" + signMethod + + ",timestamp=" + timeStamp + + ",authId=" + accessKey + + ",iotInstanceId=" + iotInstanceId + + ",consumerGroupId=" + consumerGroupId + + "|"; + //计算签名,password组装方法,请参见AMQP客户端接入说明文档。 + String signContent = "authId=" + accessKey + "×tamp=" + timeStamp; + String password = doSign(signContent, accessSecret, signMethod); + String connectionUrl = "failover:(amqps://" + host + ":5671?amqp.idleTimeout=80000)" + + "?failover.reconnectDelay=30"; + + Hashtable hashtable = new Hashtable<>(); + hashtable.put("connectionfactory.SBCF", connectionUrl); + hashtable.put("queue.QUEUE", "default"); + hashtable.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.qpid.jms.jndi.JmsInitialContextFactory"); + Context context = new InitialContext(hashtable); + ConnectionFactory cf = (ConnectionFactory)context.lookup("SBCF"); + Destination queue = (Destination)context.lookup("QUEUE"); + // 创建连接。 + Connection connection = cf.createConnection(userName, password); + connections.add(connection); + + ((JmsConnection)connection).addConnectionListener(myJmsConnectionListener); + // 创建会话。 + // Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。 + // Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。 + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + + connection.start(); + // 创建Receiver连接。 + MessageConsumer consumer = session.createConsumer(queue); + consumer.setMessageListener(messageListener); + } + + logger.info("amqp demo is started successfully, and will exit after 60s "); + + // 结束程序运行 + Thread.sleep(60 * 1000); + logger.info("run shutdown"); + + connections.forEach(c -> { + try { + c.close(); + } catch (JMSException e) { + logger.error("failed to close connection", e); + } + }); + + executorService.shutdown(); + if (executorService.awaitTermination(10, TimeUnit.SECONDS)) { + logger.info("shutdown success"); + } else { + logger.info("failed to handle messages"); + } + } + + private static MessageListener messageListener = new MessageListener() { + @Override + public void onMessage(final Message message) { + try { + //1.收到消息之后一定要ACK。 + // 推荐做法:创建Session选择Session.AUTO_ACKNOWLEDGE,这里会自动ACK。 + // 其他做法:创建Session选择Session.CLIENT_ACKNOWLEDGE,这里一定要调message.acknowledge()来ACK。 + // message.acknowledge(); + //2.建议异步处理收到的消息,确保onMessage函数里没有耗时逻辑。 + // 如果业务处理耗时过程过长阻塞住线程,可能会影响SDK收到消息后的正常回调。 + executorService.submit(new Runnable() { + @Override + public void run() { + processMessage(message); + } + }); + } catch (Exception e) { + logger.error("submit task occurs exception ", e); + } + } + }; + + /** + * 在这里处理您收到消息后的具体业务逻辑。 + */ + private static void processMessage(Message message) { + try { + byte[] body = message.getBody(byte[].class); + String content = new String(body); + String topic = message.getStringProperty("topic"); + String messageId = message.getStringProperty("messageId"); + logger.info("receive message" + + ",\n topic = " + topic + + ",\n messageId = " + messageId + + ",\n content = " + content); + } catch (Exception e) { + logger.error("processMessage occurs error ", e); + } + } + + private static JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() { + /** + * 连接成功建立。 + */ + @Override + public void onConnectionEstablished(URI remoteURI) { + logger.info("onConnectionEstablished, remoteUri:{}", remoteURI); + } + + /** + * 尝试过最大重试次数之后,最终连接失败。 + */ + @Override + public void onConnectionFailure(Throwable error) { + logger.error("onConnectionFailure, {}", error.getMessage()); + } + + /** + * 连接中断。 + */ + @Override + public void onConnectionInterrupted(URI remoteURI) { + logger.info("onConnectionInterrupted, remoteUri:{}", remoteURI); + } + + /** + * 连接中断后又自动重连上。 + */ + @Override + public void onConnectionRestored(URI remoteURI) { + logger.info("onConnectionRestored, remoteUri:{}", remoteURI); + } + + @Override + public void onInboundMessage(JmsInboundMessageDispatch envelope) {} + + @Override + public void onSessionClosed(Session session, Throwable cause) {} + + @Override + public void onConsumerClosed(MessageConsumer consumer, Throwable cause) {} + + @Override + public void onProducerClosed(MessageProducer producer, Throwable cause) {} + }; + + /** + * 计算签名,password组装方法,请参见AMQP客户端接入说明文档。 + */ + private static String doSign(String toSignString, String secret, String signMethod) throws Exception { + SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), signMethod); + Mac mac = Mac.getInstance(signMethod); + mac.init(signingKey); + byte[] rawHmac = mac.doFinal(toSignString.getBytes()); + return Base64.encodeBase64String(rawHmac); + } +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/BaseSample.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/BaseSample.java new file mode 100644 index 00000000..796ab8dc --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/BaseSample.java @@ -0,0 +1,42 @@ +package com.aliyun.alink.devicesdk.demo; + +import com.aliyun.alink.dm.api.DeviceInfo; +import com.aliyun.alink.linksdk.cmp.core.base.AMessage; +import com.aliyun.alink.linksdk.tools.AError; + +public class BaseSample { + + protected String TAG = getClass().getSimpleName(); + protected String productKey = null; + protected String deviceName = null; + + public BaseSample(String pk, String dn) { + productKey = pk; + deviceName = dn; + } + + protected String getMessage(AMessage message) { + if (message == null) { + return null; + } + if (message.data instanceof byte[]) { + return new String((byte[]) message.data); + } + return String.valueOf(message.data); + } + + protected String getError(AError error) { + if (error == null) { + return null; + } + return "[code=" + error.getCode() + ",msg=" + error.getMsg() + ",subCode=" + error.getSubCode() + ",subMsg=" + error.getSubMsg() + "]"; + } + + + protected String getPkDn(DeviceInfo info) { + if (info == null) { + return null; + } + return "[pk=" + info.productKey + ",dn=" + info.deviceName + "]"; + } +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/COTASample.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/COTASample.java new file mode 100644 index 00000000..4d14c039 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/COTASample.java @@ -0,0 +1,105 @@ +package com.aliyun.alink.devicesdk.demo; + +import com.aliyun.alink.dm.model.RequestModel; +import com.aliyun.alink.linkkit.api.LinkKit; +import com.aliyun.alink.linksdk.cmp.connect.channel.MqttRrpcRequest; +import com.aliyun.alink.linksdk.cmp.core.base.ARequest; +import com.aliyun.alink.linksdk.cmp.core.base.AResponse; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectRrpcHandle; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectRrpcListener; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectSendListener; +import com.aliyun.alink.linksdk.tools.AError; +import com.aliyun.alink.linksdk.tools.ALog; + +import java.util.HashMap; +import java.util.Map; + +public class COTASample extends BaseSample { + + + public COTASample(String pk, String dn) { + super(pk, dn); + } + + /** + * 设置远程配置下行监听器 + * 会有云端请求,先去订阅下行的 topic + */ + public void setCOTAChangeListener() { + LinkKit.getInstance().getDeviceCOTA().setCOTAChangeListener(new IConnectRrpcListener() { + @Override + public void onSubscribeSuccess(ARequest aRequest) { + ALog.d(TAG, "onSubscribeSuccess() called with: aRequest = [" + aRequest + "]"); + } + + @Override + public void onSubscribeFailed(ARequest aRequest, AError aError) { + ALog.d(TAG, "onSubscribeFailed() called with: aRequest = [" + aRequest + "], aError = [" + getError(aError) + "]"); + } + + @Override + public void onReceived(ARequest aRequest, IConnectRrpcHandle iConnectRrpcHandle) { + ALog.d(TAG, "onReceived() called with: aRequest = [" + aRequest + "], iConnectRrpcHandle = [" + iConnectRrpcHandle + "]"); + if (aRequest instanceof MqttRrpcRequest) { + // 云端下行数据 拿到 + String cotaData = new String((byte[]) ((MqttRrpcRequest) aRequest).payloadObj); + ALog.d(TAG, "received data=" + cotaData); +// ((MqttRrpcRequest) aRequest).payloadObj; +// ResponseModel> responseModel = JSONObject.parseObject(((MqttRrpcRequest) aRequest).payloadObj, new TypeReference>>(){}.getType()); + + } + // 返回数据示例 + /*{ + "id": "123", + "version": "1.0", + "code": 200, + "data": { + "configId": "123dagdah", + "configSize": 1234565, + "sign": "123214adfadgadg", + "signMethod": "Sha256", + "url": "xxx", + "getType": "file" + } + }*/ + + } + + @Override + public void onResponseSuccess(ARequest aRequest) { + ALog.d(TAG, "onResponseSuccess() called with: aRequest = [" + aRequest + "]"); + } + + @Override + public void onResponseFailed(ARequest aRequest, AError aError) { + ALog.d(TAG, "onResponseFailed() called with: aRequest = [" + aRequest + "], aError = [" + getError(aError) + "]"); + } + }); + } + + /** + * 获取云端当前的配置 + */ + public void cOTAGet() { + RequestModel requestModel = new RequestModel(); + requestModel.id = "123"; + requestModel.method = "thing.config.get"; + requestModel.version = "1.0"; + Map paramsMap = new HashMap(); + paramsMap.put("configScope", "product"); + paramsMap.put("getType", "file"); + requestModel.params = paramsMap; + + LinkKit.getInstance().getDeviceCOTA().COTAGet(requestModel, new IConnectSendListener() { + @Override + public void onResponse(ARequest aRequest, AResponse aResponse) { + ALog.d(TAG, "onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + (aResponse == null ? null : aResponse.data) + "]"); + } + + @Override + public void onFailure(ARequest aRequest, AError aError) { + ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + getError(aError) + "]"); + } + }); + } +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/DeviceInfoData.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/DeviceInfoData.java new file mode 100644 index 00000000..60f1f5d5 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/DeviceInfoData.java @@ -0,0 +1,48 @@ +package com.aliyun.alink.devicesdk.demo; + +import com.alibaba.fastjson.JSONObject; +import com.aliyun.alink.dm.api.BaseInfo; +import com.aliyun.alink.dm.api.DeviceInfo; + +import java.util.List; + +/* + * Copyright (c) 2014-2016 Alibaba Group. All rights reserved. + * License-Identifier: Apache-2.0 + * + * 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 + * + * http://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. + * + */ + +public class DeviceInfoData extends DeviceInfo { + + /** + * 区域 + */ + public String region = "cn-shanghai"; + /** + * 与网关关联的子设备信息 + * 后续网关测试demo 会 添加子设备 删除子设备 建立 topo关系 子设备上下线等 + */ + public List subDevice = null; + + @Override + public String toString() { + return JSONObject.toJSONString(this); + } + + /** + * 实例id + */ + public String instanceId = ""; +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/DeviceShadowSample.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/DeviceShadowSample.java new file mode 100644 index 00000000..9dcd1031 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/DeviceShadowSample.java @@ -0,0 +1,194 @@ +package com.aliyun.alink.devicesdk.demo; + +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.aliyun.alink.apiclient.threadpool.ThreadPool; +import com.aliyun.alink.dm.api.IShadowRRPC; +import com.aliyun.alink.dm.shadow.ShadowResponse; +import com.aliyun.alink.linkkit.api.LinkKit; +import com.aliyun.alink.linksdk.cmp.connect.channel.MqttPublishRequest; +import com.aliyun.alink.linksdk.cmp.core.base.ARequest; +import com.aliyun.alink.linksdk.cmp.core.base.AResponse; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectRrpcHandle; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectSendListener; +import com.aliyun.alink.linksdk.tools.AError; +import com.aliyun.alink.linksdk.tools.ALog; + + +/** + * 设备影子使用示例 + */ +public class DeviceShadowSample { + private static final String TAG = "DeviceShadowSample"; + + private int version = 1; + + + // 更新设备影子 需要根据获得到的设备影子读取返回的 version值,在更新的时候 {ver} 替换为version+1 + private String shadowUpdate = "{" + "\"method\": \"update\"," + "\"state\": {" + "\"reported\": {" + + "\"color\": \"red\"" + ",\"mode\": \"1\"" + "}" + "}," + "\"version\": {ver}" + "}"; + + // 获取设备影子 + private String shadowGet = "{" + "\"method\": \"get\"" + "}"; + + //删除设备影子 color 属性 {ver}需要替换 + private String shadowDelete = "{" + "\"method\": \"delete\"," + "\"state\": {" + "\"reported\": {" + + "\"color\": \"null\"" + "}" + "}," + "\"version\": {ver}" + "}"; + + //删除设备影子所有属性 {ver}需要替换 + private String shadowDeleteAll = "{" + "\"method\": \"delete\"," + "\"state\": {" + + "\"reported\":\"null\"" + "}," + "\"version\": {ver}" + "}"; + + /** + * 获取设备影子 + */ + public void shadowGet() { + ALog.d(TAG, "shadowGet"); + shadowUpstream(shadowGet); + } + + /** + * 设备影子更新 + */ + public void shadowUpdate() { + ALog.d(TAG, "shadowUpdate"); + version++; + shadowUpstream(shadowUpdate.replace("{ver}", String.valueOf(version))); + } + + /** + * 删除设备影子 + */ + public void shadowDelete() { + ALog.d(TAG, "shadowDelete"); + version++; + shadowUpstream(shadowDelete.replace("{ver}", String.valueOf(version))); + } + + /** + * 删除所有设备影子 + */ + public void shadowDeleteAll() { + ALog.d(TAG, "shadowDeleteAll"); + version++; + shadowUpstream(shadowDeleteAll.replace("{ver}", String.valueOf(version))); + } + + /** + * 先订阅设备影子的更新 topic + * 云端下发设备影子数据之后 + */ + public void listenDownStream() { + + ThreadPool.execute(new Runnable() { + @Override + public void run() { + LinkKit.getInstance().getDeviceShadow().setShadowChangeListener(new IShadowRRPC() { + @Override + public void onSubscribeSuccess(ARequest aRequest) { + ALog.d(TAG, "设备影子下行订阅成功"); + ALog.d(TAG, "onSubscribeSuccess() called with: aRequest = [" + aRequest + "]"); + } + + @Override + public void onSubscribeFailed(ARequest aRequest, AError aError) { + ALog.d(TAG, "设备影子下行订阅失败"); + ALog.d(TAG, "onSubscribeFailed() called with: aRequest = [" + aRequest + "], aError = [" + aError + "]"); + } + + @Override + public void onReceived(ARequest aRequest, AResponse aResponse, IConnectRrpcHandle iConnectRrpcHandle) { + ALog.d(TAG, "onReceived() called with: aRequest = [" + aRequest + "], iConnectRrpcHandle = [" + iConnectRrpcHandle + "]"); + // TODO user logic + ALog.d(TAG, "收到设备影子下行指令"); + try { + if (aRequest != null) { + String dataStr = null; + if (aResponse.data instanceof byte[]) { + dataStr = new String((byte[]) aResponse.data, "UTF-8"); + } else if (aResponse.data instanceof String) { + dataStr = (String) aResponse.data; + } else { + dataStr = String.valueOf(aResponse.data); + } + ALog.d(TAG, "dataStr = " + dataStr); + // 返回数据示例 + //{"method":"control","payload":{"state":{"desired":{"mode":2,"color":"white"},"reported":{"mode":"1","color":"red"}},"metadata":{"desired":{"mode":{"timestamp":1547642408},"color":{"timestamp":1547642408}},"reported":{"mode":{"timestamp":1547642408},"color":{"timestamp":1547642408}}}},"timestamp":1547642408,"version":12} + // 仅供参考 + ShadowResponse shadowResponse = JSONObject.parseObject(dataStr, new TypeReference>() { + }.getType()); + if (shadowResponse != null && shadowResponse.version != null) { + version = Integer.valueOf(shadowResponse.version); + } + + AResponse response = new AResponse(); + // TODO 用户实现控制设备 + // 用户控制设备之后 上报影子的值到云端 + // 上报设置之后的值到云端 + // 根据当前实际值上报 + response.data = shadowUpdate.replace("{ver}", String.valueOf(++version)); + // 第一个值 replyTopic 有默认值 用户不需要设置 + iConnectRrpcHandle.onRrpcResponse(null, response); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void onResponseSuccess(ARequest aRequest) { + ALog.d(TAG, "onResponseSuccess() called with: aRequest = [" + aRequest + "]"); + } + + @Override + public void onResponseFailed(ARequest aRequest, AError aError) { + ALog.w(TAG, "onResponseFailed() called with: aRequest = [" + aRequest + "], aError = [" + aError + "]"); + } + }); + } + }); + } + + private void shadowUpstream(String requestData) { + if (requestData == null) { + ALog.e(TAG, "shadowUpstream error requestData=null."); + return; + } + LinkKit.getInstance().getDeviceShadow().shadowUpload(requestData, new IConnectSendListener() { + @Override + public void onResponse(ARequest aRequest, AResponse aResponse) { + ALog.d(TAG, "onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + (aResponse == null ? null : aResponse.data) + "]"); + try { + if (aRequest instanceof MqttPublishRequest && aResponse != null) { + String dataStr = null; + if (aResponse.data instanceof byte[]) { + dataStr = new String((byte[]) aResponse.data, "UTF-8"); + } else if (aResponse.data instanceof String) { + dataStr = (String) aResponse.data; + } else { + dataStr = String.valueOf(aResponse.data); + } + ALog.d(TAG, "dataStr = " + dataStr); + // {"method":"reply","payload":{"status":"success","state":{"reported":{}},"metadata":{"reported":{}}},"timestamp":1547641855,"version":7,"clientToken":"null"} + ShadowResponse response = JSONObject.parseObject(dataStr, new TypeReference>() { + }.getType()); + if (response != null && response.version != null) { + version = Integer.valueOf(response.version); + } + } + } catch (NumberFormatException e) { + e.printStackTrace(); + ALog.e(TAG, "update version failed."); + } catch (Exception e) { + ALog.e(TAG, "update response parse exception."); + } + } + + @Override + public void onFailure(ARequest aRequest, AError aError) { + ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + aError + "]"); + } + }); + } + +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/FileUtils.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/FileUtils.java new file mode 100644 index 00000000..672ac5cb --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/FileUtils.java @@ -0,0 +1,39 @@ +package com.aliyun.alink.devicesdk.demo; + +import java.io.*; + +public class FileUtils { + + public static String readFile(String path) { + InputStreamReader reader = null; + BufferedReader bufReader = null; + try { + File filename = new File(path); + reader = new InputStreamReader(new FileInputStream(filename)); + bufReader = new BufferedReader(reader); + String line = ""; + String result = ""; + while ((line = bufReader.readLine()) != null) + result += line; + return result; + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (bufReader != null){ + bufReader.close(); + } + if (bufReader != null){ + bufReader.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e){ + e.printStackTrace(); + } + } + return null; + } +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/GatewaySample.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/GatewaySample.java new file mode 100644 index 00000000..47ed71c7 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/GatewaySample.java @@ -0,0 +1,380 @@ +package com.aliyun.alink.devicesdk.demo; + +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.aliyun.alink.dm.api.*; +import com.aliyun.alink.dm.model.ResponseModel; +import com.aliyun.alink.linkkit.api.LinkKit; +import com.aliyun.alink.linksdk.channel.gateway.api.subdevice.ISubDeviceActionListener; +import com.aliyun.alink.linksdk.channel.gateway.api.subdevice.ISubDeviceChannel; +import com.aliyun.alink.linksdk.channel.gateway.api.subdevice.ISubDeviceConnectListener; +import com.aliyun.alink.linksdk.channel.gateway.api.subdevice.ISubDeviceRemoveListener; +import com.aliyun.alink.linksdk.cmp.connect.channel.MqttRrpcRequest; +import com.aliyun.alink.linksdk.cmp.core.base.AMessage; +import com.aliyun.alink.linksdk.cmp.core.base.ARequest; +import com.aliyun.alink.linksdk.cmp.core.base.AResponse; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectRrpcHandle; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectRrpcListener; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectSendListener; +import com.aliyun.alink.linksdk.tmp.device.payload.ValueWrapper; +import com.aliyun.alink.linksdk.tools.AError; +import com.aliyun.alink.linksdk.tools.ALog; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class GatewaySample extends BaseSample { + private List userSubDev = null; + private List inputSubDev = null; + + private String testPublishTopic = "/sys/{productKey}/{deviceName}/thing/event/property/post"; + private String testSubscribePropertyService = "/sys/{productKey}/{deviceName}/thing/service/property/set"; + + + public GatewaySample(String pk, String dn, List subDevice) { + super(pk, dn); + inputSubDev = subDevice; + } + + /** + * 子设备动态注册 + * 云端安全策略问题 需要先在云端创建 子设备 + */ + public void subdevRegister() { + ALog.d(TAG, "subdevRegister"); + LinkKit.getInstance().getGateway().gatewaySubDevicRegister(inputSubDev, new IConnectSendListener() { + @Override + public void onResponse(ARequest aRequest, AResponse aResponse) { + ALog.d(TAG, "subdevRegister onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + (aResponse == null ? "null" : aResponse.data) + "]"); + try { + ResponseModel> response = JSONObject.parseObject(aResponse.data.toString(), new TypeReference>>() { + }.getType()); + //TODO 保存子设备的三元组信息 + // for test + userSubDev = response.data; + addSubDevice(0); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void onFailure(ARequest aRequest, AError aError) { + ALog.d(TAG, "subdevRegister onFailure() called with: aRequest = [" + aRequest + "], aError = [" + getError(aError) + "]"); + } + }); + } + + /** + * 获取当前网关的子设备列表 + * 需要先添加子设备到网关 + */ + public void getSubDevices() { + ALog.d(TAG, "getSubDevices"); + LinkKit.getInstance().getGateway().gatewayGetSubDevices(new IConnectSendListener() { + @Override + public void onResponse(ARequest aRequest, AResponse aResponse) { + ALog.d(TAG, "getSubDevices onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + (aResponse == null ? "null" : aResponse.data) + "]"); + } + + @Override + public void onFailure(ARequest aRequest, AError aError) { + ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + getError(aError) + "]"); + } + }); + } + + /** + * 添加子设备到网关 + * 子设备动态注册之后 可以拿到子设备的 deviceSecret 信息,签名的时候需要使用到 + * 签名方式 sign = hmac_md5(deviceSecret, clientId123deviceNametestproductKey123timestamp1524448722000) + */ + private void addSubDevice(final int index) { + if (userSubDev == null || userSubDev.size() < 1) { + ALog.e(TAG, "无有效已动态注册的设备,添加失败"); + return; + } + final DeviceInfo info = userSubDev.get(index); + LinkKit.getInstance().getGateway().gatewayAddSubDevice(info, new ISubDeviceConnectListener() { + @Override + public String getSignMethod() { + ALog.d(TAG, "getSignMethod() called"); + return "hmacsha1"; + } + + @Override + public String getSignValue() { + ALog.d(TAG, "getSignValue() called"); + Map signMap = new HashMap(); + signMap.put("productKey", info.productKey); + signMap.put("deviceName", info.deviceName); +// signMap.put("timestamp", String.valueOf(System.currentTimeMillis())); + signMap.put("clientId", getClientId()); + return SignUtils.hmacSign(signMap, info.deviceSecret); + } + + @Override + public String getClientId() { + ALog.d(TAG, "getClientId() called"); + return "id"; + } + + @Override + public Map getSignExtraData() { + return null; + } + + @Override + public void onConnectResult(boolean isSuccess, ISubDeviceChannel iSubDeviceChannel, AError aError) { + ALog.d(TAG, "onConnectResult() called with: isSuccess = [" + isSuccess + "], iSubDeviceChannel = [" + iSubDeviceChannel + "], aError = [" + getError(aError) + "]"); + if (isSuccess) { + ALog.d(TAG, "子设备添加成功 " + getPkDn(info)); + // for test + // 添加成功之后调用子设备登录 + subDevOnline(index); + } + } + + @Override + public void onDataPush(String s, AMessage message) { + // new String((byte[]) message.getData()) + // {"method":"thing.service.property.set","id":"184220091","params":{"test":2},"version":"1.0.0"} 示例 + ALog.d(TAG, "收到子设备下行数据 onDataPush() called with: topic = [" + s + "], message = [" + getMessage(message) + "]"); + } + }); + } + + /** + * 删除子设备 + */ + private void deleteSubDevice() { + if (userSubDev == null || userSubDev.size() < 1) { + ALog.e(TAG, "无有效已动态注册的设备,删除失败"); + return; + } + final DeviceInfo info = userSubDev.get(0); + LinkKit.getInstance().getGateway().gatewayDeleteSubDevice(info, new ISubDeviceRemoveListener() { + @Override + public void onSuceess() { + ALog.d(TAG, "deleteSubDevice onSuceess() called " + getPkDn(info)); + } + + @Override + public void onFailed(AError aError) { + ALog.d(TAG, "deleteSubDevice onFailed() called with: aError = [" + getError(aError) + "]" + getPkDn(info)); + } + }); + } + + /** + * 网关添加子设备之后才能代理子设备上线 + */ + private void subDevOnline(int index) { + if (userSubDev == null || userSubDev.size() < 1) { + ALog.e(TAG, "无有效已动态注册的设备,删除失败"); + return; + } + final DeviceInfo info = userSubDev.get(index); + LinkKit.getInstance().getGateway().gatewaySubDeviceLogin(info, new ISubDeviceActionListener() { + @Override + public void onSuccess() { + ALog.d(TAG, "subDevOnline onSuccess() called " + getPkDn(info)); +// subDevDisable(); +// subDevDelete(); + // 测试子设备物模型 +// testSubdevThing(); + } + + @Override + public void onFailed(AError aError) { + ALog.d(TAG, "subDevOnline onFailed() called with: aError = [" + getError(aError) + "]" + getPkDn(info)); + } + }); + } + + /** + * 网关添加子设备之后才能代理子设备下线 + */ + public void subDevOffline() { + ALog.d(TAG, "subDevOffline"); + if (userSubDev == null || userSubDev.size() < 1) { + ALog.e(TAG, "无有效已动态注册的设备,删除失败"); + return; + } + final DeviceInfo info = userSubDev.get(0); + LinkKit.getInstance().getGateway().gatewaySubDeviceLogout(info, new ISubDeviceActionListener() { + @Override + public void onSuccess() { + ALog.d(TAG, "subDevOffline onSuccess() called " + getPkDn(info)); + deleteSubDevice(); + } + + @Override + public void onFailed(AError aError) { + ALog.d(TAG, "subDevOffline onFailed() called with: aError = [" + getError(aError) + "]" + getPkDn(info)); + } + }); + } + + /** + * 代理子设备订阅 + */ + public void subDevSubscribe() { + if (userSubDev == null || userSubDev.size() < 1) { + ALog.e(TAG, "无有效已动态注册的设备,删除失败"); + return; + } + final DeviceInfo info = userSubDev.get(0); + String topic = testSubscribePropertyService; + + final String tempTopic = topic.replace("{deviceName}", info.deviceName) + .replace("{productKey}", info.productKey); + + LinkKit.getInstance().getGateway().gatewaySubDeviceSubscribe(tempTopic, info, new ISubDeviceActionListener() { + @Override + public void onSuccess() { + ALog.d(TAG, "subDevSubscribe onSuccess() called " + getPkDn(info)); + } + + @Override + public void onFailed(AError aError) { + ALog.d(TAG, "subDevSubscribe onFailed() called with: aError = [" + getError(aError) + "]" + getPkDn(info)); + } + }); + } + + /** + * 代理子设备发布 + */ + public void subDevPublish() { + if (userSubDev == null || userSubDev.size() < 1) { + ALog.e(TAG, "无有效已动态注册的设备,删除失败"); + return; + } + final DeviceInfo info = userSubDev.get(0); + String topic = testPublishTopic.replace("{deviceName}", info.deviceName) + .replace("{productKey}", info.productKey); + String data = "";//TODO add by user + LinkKit.getInstance().getGateway().gatewaySubDevicePublish(topic, data, info, new ISubDeviceActionListener() { + @Override + public void onSuccess() { + ALog.d(TAG, "subDevPublish onSuccess() called " + getPkDn(info)); + } + + @Override + public void onFailed(AError aError) { + ALog.d(TAG, "subDevPublish onFailed() called with: aError = [" + getError(aError) + "]" + getPkDn(info)); + } + }); + } + + /** + * 代理子设备取消订阅 + */ + public void subDevUnsubscribe() { + if (userSubDev == null || userSubDev.size() < 1) { + ALog.e(TAG, "无有效已动态注册的设备,删除失败"); + return; + } + final DeviceInfo info = userSubDev.get(0); + String topic = testSubscribePropertyService.replace("{deviceName}", info.deviceName) + .replace("{productKey}", info.productKey); + LinkKit.getInstance().getGateway().gatewaySubDeviceUnsubscribe(topic, info, new ISubDeviceActionListener() { + @Override + public void onSuccess() { + ALog.d(TAG, "subDevUnsubscribe onSuccess() called " + getPkDn(info)); + } + + @Override + public void onFailed(AError aError) { + ALog.d(TAG, "subDevUnsubscribe onFailed() called with: aError = [" + getError(aError) + "]" + getPkDn(info)); + } + }); + } + + + /** + * 子设备禁用监听 + */ + private void subDevDisable() { + if (userSubDev == null || userSubDev.size() < 1) { + ALog.e(TAG, "无有效已动态注册的设备,删除失败"); + return; + } + final DeviceInfo info = userSubDev.get(0); + LinkKit.getInstance().getGateway().gatewaySetSubDeviceDisableListener(info, new IConnectRrpcListener() { + @Override + public void onSubscribeSuccess(ARequest aRequest) { + ALog.d(TAG, "订阅禁用下行成功"); + } + + @Override + public void onSubscribeFailed(ARequest aRequest, AError aError) { + ALog.d(TAG, "订阅禁用下行失败 " + getError(aError)); + } + + @Override + public void onReceived(ARequest aRequest, IConnectRrpcHandle iConnectRrpcHandle) { + ALog.d(TAG, "== onReceived() called with: aRequest = [" + aRequest + "], iConnectRrpcHandle = [" + iConnectRrpcHandle + "]" + getPkDn(info)); + AResponse response = new AResponse(); + // 回复示例 + response.data = "{\"id\":\"123\", \"code\":\"200\"" + ",\"data\":{} }"; + //TODO + if (aRequest instanceof MqttRrpcRequest) { + String receivedData = new String((byte[]) ((MqttRrpcRequest) aRequest).payloadObj); + //{"method":"thing.disable","id":"123643484","params":{},"version":"1.0.0"} 参考数据 + // TODO 数据解析处理 + iConnectRrpcHandle.onRrpcResponse(((MqttRrpcRequest) aRequest).replyTopic, response); + } + } + + @Override + public void onResponseSuccess(ARequest aRequest) { + ALog.d(TAG, "subDevDisable onResponseSuccess() called with: aRequest = [" + aRequest + "]"); + } + + @Override + public void onResponseFailed(ARequest aRequest, AError aError) { + ALog.d(TAG, "subDevDisable onResponseFailed() called with: aRequest = [" + aRequest + "], aError = [" + getError(aError) + "]"); + } + }); + } + + /** + * 测试子设备物模型 + * 先获取子设备物模型 + */ + public void testSubdevThing() { + if (userSubDev == null || userSubDev.size() < 1) { + ALog.e(TAG, "无有效已动态注册的设备,删除失败"); + return; + } + final DeviceInfo info = userSubDev.get(0); + Map subDevInitState = new HashMap(); +// subDevInitState.put(); //TODO + LinkKit.getInstance().getGateway().initSubDeviceThing(null, info, subDevInitState, new IDMCallback() { + @Override + public void onSuccess(InitResult initResult) { + ALog.d(TAG, "initSubDeviceThing onSuccess() called with: initResult = [" + initResult + "]"); + testSubDevThing(info); + } + + @Override + public void onFailure(AError aError) { + ALog.d(TAG, "initSubDeviceThing onFailure() called with " + getError(aError)); + } + }); + } + + /** + * 测试子设备物模型 + * @param info 子设备信息 + */ + private void testSubDevThing(DeviceInfo info) { + SubThingSample sample = new SubThingSample(info.productKey, info.deviceName); + sample.readData(System.getProperty("user.dir") + "/test_sub_case.json"); + sample.setServiceHandler(); + sample.report(); + } +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/HelloWorld.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/HelloWorld.java new file mode 100644 index 00000000..92930135 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/HelloWorld.java @@ -0,0 +1,412 @@ +package com.aliyun.alink.devicesdk.demo; + +import com.alibaba.fastjson.JSONObject; +import com.aliyun.alink.apiclient.CommonRequest; +import com.aliyun.alink.apiclient.CommonResponse; +import com.aliyun.alink.apiclient.IoTCallback; +import com.aliyun.alink.apiclient.utils.StringUtils; +import com.aliyun.alink.dm.api.DeviceInfo; +import com.aliyun.alink.dm.api.InitResult; +import com.aliyun.alink.dm.api.IoTApiClientConfig; +import com.aliyun.alink.dm.model.ResponseModel; +import com.aliyun.alink.linkkit.api.ILinkKitConnectListener; +import com.aliyun.alink.linkkit.api.IoTMqttClientConfig; +import com.aliyun.alink.linkkit.api.LinkKit; +import com.aliyun.alink.linkkit.api.LinkKitInitParams; +import com.aliyun.alink.linksdk.channel.core.base.ARequest; +import com.aliyun.alink.linksdk.channel.core.base.IOnCallListener; +import com.aliyun.alink.linksdk.channel.core.persistent.mqtt.MqttConfigure; +import com.aliyun.alink.linksdk.channel.core.persistent.mqtt.MqttInitParams; +import com.aliyun.alink.linksdk.tmp.device.payload.ValueWrapper; +import com.aliyun.alink.linksdk.tools.AError; +import com.aliyun.alink.linksdk.tools.ALog; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttToken; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class HelloWorld { + private static final String TAG = "HelloWorld"; + + private String pk, dn; + private ThingSample thingTestManager = null; + + public static void main(String[] args) { + ALog.d(TAG, "Hello world!"); + ALog.setLevel(ALog.LEVEL_DEBUG); + HelloWorld manager = new HelloWorld(); + ALog.d(TAG, "args=" + Arrays.toString(args)); + System.out.println(System.getProperty("user.dir")); + String diPath = System.getProperty("user.dir") + "/device_id.json"; + // TODO + // String deviceInfo = FileUtils.readFile("D:\\mycode\\java-learn\\project\\JavaLinkKitDemo\\device_id.json"); + String deviceInfo = FileUtils.readFile("E:\\github\\java-learn\\project\\thing-model\\JavaLinkKitDemo\\device_id.json"); + if (deviceInfo == null) { + ALog.e(TAG, "main - need device info path."); + return; + } + Gson mGson = new Gson(); + DeviceInfoData deviceInfoData = mGson.fromJson(deviceInfo, DeviceInfoData.class); + if (deviceInfoData == null) { + ALog.e(TAG, "main - deviceInfo format error."); + return; + } + + // 如果device_id.json中没有设置deviceSecret, demo默认先走动态注册方式获取秘钥 + if (StringUtils.isEmptyString(deviceInfoData.deviceSecret)) { + manager.deviceRegister(deviceInfoData); + ALog.d(TAG, "测试一型一密动态注册,只测试动态注册"); + ALog.d(TAG, "请将获取到的deviceSecret填入到deviceId.json文件中继续一型一密的流程"); + return; + } + + ALog.d(TAG, "测试一机一密和物模型"); + manager.init(deviceInfoData); + } + + + public void init(final DeviceInfoData deviceInfoData) { + this.pk = deviceInfoData.productKey; + this.dn = deviceInfoData.deviceName; + LinkKitInitParams params = new LinkKitInitParams(); + /** + * 设置 Mqtt 初始化参数 + */ + IoTMqttClientConfig config = new IoTMqttClientConfig(); + config.productKey = deviceInfoData.productKey; + config.deviceName = deviceInfoData.deviceName; + config.deviceSecret = deviceInfoData.deviceSecret; + + if(!deviceInfoData.instanceId.isEmpty()) { + //如果实例详情页面有实例的id, 建议开发者填入实例id. 推荐的做法 + config.channelHost = "ssl://" + deviceInfoData.instanceId + ".mqtt.iothub.aliyuncs.com:443"; + } else { + //如果实例详情页面没有实例的id, 建议开发者填入实例所在的region. 注:该用法不支持深圳和北京两个region + config.channelHost = deviceInfoData.productKey + ".iot-as-mqtt." + deviceInfoData.region + ".aliyuncs.com:443"; + } + + /** + * 是否接受离线消息 + * 对应 mqtt 的 cleanSession 字段 + */ + config.receiveOfflineMsg = false; + params.mqttClientConfig = config; + + + /** + * 设置初始化三元组信息,用户传入 + */ + DeviceInfo deviceInfo = new DeviceInfo(); + deviceInfo.productKey = pk; + deviceInfo.deviceName = dn; + deviceInfo.deviceSecret = deviceInfoData.deviceSecret; + + params.deviceInfo = deviceInfo; + + /** + * 设置设备当前的初始状态值,属性需要和云端创建的物模型属性一致 + * 如果这里什么属性都不填,物模型就没有当前设备相关属性的初始值。 + * 用户调用物模型上报接口之后,物模型会有相关数据缓存。 + */ + Map propertyValues = new HashMap(); + params.propertyValues = propertyValues; + params.fmVersion = "1.0.2"; + + /** + * 设备进行初始化,并连云 + */ + LinkKit.getInstance().init(params, new ILinkKitConnectListener() { + public void onError(AError aError) { + ALog.e(TAG, "Init Error error=" + aError); + } + + public void onInitDone(InitResult initResult) { + ALog.i(TAG, "onInitDone result=" + initResult); + executeScheduler(deviceInfoData); + } + }); + } + + /** + * 定时执行 + * @param deviceInfoData + */ + public void executeScheduler(DeviceInfoData deviceInfoData) { + + /** + * 测试物模型,请参照testDeviceModel函数中的TODO注释将物模型字段替换为当前产品的物模型数据 + */ + // testDeviceModel(); + + /** 测试单纯mqtt通信能力 + * 物模型用例json的格式,用户可以通过自定义的topic使用自定义的格式, 用户可以使用SDK中单纯的mqtt通信能力满足这方面的开发需求 + */ + // testMqtt(); + + /** + * 测试设备标签 + */ + // testLabel(); + + /** + * 测试远程配置 + */ + // testCota(); + + /** + * 测试网关子设备管理功能,高级版功能 + */ + testGateway(deviceInfoData); + + /** + * 测试获取设备影子 + */ + // testDeviceShadow(); + } + + /** + * 物模型测试代码 + */ + private void testDeviceModel(){ + thingTestManager = new ThingSample(pk, dn); + /* 创建下行消息处理回调 */ + thingTestManager.setServiceHandler(); + /* 上报属性 */ + thingTestManager.reportDemoProperty(); + /* 上报事件 */ + thingTestManager.reportDemoEvent(); + } + + /** + * 设备影子测试代码 + */ + private void testDeviceShadow() { + DeviceShadowSample sample = new DeviceShadowSample(); + try { + sample.listenDownStream(); + sample.shadowGet(); + try { + Thread.sleep(5*1000); + } catch (Exception e){ + + } + // 异步操作,注意别和删除操作一起执行,不能保持时序 + sample.shadowUpdate(); + + // 异步操作,注意别和更新一起执行 + // sample.shadowDelete(); + // 异步操作, + // sample.shadowDeleteAll(); + } catch (Exception e) { + e.printStackTrace(); + } + //sample.shadowDelete(); + } + + + /** + * 动态注册示例代码,适用于所有region + * 1.现在云端创建产品和设备; + * 2.在云端开启动态注册; + * 3.填入pk、dn、ps; + * 4.调用该方法; + * 5.拿到deviceSecret返回之后 调初始化建联; + */ + public void deviceRegister( DeviceInfoData deviceInfoData) { + + //动态注册step1: 确定一型一密的类型(免预注册, 还是非免预注册) + //case 1: 如果registerType里面填写了regnwl, 表明设备的一型一密方式为免预注册(即无需创建设备) + //case 2: 如果这个字段为空, 则表示为需要预注册的一型一密(需要实现创建设备) + String registerType = "register"; + + //动态注册step2: 设置动态注册的注册接入点域名 + if(!deviceInfoData.instanceId.isEmpty()) { + //如果实例详情页面有实例的id, 建议开发者填入实例id. 推荐的做法 + MqttConfigure.mqttHost = "ssl://" + deviceInfoData.instanceId + ".mqtt.iothub.aliyuncs.com:443"; + } else { + //如果实例详情页面没有实例的id, 建议开发者填入实例所在的region. 注:该用法不支持深圳和北京两个region + MqttConfigure.mqttHost = deviceInfoData.productKey + ".iot-as-mqtt." + deviceInfoData.region + ".aliyuncs.com:443"; + } + + final MqttInitParams initParams = new MqttInitParams(deviceInfoData.productKey, deviceInfoData.productSecret, deviceInfoData.deviceName, "",registerType); + + //动态注册step3: 如果用户所用的实例为新版本的公共实例或者企业实例(控制台中有实例详情的页面), 需设置动态注册的实例id + initParams.instanceId = deviceInfoData.instanceId; + + final Object lock = new Object(); + LinkKit.getInstance().deviceDynamicRegister(initParams, new IOnCallListener() { + @Override + public void onSuccess(com.aliyun.alink.linksdk.channel.core.base.ARequest request, com.aliyun.alink.linksdk.channel.core.base.AResponse response) { + try { + String responseData = new String((byte[]) response.data); + JSONObject jsonObject = JSONObject.parseObject(responseData); + // 一型一密免预注册返回 + String clientId = jsonObject.getString("clientId"); + String deviceToken = jsonObject.getString("deviceToken"); + // 一型一密预注册返回 + String deviceSecret = jsonObject.getString("deviceSecret"); + ALog.e(TAG, "mqtt dynamic registration succeed,deviceSecret:" + deviceSecret); + synchronized (lock){ + lock.notify(); + } + + } catch (Exception e) { + } + } + + @Override + public void onFailed(ARequest aRequest, com.aliyun.alink.linksdk.channel.core.base.AError aError) { + ALog.e(TAG, "mqtt dynamic registration failed"); + synchronized (lock){ + lock.notify(); + } + } + + @Override + public boolean needUISafety() { + return false; + } + }); + + try { + //等待服务端下行报文,一般1s内就会返回 + synchronized (lock) { + lock.wait(3000); + } + + //动态注册step4: 关闭动态注册的实例. + // 注意:该接口不能在LinkKit.getInstance().deviceDynamicRegister的onSuccess/onFailed回调中执行,否则会报错 + LinkKit.getInstance().stopDeviceDynamicRegister(2000, null, new IMqttActionListener() { + @Override + public void onSuccess(IMqttToken iMqttToken) { + ALog.e(TAG,"mqtt dynamic registration success"); + } + + @Override + public void onFailure(IMqttToken iMqttToken, Throwable throwable) { + ALog.e(TAG,"mqtt dynamic registration failed"); + } + }); + + } catch (Exception e) { + } + } + + /** + * 动态注册示例代码(仅上海region支持,不建议使用) + * 1.现在云端创建产品和设备; + * 2.在云端开启动态注册; + * 3.填入pk、dn、ps; + * 4.调用该方法; + * 5.拿到deviceSecret返回之后 调初始化建联; + */ + public void deviceRegisterDeprecated(DeviceInfo deviceInfo) { + LinkKitInitParams params = new LinkKitInitParams(); + IoTMqttClientConfig config = new IoTMqttClientConfig(); + config.productKey = deviceInfo.productKey; + config.deviceName = deviceInfo.deviceName; + + params.mqttClientConfig = config; + params.connectConfig = new IoTApiClientConfig(); + + params.deviceInfo = deviceInfo; + + final CommonRequest request = new CommonRequest(); + request.setPath("/auth/register/device"); + LinkKit.getInstance().deviceRegister(params, request, new IoTCallback() { + public void onFailure(CommonRequest commonRequest, Exception e) { + ALog.e(TAG, "动态注册失败 " + e); + } + + public void onResponse(CommonRequest commonRequest, CommonResponse commonResponse) { + if (commonResponse == null || StringUtils.isEmptyString(commonResponse.getData())) { + ALog.e(TAG, "动态注册失败 response=null"); + return; + } + try { + ResponseModel> response = new Gson().fromJson(commonResponse.getData(), new TypeToken>>() { + }.getType()); + if (response != null && "200".equals(response.code)) { + ALog.d(TAG, "register success " + (commonResponse == null ? "" : commonResponse.getData())); + /** 获取 deviceSecret, 存储到本地,然后执行初始化建联 + * 这个流程只能走一次,获取到 secret 之后,下次启动需要读取本地存储的三元组, + * 直接执行初始化建联,不可以再走动态初始化 + */ + // deviceSecret = response.data.get("deviceSecret"); + // init(pk,dn,ds); + return; + } + } catch (Exception e) { + + } + ALog.d(TAG, "register fail " + commonResponse.getData()); + } + }); + } + + + + private void deinit(){ + LinkKit.getInstance().deinit(); + } + + /** + * 测试 Mqtt 基础topic封装 + * 发布 + * 订阅 + * 取消订阅 + * 注册资源监听,一般用于服务 + */ + private void testMqtt(){ + MqttSample sample = new MqttSample(pk, dn); + sample.publish(); + sample.subscribe(); + sample.unSubscribe(); + sample.registerResource(); + } + + /** + * 测试 COTA 远程配置 + */ + private void testCota() { + COTASample sample = new COTASample(pk, dn); + // 监听云端 COTA 下行数据更新 + sample.setCOTAChangeListener(); + // 获取 COTA 更新 + sample.cOTAGet(); + } + + /** + * 标签测试 + */ + private void testLabel() { + LabelSample sample = new LabelSample(pk, dn); + // 测试标签更新 + sample.labelUpdate(); + // 测试标签删除 +// sample.labelDelete(); + } + + /** + * @param deviceInfoData + * 网关测试 + */ + private void testGateway(DeviceInfoData deviceInfoData) { + GatewaySample sample = new GatewaySample(pk, dn, deviceInfoData.subDevice); + sample.getSubDevices(); + // 注册 + 添加 + 登录 + 上报 + sample.subdevRegister(); + +// try { +// Thread.sleep(10*1000); +// // 测试下线 + 删除 +// sample.subDevOffline(); +// } catch (Exception e){ +// e.printStackTrace(); +// } + } +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/LabelSample.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/LabelSample.java new file mode 100644 index 00000000..709a5787 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/LabelSample.java @@ -0,0 +1,73 @@ +package com.aliyun.alink.devicesdk.demo; + +import com.aliyun.alink.dm.model.RequestModel; +import com.aliyun.alink.linkkit.api.LinkKit; +import com.aliyun.alink.linksdk.cmp.core.base.ARequest; +import com.aliyun.alink.linksdk.cmp.core.base.AResponse; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectSendListener; +import com.aliyun.alink.linksdk.tools.AError; +import com.aliyun.alink.linksdk.tools.ALog; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LabelSample extends BaseSample { + + public LabelSample(String pk, String dn) { + super(pk, dn); + } + + public void labelDelete() { + RequestModel>> requestModel = new RequestModel>>(); + requestModel.id = "123"; + requestModel.method = "thing.deviceinfo.delete"; + requestModel.version = "1.0"; + List> paramsList = new ArrayList>(); + + Map listItemMap = new HashMap(); + listItemMap.put("attrKey", "Temperature"); + + paramsList.add(listItemMap); + requestModel.params = paramsList; + + LinkKit.getInstance().getDeviceLabel().labelDelete(requestModel, new IConnectSendListener() { + @Override + public void onResponse(ARequest aRequest, AResponse aResponse) { + ALog.d(TAG, "onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + (aResponse == null ? "" : aResponse.data) + "]"); + } + + @Override + public void onFailure(ARequest aRequest, AError aError) { + ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + getError(aError) + "]"); + } + }); + } + + public void labelUpdate() { + RequestModel>> requestModel = new RequestModel>>(); + requestModel.id = "123"; + requestModel.method = "thing.deviceinfo.update"; + requestModel.version = "1.0"; + List> paramsList = new ArrayList>(); + + Map listItemMap = new HashMap(); + listItemMap.put("attrKey", "Temperature"); + listItemMap.put("attrValue", "56.8"); + + paramsList.add(listItemMap); + requestModel.params = paramsList; + LinkKit.getInstance().getDeviceLabel().labelUpdate(requestModel, new IConnectSendListener() { + @Override + public void onResponse(ARequest aRequest, AResponse aResponse) { + ALog.d(TAG, "onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + (aResponse == null ? "" : aResponse.data) + "]"); + } + + @Override + public void onFailure(ARequest aRequest, AError aError) { + ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + getError(aError) + "]"); + } + }); + } +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/MqttSample.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/MqttSample.java new file mode 100644 index 00000000..bbcb8803 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/MqttSample.java @@ -0,0 +1,160 @@ +package com.aliyun.alink.devicesdk.demo; + +import com.aliyun.alink.linkkit.api.LinkKit; +import com.aliyun.alink.linksdk.cmp.api.CommonResource; +import com.aliyun.alink.linksdk.cmp.api.ResourceRequest; +import com.aliyun.alink.linksdk.cmp.connect.channel.MqttPublishRequest; +import com.aliyun.alink.linksdk.cmp.connect.channel.MqttResource; +import com.aliyun.alink.linksdk.cmp.connect.channel.MqttSubscribeRequest; +import com.aliyun.alink.linksdk.cmp.core.base.ARequest; +import com.aliyun.alink.linksdk.cmp.core.base.AResource; +import com.aliyun.alink.linksdk.cmp.core.base.AResponse; +import com.aliyun.alink.linksdk.cmp.core.listener.*; +import com.aliyun.alink.linksdk.tools.AError; +import com.aliyun.alink.linksdk.tools.ALog; + +public class MqttSample extends BaseSample { + final static String TAG = "MqttSample"; + + public MqttSample(String pk, String dn) { + super(pk, dn); + } + + /** + * 发布接口示例 + */ + public void publish() { + MqttPublishRequest request = new MqttPublishRequest(); + // topic 用户根据实际场景填写 + request.topic = "/sys/" + productKey + "/" + deviceName + "/thing/deviceinfo/update"; + /** + * 订阅回复的 replyTopic + * 如果业务有相应的响应需求,可以设置 replyTopic,且 isRPC=true + */ +// request.replyTopic = request.topic + "_reply"; + /** + * isRPC = true; 表示先订阅 replyTopic,然后再发布; + * isRPC = false; 不会订阅回复 + */ +// request.isRPC = true; + + /** + * 设置请求的 qos + */ + request.qos = 0; + // 更新标签 仅做测试 + request.payloadObj = "{\"id\":2, \"params\":{\"version\":\"1.0.0\"}}"; + LinkKit.getInstance().publish(request, new IConnectSendListener() { + @Override + public void onResponse(ARequest aRequest, AResponse aResponse) { + // publish 结果 + ALog.d(TAG, "onResponse " + (aResponse == null ? "" : aResponse.data)); + } + + @Override + public void onFailure(ARequest aRequest, AError aError) { + // publish 失败 + ALog.d(TAG, "onFailure " + (aError == null ? "" : (aError.getCode() + aError.getMsg()))); + } + }); + } + + /** + * 订阅接口示例 + */ + public void subscribe() { + MqttSubscribeRequest request = new MqttSubscribeRequest(); + // topic 用户根据实际场景填写 + request.topic = "/sys/" + productKey + "/" + deviceName + "/thing/deviceinfo/update"; + request.isSubscribe = true; + LinkKit.getInstance().subscribe(request, new IConnectSubscribeListener() { + @Override + public void onSuccess() { + // 订阅成功 + ALog.d(TAG, "onSuccess "); + } + + @Override + public void onFailure(AError aError) { + // 订阅失败 + ALog.d(TAG, "onFailure " + getError(aError)); + } + }); + } + + /** + * 取消订阅接口示例 + */ + public void unSubscribe() { + MqttSubscribeRequest request = new MqttSubscribeRequest(); + // topic 用户根据实际场景填写 + request.topic = "/sys/" + productKey + "/" + deviceName + "/thing/deviceinfo/update"; + request.isSubscribe = false; + LinkKit.getInstance().unsubscribe(request, new IConnectUnscribeListener() { + @Override + public void onSuccess() { + // 取消订阅成功 + ALog.d(TAG, "onSuccess "); + } + + @Override + public void onFailure(AError aError) { + // 取消订阅失败 + ALog.d(TAG, "onFailure " + getError(aError)); + } + }); + } + + + /** + * 注册资源 接口示例 + * 1.先订阅下行的 topic + * 2.云端通过该 topic 下行,发送指令; + * 3.收到数据并相应。 + */ + public void registerResource() { + final CommonResource resource = new CommonResource(); + resource.topic = "/ext/rrpc/+/" + productKey + "/" + deviceName + "/get"; + resource.replyTopic = resource.topic; + + LinkKit.getInstance().registerResource(resource, new IResourceRequestListener() { + @Override + public void onHandleRequest(AResource aResource, ResourceRequest resourceRequest, IResourceResponseListener iResourceResponseListener) { + // 收到云端数据下行 + ALog.d(TAG, "onHandleRequest aResource=" + aResource + ", resourceRequest=" + resourceRequest + ", iResourceResponseListener=" + iResourceResponseListener); + // 下行数据解析示例 +// String downstreamData = new String((byte[]) resourceRequest.payloadObj); + // 示例 {"id":"269297015","version":"1.0","method":"thing.event.property.post","params":{"lightData":{"vv":12}}} + + // 如果数据是json,且包含id字段,格式可以按照如下示例回复,传输数据请根据实际情况定制 +// if (aResource instanceof CommonResource) { +// ((CommonResource) aResource).replyTopic = resourceRequest.topic; +// } +// if (iResourceResponseListener != null) { +// AResponse response = new AResponse(); +// +// response.data = "{\"id\":\"123\", \"code\":\"200\"" + ",\"data\":{} }"; +// iResourceResponseListener.onResponse(aResource, resourceRequest, response); +// } + // 如果不一定是json格式,可以参考如下方式回复 + MqttPublishRequest rrpcResponse = new MqttPublishRequest(); + rrpcResponse.topic = resourceRequest.topic; + rrpcResponse.payloadObj ="xxx"; + + LinkKit.getInstance().publish(rrpcResponse,null); + } + + @Override + public void onSuccess() { + // 注册资源成功 + ALog.d(TAG, "onSuccess "); + } + + @Override + public void onFailure(AError aError) { + // 注册资源失败 + ALog.d(TAG, "onFailure " + getError(aError)); + } + }); + } +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/SubThingSample.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/SubThingSample.java new file mode 100644 index 00000000..702241f9 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/SubThingSample.java @@ -0,0 +1,575 @@ +package com.aliyun.alink.devicesdk.demo; + +import com.alibaba.fastjson.JSONObject; +import com.aliyun.alink.apiclient.utils.StringUtils; +import com.aliyun.alink.dm.api.BaseInfo; +import com.aliyun.alink.dm.api.IThing; +import com.aliyun.alink.linkkit.api.LinkKit; +import com.aliyun.alink.linksdk.cmp.connect.channel.MqttPublishRequest; +import com.aliyun.alink.linksdk.cmp.core.base.AMessage; +import com.aliyun.alink.linksdk.cmp.core.base.ARequest; +import com.aliyun.alink.linksdk.cmp.core.base.AResponse; +import com.aliyun.alink.linksdk.cmp.core.base.ConnectState; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectNotifyListener; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectSendListener; +import com.aliyun.alink.linksdk.tmp.api.InputParams; +import com.aliyun.alink.linksdk.tmp.api.OutputParams; +import com.aliyun.alink.linksdk.tmp.device.payload.ValueWrapper; +import com.aliyun.alink.linksdk.tmp.devicemodel.Arg; +import com.aliyun.alink.linksdk.tmp.devicemodel.Event; +import com.aliyun.alink.linksdk.tmp.devicemodel.Property; +import com.aliyun.alink.linksdk.tmp.devicemodel.Service; +import com.aliyun.alink.linksdk.tmp.listener.IPublishResourceListener; +import com.aliyun.alink.linksdk.tmp.listener.ITResRequestHandler; +import com.aliyun.alink.linksdk.tmp.listener.ITResResponseCallback; +import com.aliyun.alink.linksdk.tmp.utils.ErrorInfo; +import com.aliyun.alink.linksdk.tmp.utils.GsonUtils; +import com.aliyun.alink.linksdk.tmp.utils.TmpConstant; +import com.aliyun.alink.linksdk.tools.AError; +import com.aliyun.alink.linksdk.tools.ALog; +import com.aliyun.alink.linksdk.tools.TextUtils; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public class SubThingSample extends BaseSample{ + private static final String TAG = "SubThingSample"; + + private final static String SERVICE_SET = "set"; + private final static String SERVICE_GET = "get"; + private final static String CONNECT_ID = "LINK_PERSISTENT"; + final static Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$"); + + private final static int DEF_VALUE = Integer.MIN_VALUE; + + private String identity = null; + private String value = null; + private ValueWrapper valueWrapper = null; + private HashMap valueWrapperMap = null; + private ThingData mThingData = null; + private Gson mGson = new Gson(); + + private boolean isEvent = false; + + private BaseInfo baseInfo = null; + private IThing subdevThing = null; + + public SubThingSample(String pk, String dn) { + super(pk, dn); + baseInfo = new BaseInfo(); + baseInfo.productKey = pk; + baseInfo.deviceName = dn; + + subdevThing = LinkKit.getInstance().getGateway().getSubDeviceThing(baseInfo).first; + } + + public void readData(String path) { + String data = FileUtils.readFile(path); + mThingData = mGson.fromJson(data, ThingData.class); + if (mThingData == null) { + ALog.e(TAG, "数据格式错误"); + return; + } + identity = mThingData.identifier; + value = mThingData.value; + if ("event".equals(mThingData.type)) { + isEvent = true; + getPostEvent(); + } else { + isEvent = false; + getPost(identity, value); + } + } + + private void getPost(String identity, String value) { + try { + if (StringUtils.isEmptyString(identity)) { + ALog.w(TAG, "属性错误"); + return; + } + List propertyList = subdevThing.getProperties(); + if (propertyList == null) { + ALog.w(TAG, "选择的产品property列表为空"); + return; + } + Property property = null; + for (int i = 0; i < propertyList.size(); i++) { + property = propertyList.get(i); + if (property == null) { + continue; + } + if (identity.equals(property.getIdentifier())) { + break; + } + property = null; + } + if (property == null) { + ALog.w(TAG, "属性不存在"); + return; + } + if (TmpConstant.TYPE_VALUE_INTEGER.equals(property.getDataType().getType())) { + int parseData = getInt(value); + if (parseData != DEF_VALUE) { + updateCache(property.getIdentifier(), new ValueWrapper.IntValueWrapper(parseData)); + } else { + ALog.w(TAG, "数据格式不对"); + } + return; + } + if (TmpConstant.TYPE_VALUE_FLOAT.equals(property.getDataType().getType())) { + Double parseData = getDouble(value); + if (parseData != null) { + updateCache(property.getIdentifier(), new ValueWrapper.DoubleValueWrapper(parseData)); + } else { + ALog.w(TAG, "数据格式不对"); + } + return; + } + if (TmpConstant.TYPE_VALUE_DOUBLE.equals(property.getDataType().getType())) { + Double parseData = getDouble(value); + if (parseData != null) { + updateCache(property.getIdentifier(), new ValueWrapper.DoubleValueWrapper(parseData)); + } else { + ALog.w(TAG, "数据格式不对"); + } + return; + } + if (TmpConstant.TYPE_VALUE_BOOLEAN.equals(property.getDataType().getType())) { + int parseData = getInt(value); + if (parseData == 0 || parseData == 1) { + updateCache(property.getIdentifier(), new ValueWrapper.BooleanValueWrapper(parseData)); + } else { + ALog.w(TAG, "数据格式不对"); + } + return; + } + if (TmpConstant.TYPE_VALUE_TEXT.equals(property.getDataType().getType())) { + updateCache(property.getIdentifier(), new ValueWrapper.StringValueWrapper(value)); + return; + } + if (TmpConstant.TYPE_VALUE_DATE.equals(property.getDataType().getType())) { + updateCache(property.getIdentifier(), new ValueWrapper.DateValueWrapper(value)); + return; + } + if (TmpConstant.TYPE_VALUE_ENUM.equalsIgnoreCase(property.getDataType().getType())) { + updateCache(property.getIdentifier(), new ValueWrapper.EnumValueWrapper(getInt(value))); + return; + } + if (TmpConstant.TYPE_VALUE_ARRAY.equalsIgnoreCase(property.getDataType().getType())) { + ValueWrapper.ArrayValueWrapper arrayValueWrapper = GsonUtils.fromJson(value, new TypeToken() { + }.getType()); + updateCache(property.getIdentifier(), arrayValueWrapper); + return; + } + // 结构体数据解析 结构体不支持嵌套结构体和数组 + if (TmpConstant.TYPE_VALUE_STRUCT.equals(property.getDataType().getType())) { + try { + List> specsList = (List>) property.getDataType().getSpecs(); + if (specsList == null || specsList.size() == 0) { + ALog.w(TAG, "云端创建的struct结构为空,不上传任何值。"); + return; + } + Gson gson = new Gson(); + JsonObject dataJson = gson.fromJson(value, JsonObject.class); + Map dataMap = new HashMap(); + Map specsItem = null; + for (int i = 0; i < specsList.size(); i++) { + specsItem = specsList.get(i); + if (specsItem == null) { + continue; + } + String idKey = (String) specsItem.get("identifier"); + String dataType = (String) ((Map) specsItem.get("dataType")).get("type"); + if (idKey != null && dataJson.has(idKey) && dataType != null) { + ValueWrapper valueItem = null; + if ("int".equals(dataType)) { + valueItem = new ValueWrapper.IntValueWrapper(getInt(String.valueOf(dataJson.get(idKey)))); + } else if ("text".equals(dataType)) { + valueItem = new ValueWrapper.StringValueWrapper(dataJson.get(idKey).getAsString()); + } else if ("float".equals(dataType) || "double".equals(dataType)) { + valueItem = new ValueWrapper.DoubleValueWrapper(getDouble(String.valueOf(dataJson.get(idKey)))); + } else if ("bool".equals(dataType)) { + valueItem = new ValueWrapper.BooleanValueWrapper(getInt(String.valueOf(dataJson.get(idKey)))); + } else if ("date".equals(dataType)) { + if (isValidInt(String.valueOf(dataJson.get(idKey)))) { + valueItem = new ValueWrapper.DateValueWrapper(String.valueOf(dataJson.get(idKey))); + } else { + ALog.w(TAG, "数据格式不对"); + } + } else if ("enum".equals(dataType)) { + valueItem = new ValueWrapper.EnumValueWrapper(getInt(String.valueOf(dataJson.get(idKey)))); + } else { + ALog.w(TAG, "数据格式不支持"); + } + if (valueItem != null) { + dataMap.put(idKey, valueItem); + } + } + } + + updateCache(property.getIdentifier(), new ValueWrapper.StructValueWrapper(dataMap)); + } catch (Exception e) { + ALog.e(TAG, "数据格式不正确"); + } + return; + } + ALog.w(TAG, "该类型Demo暂不支持,用户可参照其他类型代码示例开发支持。"); + } catch (Exception e) { + ALog.e(TAG, "数据格式不对"); + e.printStackTrace(); + } + } + + private void getPostEvent() { + if (StringUtils.isEmptyString(identity)) { + ALog.w(TAG, "事件identifier错误"); + return; + } + List propertyList = subdevThing.getEvents(); + if (propertyList == null) { + ALog.w(TAG, "选择的产品 event列表为空"); + return; + } + Event event = null; + for (int i = 0; i < propertyList.size(); i++) { + event = propertyList.get(i); + if (event == null) { + continue; + } + if (identity.equals(event.getIdentifier())) { + break; + } + event = null; + } + if (event == null) { + ALog.w(TAG, "事件不存在"); + return; + } + + HashMap hashMap = new HashMap(); + try { + JSONObject object = JSONObject.parseObject(value); + if (object == null) { + ALog.d(TAG, "参数不能为空"); + return; + } + if (event.getOutputData() != null) { + for (int i = 0; i < event.getOutputData().size(); i++) { + Arg arg = event.getOutputData().get(i); + if (arg == null || arg.getDataType() == null || arg.getIdentifier() == null) { + continue; + } + String idnValue = String.valueOf(object.get(arg.getIdentifier())); + if (idnValue == null || object.get(arg.getIdentifier()) == null) { + continue; + } + if (TmpConstant.TYPE_VALUE_INTEGER.equals(arg.getDataType().getType())) { + int parseData = getInt(idnValue); + if (parseData != DEF_VALUE) { + hashMap.put(arg.getIdentifier(), new ValueWrapper.IntValueWrapper(parseData)); + } else { + ALog.d(TAG, "数据格式不对"); + break; + } + continue; + } + if (TmpConstant.TYPE_VALUE_FLOAT.equals(arg.getDataType().getType())) { + Double parseData = getDouble(idnValue); + if (parseData != null) { + hashMap.put(arg.getIdentifier(), new ValueWrapper.DoubleValueWrapper(parseData)); + } else { + ALog.d(TAG, "数据格式不对"); + break; + } + continue; + } + if (TmpConstant.TYPE_VALUE_DOUBLE.equals(arg.getDataType().getType())) { + Double parseData = getDouble(idnValue); + if (parseData != null) { + hashMap.put(arg.getIdentifier(), new ValueWrapper.DoubleValueWrapper(parseData)); + } else { + ALog.d(TAG, "数据格式不对"); + break; + } + continue; + } + if (TmpConstant.TYPE_VALUE_BOOLEAN.equals(arg.getDataType().getType())) { + int parseData = getInt(idnValue); + if (parseData == 0 || parseData == 1) { + hashMap.put(arg.getIdentifier(), new ValueWrapper.BooleanValueWrapper(parseData)); + } else { + ALog.d(TAG, "数据格式不对"); + break; + } + continue; + } + if (TmpConstant.TYPE_VALUE_TEXT.equals(arg.getDataType().getType())) { + hashMap.put(arg.getIdentifier(), new ValueWrapper.StringValueWrapper(idnValue)); + continue; + } + if (TmpConstant.TYPE_VALUE_DATE.equals(arg.getDataType().getType())) { + hashMap.put(arg.getIdentifier(), new ValueWrapper.DateValueWrapper(idnValue)); + continue; + } + if (TmpConstant.TYPE_VALUE_ENUM.equalsIgnoreCase(arg.getDataType().getType())) { + hashMap.put(arg.getIdentifier(), new ValueWrapper.EnumValueWrapper(getInt(idnValue))); + continue; + } + if (TmpConstant.TYPE_VALUE_ARRAY.equalsIgnoreCase(arg.getDataType().getType())) { + ValueWrapper.ArrayValueWrapper arrayValueWrapper = GsonUtils.fromJson(idnValue, new TypeToken() { + }.getType()); + hashMap.put(arg.getIdentifier(), arrayValueWrapper); + continue; + } + } + } + } catch (Exception e) { + e.printStackTrace(); + ALog.w(TAG, "数据格式错误"); + return; + } + valueWrapperMap = hashMap; + } + + private void reportEvent() { + OutputParams params = new OutputParams(valueWrapperMap); + subdevThing.thingEventPost(identity, params, new IPublishResourceListener() { + public void onSuccess(String resId, Object o) { + // 事件上报成功 + ALog.d(TAG, "onSuccess() called with: s = [" + resId + "], o = [" + o + "]"); + } + + public void onError(String resId, AError aError) { + // 事件上报失败 + ALog.w(TAG, "onError() called with: s = [" + resId + "], aError = [" + getError(aError) + "]"); + } + }); + } + + /** + * 上报灯的状态 + */ + private void updateCache(String identifier, ValueWrapper valueWrapper) { + identity = identifier; + this.valueWrapper = valueWrapper; + } + + public void report() { + if (isEvent) { + ALog.d(TAG, "上报事件" + identity); + reportEvent(); + } else { + ALog.d(TAG, "上报属性" + identity); + reportProperty(); + } + } + + private void reportProperty(){ + if (StringUtils.isEmptyString(identity) || valueWrapper == null) { + ALog.e(TAG, "数据格式错误"); + return; + } + + ALog.d(TAG, "上报 属性identity=" + identity); + + Map reportData = new HashMap(); + reportData.put(identity, valueWrapper); + subdevThing.thingPropertyPost(reportData, new IPublishResourceListener() { + + public void onSuccess(String s, Object o) { + // 属性上报成功 + ALog.d(TAG, "上报成功 onSuccess() called with: s = [" + s + "], o = [" + o + "]"); + } + + public void onError(String s, AError aError) { + // 属性上报失败 + ALog.d(TAG, "上报失败onError() called with: s = [" + s + "], aError = [" + getError(aError) + "]"); + } + }); + } + /** + * 云端调用设备的某项服务的时候,设备端需要响应该服务并回复。 + * 设备端事件触发的时候需要调用这个接口上报事件,如事件告警等 + * 需要用户在云端定义不同的 Error 的类型 + */ + public void setServiceHandler() { + ALog.d(TAG, "setServiceHandler() called"); + List srviceList = subdevThing.getServices(); + for (int i = 0; srviceList != null && i < srviceList.size(); i++) { + Service service = srviceList.get(i); + subdevThing.setServiceHandler(service.getIdentifier(), mCommonHandler); + } + LinkKit.getInstance().registerOnNotifyListener(connectNotifyListener); + // + } + + private String printAMessage(AMessage aMessage) { + return (aMessage == null || aMessage.data == null) ? "" : new String((byte[]) aMessage.data); + } + + private IConnectNotifyListener connectNotifyListener = new IConnectNotifyListener() { + public void onNotify(String connectId, String topic, AMessage aMessage) { + ALog.d(TAG, "onNotify() called with: connectId = [" + connectId + "], topic = [" + topic + "], aMessage = [" + printAMessage(aMessage) + "]"); + if (CONNECT_ID.equals(connectId) && !StringUtils.isEmptyString(topic) && + topic.startsWith("/sys/" + productKey + "/" + deviceName + "/rrpc/request")) { + ALog.d(TAG, "收到云端同步下行" + printAMessage(aMessage)); +// ALog.d(TAG, "receice Message=" + new String((byte[]) aMessage.data)); + // 服务端返回数据示例 {"method":"thing.service.test_service","id":"123374967","params":{"vv":60},"version":"1.0.0"} + MqttPublishRequest request = new MqttPublishRequest(); + request.isRPC = false; + request.topic = topic.replace("request", "response"); + String resId = topic.substring(topic.indexOf("rrpc/request/") + 13); + request.msgId = resId; + // TODO 用户根据实际情况填写 仅做参考 + request.payloadObj = "{\"id\":\"" + resId + "\", \"code\":\"200\"" + ",\"data\":{} }"; +// aResponse.data = + LinkKit.getInstance().getMqttClient().publish(request, new IConnectSendListener() { + public void onResponse(ARequest aRequest, AResponse aResponse) { + ALog.d(TAG, "onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + aResponse + "]"); + } + + public void onFailure(ARequest aRequest, AError aError) { + ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + getError(aError) + "]"); + } + }); + } + else if (CONNECT_ID.equals(connectId) && !TextUtils.isEmpty(topic) && + topic.startsWith("/ext/rrpc/")) { + ALog.d(TAG, "收到云端自定义RRPC下行"); + // ALog.d(TAG, "receice Message=" + new String((byte[]) aMessage.data)); + // 服务端返回数据示例 {"method":"thing.service.test_service","id":"123374967","params":{"vv":60},"version":"1.0.0"} + MqttPublishRequest request = new MqttPublishRequest(); + // 支持 0 和 1, 默认0 + // request.qos = 0; + request.isRPC = false; + request.topic = topic.replace("request", "response"); + String[] array = topic.split("/"); + String resId = array[3]; + request.msgId = resId; + // TODO 用户根据实际情况填写 仅做参考 + request.payloadObj = "{\"id\":\"" + resId + "\", \"code\":\"200\"" + ",\"data\":{} }"; + // aResponse.data = + LinkKit.getInstance().publish(request, new IConnectSendListener() { + @Override + public void onResponse(ARequest aRequest, AResponse aResponse) { + ALog.d(TAG, "onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + aResponse + "]"); + } + + @Override + public void onFailure(ARequest aRequest, AError aError) { + ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + aError + "]"); + } + }); + } + } + + public boolean shouldHandle(String s, String s1) { + return true; + } + + public void onConnectStateChange(String s, ConnectState connectState) { + + } + }; + + private ITResRequestHandler mCommonHandler = new ITResRequestHandler() { + public void onProcess(String identify, Object result, ITResResponseCallback itResResponseCallback) { + ALog.d(TAG, "onProcess() called with: s = [" + identify + "], o = [" + result + "], itResResponseCallback = [" + itResResponseCallback + "]"); + ALog.d(TAG, "收到云端异步服务调用 " + identify); + try { + if (SERVICE_SET.equals(identify)) { + // TODO 用户按照真实设备的接口调用 设置设备的属性 + // 设置完真实设备属性之后,上报设置完成的属性值 + // 用户根据实际情况判断属性是否设置成功 这里测试直接返回成功 + boolean isSetPropertySuccess = true; + if (isSetPropertySuccess) { + if (result instanceof InputParams) { + Map data = (Map) ((InputParams) result).getData(); +// data.get() + ALog.d(TAG, "收到异步下行数据 " + data); + // 响应云端 接收数据成功 + itResResponseCallback.onComplete(identify, null, null); + } else { + itResResponseCallback.onComplete(identify, null, null); + } + } else { + AError error = new AError(); + error.setCode(100); + error.setMsg("setPropertyFailed."); + itResResponseCallback.onComplete(identify, new ErrorInfo(error), null); + } + + } else if (SERVICE_GET.equals(identify)) { + // 初始化的时候将默认值初始化传进来,物模型内部会直接返回云端缓存的值 + + } else { + // 根据不同的服务做不同的处理,跟具体的服务有关系 + ALog.d(TAG, "用户根据真实的服务返回服务的值,请参照set示例"); + OutputParams outputParams = new OutputParams(); +// outputParams.put("op", new ValueWrapper.IntValueWrapper(20)); + itResResponseCallback.onComplete(identify, null, outputParams); + } + } catch (Exception e) { + e.printStackTrace(); + ALog.d(TAG, "TMP 返回数据格式异常"); + } + } + + public void onSuccess(Object o, OutputParams outputParams) { + ALog.d(TAG, "onSuccess() called with: o = [" + o + "], outputParams = [" + outputParams + "]"); + ALog.d(TAG, "注册服务成功"); + } + + public void onFail(Object o, ErrorInfo errorInfo) { + ALog.d(TAG, "onFail() called with: o = [" + o + "], errorInfo = [" + errorInfo + "]"); + ALog.d(TAG, "注册服务失败"); + } + }; + + private boolean isValidDouble(String value) { + if (StringUtils.isEmptyString(value)) { + return false; + } + try { + if (pattern != null && pattern.matcher(value) != null) { + if (pattern.matcher(value).matches()) { + return true; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + private Double getDouble(String value) { + if (isValidDouble(value)) { + return Double.parseDouble(value); + } + return null; + } + + private boolean isValidInt(String value) { + if (!StringUtils.isEmptyString(value)) { + return true; + } + return false; + } + + + private int getInt(String value) { + if (isValidInt(value)) { + try { + return Integer.parseInt(value); + } catch (Exception e) { + e.printStackTrace(); + } + } + return DEF_VALUE; + } +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/ThingData.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/ThingData.java new file mode 100644 index 00000000..6fd6b6cf --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/ThingData.java @@ -0,0 +1,9 @@ +package com.aliyun.alink.devicesdk.demo; + +import java.io.Serializable; + +public class ThingData implements Serializable { + public String type; + public String identifier; + public String value; +} diff --git a/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/ThingSample.java b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/ThingSample.java new file mode 100644 index 00000000..b074c350 --- /dev/null +++ b/project/thing-model/JavaLinkKitDemo/src/main/java/com/aliyun/alink/devicesdk/demo/ThingSample.java @@ -0,0 +1,290 @@ +package com.aliyun.alink.devicesdk.demo; + +import com.aliyun.alink.apiclient.utils.StringUtils; +import com.aliyun.alink.linkkit.api.LinkKit; +import com.aliyun.alink.linksdk.cmp.connect.channel.MqttPublishRequest; +import com.aliyun.alink.linksdk.cmp.core.base.AMessage; +import com.aliyun.alink.linksdk.cmp.core.base.ARequest; +import com.aliyun.alink.linksdk.cmp.core.base.AResponse; +import com.aliyun.alink.linksdk.cmp.core.base.ConnectState; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectNotifyListener; +import com.aliyun.alink.linksdk.cmp.core.listener.IConnectSendListener; +import com.aliyun.alink.linksdk.tmp.api.InputParams; +import com.aliyun.alink.linksdk.tmp.api.OutputParams; +import com.aliyun.alink.linksdk.tmp.device.payload.ValueWrapper; +import com.aliyun.alink.linksdk.tmp.devicemodel.Service; +import com.aliyun.alink.linksdk.tmp.listener.IPublishResourceListener; +import com.aliyun.alink.linksdk.tmp.listener.ITResRequestHandler; +import com.aliyun.alink.linksdk.tmp.listener.ITResResponseCallback; +import com.aliyun.alink.linksdk.tmp.utils.ErrorInfo; +import com.aliyun.alink.linksdk.tools.AError; +import com.aliyun.alink.linksdk.tools.ALog; +import com.aliyun.alink.linksdk.tools.TextUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + + +public class ThingSample extends BaseSample { + + private static final String TAG = "ThingSample"; + + private final static String SERVICE_SET = "set"; + private final static String SERVICE_GET = "get"; + private final static String CONNECT_ID = "LINK_PERSISTENT"; + + public ThingSample(String pk, String dn) { + super(pk, dn); + } + + /* 上报属性 */ + public void reportDemoProperty(){ + /** + * TODO 用户根据实际情况填写 仅做参考 + * + * 我们在控制台-->产品-->功能定义一栏中,创建标识符为LightSwitch的自定义属性,类型为整形; + * 我们以该属性为例,演示属性的更新 + */ + + String identity = "LightSwitch"; + ValueWrapper intWrapper = new ValueWrapper.IntValueWrapper(1); + String lightCurrent = "LightCurrent"; + ValueWrapper lightCurrentW = new ValueWrapper.DoubleValueWrapper(1.2); + + Map reportData = new HashMap(); + reportData.put(identity, intWrapper); + reportData.put(lightCurrent, lightCurrentW); + + LinkKit.getInstance().getDeviceThing().thingPropertyPost(reportData, new IPublishResourceListener() { + + public void onSuccess(String s, Object o) { + // 属性上报成功 + ALog.d(TAG, "上报成功 onSuccess() called with: s = [" + s + "], o = [" + o + "]"); + } + + public void onError(String s, AError aError) { + // 属性上报失败 + ALog.d(TAG, "上报失败onError() called with: s = [" + s + "], aError = [" + getError(aError) + "]"); + } + }); + } + + /** + *上报事件 + */ + public void reportDemoEvent() { + + /** + * TODO 用户根据实际情况填写 仅做参考 + * + * 我们在控制台-->产品-->功能定义一栏中,创建标识符为ErrorEvent的自定义事件,该事件有一个参数ErrorCode,类型为整形; + * 另一个参数ErrorDesc,类型为text类型 + * 我们以该事件为例,演示用1条事件上报的消息更新2个参数 + */ + + String identity = "ErrorEvent"; + + HashMap valueWrapperMap = new HashMap(); + /* 为参数ErrorCode赋值 */ + ValueWrapper intWrapper = new ValueWrapper.IntValueWrapper(1); + valueWrapperMap.put("ErrorCode", intWrapper); + /* 为参数ErrorDesc赋值 */ + ValueWrapper StringWrapper = new ValueWrapper.StringValueWrapper("hello123"); + valueWrapperMap.put("ErrorDesc", StringWrapper); + + OutputParams params = new OutputParams(valueWrapperMap); + + LinkKit.getInstance().getDeviceThing().thingEventPost(identity, params, new IPublishResourceListener() { + public void onSuccess(String resId, Object o) { + // 事件上报成功 + ALog.d(TAG, "onSuccess() called with: s = [" + resId + "], o = [" + o + "]"); + } + + public void onError(String resId, AError aError) { + // 事件上报失败 + ALog.w(TAG, "onError() called with: s = [" + resId + "], aError = [" + getError(aError) + "]"); + } + }); + } + + /** + * 设备端接收服务端的属性下发和服务下发的消息,并作出反馈 + */ + public void setServiceHandler() { + ALog.d(TAG, "setServiceHandler() called"); + List srviceList = LinkKit.getInstance().getDeviceThing().getServices(); + for (int i = 0; srviceList != null && i < srviceList.size(); i++) { + Service service = srviceList.get(i); + LinkKit.getInstance().getDeviceThing().setServiceHandler(service.getIdentifier(), mCommonHandler); + } + LinkKit.getInstance().registerOnNotifyListener(connectNotifyListener); + } + + private ITResRequestHandler mCommonHandler = new ITResRequestHandler() { + public void onProcess(String identify, Object result, ITResResponseCallback itResResponseCallback) { + ALog.d(TAG, "onProcess() called with: s = [" + identify + "], o = [" + result + "], itResResponseCallback = [" + itResResponseCallback + "]"); + try { + if (SERVICE_SET.equals(identify)) { + + /** 云端下发属性,SDK收到后触发的回调 + * + * TODO: 用户需要将下发的属性值,设置到真实设备里面。 + * 若设置成功,需要将isSetPropertySuccess写为true, + * demo将通过itResResponseCallback这个回调,将设备本地更新后的属性值写到云平台, + * 云平台的设备详情的物模型数据一栏属性值将会刷新 + * 若设置失败,需要将isSetPropertySuccess写为false, demo将不更新云平台中的属性值 + * + * 这里假定用户已经将属性设置到真实设备里面,将isSetPropertySuccess写为true + */ + boolean isSetPropertySuccess = true; + + if (isSetPropertySuccess) { + if (result instanceof InputParams) { + Map data = (Map) ((InputParams) result).getData(); + // 如果控制台下发了属性OverTiltEnable, 可以通过data.get("OverTiltEnable")来获取相应的属性值 + ALog.d(TAG, "收到下行数据 " + data); + + /** + * 读取属性的值 + * + * 假设用户物模型中有OverCurrentEnable这个属性,并且用户在控制台对OverCurrentEnable进行了下发属性的操作 + * 我们下面示例代码演示如何从中读取到属性的值 + * + * + * TODO:用户需要根据自己的物模型进行适配 + */ + + // ValueWrapper.IntValueWrapper intValue = (ValueWrapper.IntValueWrapper) data.get("OverCurrentEnable"); + // if (null != intValue) { + // ALog.d(TAG, "收到下行数据 " + intValue.getValue()); + // } + } + + /** + * 向云端上报数据 + * + * errorInfo为空,表示接收数据成功,itResResponseCallback.onComplete回调将 + * 回复/sys/${productKey}/${deviceName}/thing/service/property/set_reply给云端 + * 同时,该回调会再通过/sys/${productKey}/${deviceName}/thing/service/property/post将更新后的属性上报到云端 + * 表示设备端更新该属性成功 + */ + itResResponseCallback.onComplete(identify, null, null); + + } else { + AError error = new AError(); + error.setCode(100); + error.setMsg("setPropertyFailed."); + itResResponseCallback.onComplete(identify, new ErrorInfo(error), null); + } + + } else if (SERVICE_GET.equals(identify)) { + // 初始化的时候将默认值初始化传进来,物模型内部会直接返回云端缓存的值 + + } else { + /** + * 异步服务下行处理 + */ + ALog.d(TAG, "用户根据真实的服务返回服务的值,请参照set示例"); + OutputParams outputParams = new OutputParams(); + // outputParams.put("op", new ValueWrapper.IntValueWrapper(20)); + /** + * 设备端接收到服务,并返回响应数据给服务端 + */ + itResResponseCallback.onComplete(identify, null, outputParams); + } + } catch (Exception e) { + e.printStackTrace(); + ALog.d(TAG, "TMP 返回数据格式异常"); + } + } + + public void onSuccess(Object o, OutputParams outputParams) { + ALog.d(TAG, "onSuccess() called with: o = [" + o + "], outputParams = [" + outputParams + "]"); + ALog.d(TAG, "注册服务成功"); + } + + public void onFail(Object o, ErrorInfo errorInfo) { + ALog.d(TAG, "onFail() called with: o = [" + o + "], errorInfo = [" + errorInfo + "]"); + ALog.d(TAG, "注册服务失败"); + } + }; + + /** + * 同步服务回调处理函数 + * 同步服务下行方式包括云端系统RRPC下行和用户自定义RRPC下行两种,在该函数中都分别进行处理 + * 设备收到同步服务后,需要通过LinkKit.getInstance().getMqttClient().publish接口进行及时回复,否则控制台会显示调用超时失败 + */ + private IConnectNotifyListener connectNotifyListener = new IConnectNotifyListener() { + public void onNotify(String connectId, String topic, AMessage aMessage) { + ALog.d(TAG, "onNotify() called with: connectId = [" + connectId + "], topic = [" + topic + "], aMessage = [" + printAMessage(aMessage) + "]"); + try { + if (CONNECT_ID.equals(connectId) && !StringUtils.isEmptyString(topic) && + topic.startsWith("/sys/" + productKey + "/" + deviceName + "/rrpc/request")) { + ALog.d(TAG, "收到云端系统RRPC下行" + printAMessage(aMessage)); + // ALog.d(TAG, "receice Message=" + new String((byte[]) aMessage.data)); + // 服务端返回数据示例 {"method":"thing.service.test_service","id":"123374967","params":{"vv":60},"version":"1.0.0"} + MqttPublishRequest request = new MqttPublishRequest(); + request.isRPC = false; + request.topic = topic.replace("request", "response"); + String resId = topic.substring(topic.indexOf("rrpc/request/") + 13); + request.msgId = resId; + // TODO 用户根据实际情况填写 仅做参考 + request.payloadObj = "{\"id\":\"" + resId + "\", \"code\":\"200\"" + ",\"data\":{} }"; + // aResponse.data = + LinkKit.getInstance().getMqttClient().publish(request, new IConnectSendListener() { + public void onResponse(ARequest aRequest, AResponse aResponse) { + ALog.d(TAG, "onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + aResponse + "]"); + } + + public void onFailure(ARequest aRequest, AError aError) { + ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + getError(aError) + "]"); + } + }); + } + else if (CONNECT_ID.equals(connectId) && !TextUtils.isEmpty(topic) && + topic.startsWith("/ext/rrpc/")) { + ALog.d(TAG, "收到云端自定义RRPC下行"); + // ALog.d(TAG, "receice Message=" + new String((byte[]) aMessage.data)); + // 服务端返回数据示例 {"method":"thing.service.test_service","id":"123374967","params":{"vv":60},"version":"1.0.0"} + MqttPublishRequest request = new MqttPublishRequest(); + // 支持 0 和 1, 默认0 + // request.qos = 0; + request.isRPC = false; + request.topic = topic.replace("request", "response"); + String[] array = topic.split("/"); + String resId = array[3]; + request.msgId = resId; + // TODO 用户根据实际情况填写 仅做参考 + request.payloadObj = "{\"id\":\"" + resId + "\", \"code\":\"200\"" + ",\"data\":{} }"; + // aResponse.data = + LinkKit.getInstance().publish(request, new IConnectSendListener() { + @Override + public void onResponse(ARequest aRequest, AResponse aResponse) { + ALog.d(TAG, "onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + aResponse + "]"); + } + + @Override + public void onFailure(ARequest aRequest, AError aError) { + ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + aError + "]"); + } + }); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public boolean shouldHandle(String s, String s1) { + return true; + } + + public void onConnectStateChange(String s, ConnectState connectState) { + } + }; + + private String printAMessage(AMessage aMessage) { + return (aMessage == null || aMessage.data == null) ? "" : new String((byte[]) aMessage.data); + } +} diff --git a/project/thing-model/README.md b/project/thing-model/README.md new file mode 100644 index 00000000..76446d2b --- /dev/null +++ b/project/thing-model/README.md @@ -0,0 +1 @@ +# 物模型 diff --git a/project/thing-model/thing-demo/.gitignore b/project/thing-model/thing-demo/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/project/thing-model/thing-demo/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/project/thing-model/thing-demo/README.md b/project/thing-model/thing-demo/README.md new file mode 100644 index 00000000..7daa6e36 --- /dev/null +++ b/project/thing-model/thing-demo/README.md @@ -0,0 +1,16 @@ +# 物模型系统 + +## 接口文档地址 +http://localhost:8888/thing-demo/doc.html#/home + +# EMQX +### window安装 +https://www.emqx.io/docs/zh/v5.0/deploy/install-windows.html + +### 启动EMQX +进入 E:\software\emqx-5.0.21-windows-amd64 运行 .\bin\emqx.cmd start + +### EMQX Dashboard +http://127.0.0.1:18083/#/dashboard/overview + +首次登录账号密码 admin/public diff --git a/project/thing-model/thing-demo/pom.xml b/project/thing-model/thing-demo/pom.xml new file mode 100644 index 00000000..5f4daab7 --- /dev/null +++ b/project/thing-model/thing-demo/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.11 + + + com.example + thing-demo + 0.0.1-SNAPSHOT + thing-demo + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.integration + spring-integration-mqtt + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.apache.commons + commons-pool2 + + + + org.projectlombok + lombok + true + + + + com.baomidou + mybatis-plus-boot-starter + 3.5.3.1 + + + mysql + mysql-connector-java + 5.1.49 + + + + com.github.xiaoymin + knife4j-openapi2-spring-boot-starter + 4.1.0 + + + + org.apache.commons + commons-collections4 + 4.4 + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + + + + + + src/main/java + + **/*.xml + + + + src/main/resources + + **/*.* + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + + diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/ThingDemoApplication.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/ThingDemoApplication.java new file mode 100644 index 00000000..d31d3d36 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/ThingDemoApplication.java @@ -0,0 +1,15 @@ +package com.example.thingdemo; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@MapperScan(basePackages = {"com.example.thingdemo.mapper.**"}) +public class ThingDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(ThingDemoApplication.class, args); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/advice/CustomControllerAdvice.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/advice/CustomControllerAdvice.java new file mode 100644 index 00000000..99a46de5 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/advice/CustomControllerAdvice.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2018-2028, Chill Zhuang 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 dreamlu.net 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: Chill 庄骞 (smallchill@163.com) + */ +package com.example.thingdemo.advice; + +import com.example.thingdemo.common.Result; +import com.example.thingdemo.exception.AppException; +import com.example.thingdemo.util.CommonUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.Set; + +/** + * 自定义异常处理 + * + * @author chenpq05 + * @since 2022/4/18 14:37 + */ +@Slf4j +@Configuration(proxyBeanMethods = false) +@RestControllerAdvice +public class CustomControllerAdvice { + + @ExceptionHandler(AppException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleAppException(AppException e) { + log.error("业务异常", e); + return Result.fail(e.getMessage()); + } + + + /** + * 使用 @Valid 注解,被此方法捕获 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result methodArgumentNotValidException(MethodArgumentNotValidException ex) { + log.error("捕获MethodArgumentNotValidException异常", ex); + StringBuilder sb = new StringBuilder(); + BindingResult bindingResult = ex.getBindingResult(); + for (FieldError fieldError : bindingResult.getFieldErrors()) { + String defaultMessage = fieldError.getDefaultMessage(); + boolean isMatch = CommonUtil.isEndWith(defaultMessage, CommonUtil.END_CHAR); + // 没有结尾符号,添加句号 + defaultMessage = isMatch ? defaultMessage : String.format("%s。", defaultMessage); + sb.append(defaultMessage); + } + String msg = sb.toString(); + return Result.fail(msg); + } + + /** + * 使用 @Validated 注解,被此方法捕获 + */ + @ExceptionHandler(ConstraintViolationException.class) + public Result constraintViolationException(ConstraintViolationException ex) { + log.error("捕获ConstraintViolationException异常", ex); + Set> constraintViolations = ex.getConstraintViolations(); + StringBuilder sb = new StringBuilder(); + if (!constraintViolations.isEmpty()) { + for (ConstraintViolation constraint : constraintViolations) { + String message = constraint.getMessage(); + boolean isMatch = CommonUtil.isEndWith(message, CommonUtil.END_CHAR); + // 没有结尾符号,添加句号 + message = isMatch ? message : String.format("%s。", message); + sb.append(message); + } + } + String msg = sb.toString(); + return Result.fail(400, msg); + } + + + @ExceptionHandler(NullPointerException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleNullPointerException(NullPointerException e) { + log.error("空指针异常", e); + return Result.fail(500, "空指针异常"); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) { + log.error("服务异常", e); + return Result.fail(500, "服务异常"); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/ao/DeviceAddUpdateAo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/ao/DeviceAddUpdateAo.java new file mode 100644 index 00000000..3e88d158 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/ao/DeviceAddUpdateAo.java @@ -0,0 +1,37 @@ +package com.example.thingdemo.ao; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +/** + * @author chenpq + * @since 2023-05-07 21:52:39 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "设备-新增修改ao") +public class DeviceAddUpdateAo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + private Long id; + + @ApiModelProperty(value = "设备名称") + private String deviceName; + + @ApiModelProperty(value = "设备编码") + private String deviceCode; + + @ApiModelProperty(value = "产品key") + private String productKey; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/ao/ThingAddUpdateAo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/ao/ThingAddUpdateAo.java new file mode 100644 index 00000000..668c8923 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/ao/ThingAddUpdateAo.java @@ -0,0 +1,34 @@ +package com.example.thingdemo.ao; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +/** + * @author chenpq + * @since 2023-05-04 14:08:27 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "物模型概述-新增修改ao") +public class ThingAddUpdateAo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + private Long id; + + @ApiModelProperty(value = "产品名称") + private String name; + + @ApiModelProperty(value = "版本") + private String version; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingActionCache.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingActionCache.java new file mode 100644 index 00000000..34538088 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingActionCache.java @@ -0,0 +1,41 @@ +package com.example.thingdemo.cache; + +import io.swagger.annotations.ApiModelProperty; +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class ThingActionCache implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "物模型维度名称") + private String name; + + @ApiModelProperty(value = "唯一标识符") + private String identifier; + + @ApiModelProperty(value = "action调用方式,1(同步调用)或2(异步调用)") + private Integer actionCallType; + + @ApiModelProperty(value = "是否必选:是1,否0") + private Integer required; + + @ApiModelProperty(value = "输入参数") + private List inputData; + + @ApiModelProperty(value = "输出参数") + private List outputData; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingCache.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingCache.java new file mode 100644 index 00000000..cdf02ffe --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingCache.java @@ -0,0 +1,32 @@ +package com.example.thingdemo.cache; + +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * @author chenpq + * @since 2023-05-04 14:08:27 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class ThingCache implements Serializable { + + private static final long serialVersionUID = 1L; + + private ThingProfileCache profile; + + private List properties; + + private List events; + + private List actions; + +} + + diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingEventCache.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingEventCache.java new file mode 100644 index 00000000..fbf283b6 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingEventCache.java @@ -0,0 +1,38 @@ +package com.example.thingdemo.cache; + +import io.swagger.annotations.ApiModelProperty; +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class ThingEventCache implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "物模型维度名称") + private String name; + + @ApiModelProperty(value = "唯一标识符") + private String identifier; + + @ApiModelProperty(value = "事件类型 1信息,2告警,3故障") + private Integer eventType; + + @ApiModelProperty(value = "是否必选:是1,否0") + private Integer required; + + @ApiModelProperty(value = "输出参数") + private List outputData; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingParamSpecCache.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingParamSpecCache.java new file mode 100644 index 00000000..bc64773c --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingParamSpecCache.java @@ -0,0 +1,44 @@ +package com.example.thingdemo.cache; + +import io.swagger.annotations.ApiModelProperty; +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * @author chenpq + * @since 2023-05-05 17:00:52 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class ThingParamSpecCache implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + private Long id; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符") + private String paramIdentifier; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符名称") + private String paramIdentifierName; + + @ApiModelProperty(value = "数据类型: int32(原生)、float(原生)、double(原生)、text(原生)、date(String类型UTC毫秒)、bool(0或1的int类型)、enum(int类型,枚举项定义方法与bool类型定义0和1的方法相同)、struct(结构体类型,可包含前面7种类型,下面使用\"specs\":[{}]描述包含的对象)、array(数组类型,支持int、double、float、text、struct)") + private String dataType; + + @ApiModelProperty(value = "数据规格 { \"min\": \"参数最小值(int、float、double类型特有)。\", \"max\": \"参数最大值(int、float、double类型特有)。\", \"unit\": \"属性单位(int、float、double类型特有,非必填)。\", \"unitName\": \"单位名称(int、float、double类型特有,非必填)。\", \"size\": \"数组元素的个数,最大512(array类型特有)。\", \"step\": \"步长(text、enum类型无此参数)。\", \"length\": \"数据长度,最大10240(text类型特有)。\", \"0\": \"0的值(bool类型特有)。\", \"1\": \"1的值(bool类型特有)。\", \"arrayItemType\": \"数组元素的类型(array类型特有)。\" }") + private String specs; + + @ApiModelProperty(value = "排序,小的在前面") + private Integer sort; + + @ApiModelProperty(value = "json对象key的数据规格") + private List jsonElemList; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingParamSpecJsonElemCache.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingParamSpecJsonElemCache.java new file mode 100644 index 00000000..43ab175d --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingParamSpecJsonElemCache.java @@ -0,0 +1,39 @@ +package com.example.thingdemo.cache; + +import io.swagger.annotations.ApiModelProperty; +import java.io.Serializable; +import javax.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * @author chenpq + * @since 2023-05-05 17:00:52 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class ThingParamSpecJsonElemCache implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符") + private String paramIdentifier; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符名称") + private String paramIdentifierName; + + @ApiModelProperty(value = "数据类型: int32(原生)、float(原生)、double(原生)、text(原生)、date(String类型UTC毫秒)、bool(0或1的int类型)、enum(int类型,枚举项定义方法与bool类型定义0和1的方法相同)、struct(结构体类型,可包含前面7种类型,下面使用\"specs\":[{}]描述包含的对象)、array(数组类型,支持int、double、float、text、struct)") + @NotEmpty(message = "数据类型不能为空") + private String dataType; + + @ApiModelProperty(value = "数据规格 { \"min\": \"参数最小值(int、float、double类型特有)。\", \"max\": \"参数最大值(int、float、double类型特有)。\", \"unit\": \"属性单位(int、float、double类型特有,非必填)。\", \"unitName\": \"单位名称(int、float、double类型特有,非必填)。\", \"size\": \"数组元素的个数,最大512(array类型特有)。\", \"step\": \"步长(text、enum类型无此参数)。\", \"length\": \"数据长度,最大10240(text类型特有)。\", \"0\": \"0的值(bool类型特有)。\", \"1\": \"1的值(bool类型特有)。\", \"arrayItemType\": \"数组元素的类型(array类型特有)。\" }") + private String specs; + + @ApiModelProperty(value = "排序,小的在前面") + private Integer sort; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingProfileCache.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingProfileCache.java new file mode 100644 index 00000000..79d53ae2 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingProfileCache.java @@ -0,0 +1,30 @@ +package com.example.thingdemo.cache; + +import io.swagger.annotations.ApiModelProperty; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * @author chenpq + * @since 2023-05-04 14:08:27 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class ThingProfileCache implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "产品名称") + private String name; + + @ApiModelProperty(value = "版本") + private String version; + +} + + diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingPropertiesCache.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingPropertiesCache.java new file mode 100644 index 00000000..a25c060b --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/cache/ThingPropertiesCache.java @@ -0,0 +1,40 @@ +package com.example.thingdemo.cache; + +import io.swagger.annotations.ApiModelProperty; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public class ThingPropertiesCache implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "物模型维度名称") + private String name; + + @ApiModelProperty(value = "唯一标识符") + private String identifier; + + @ApiModelProperty(value = "属性读写类型:只读(r)或读写(rw)") + private String propertiesAccessMode; + + @ApiModelProperty(value = "是否必选:是1,否0") + private Integer required; + + @ApiModelProperty(value = "数据类型: int32(原生)、float(原生)、double(原生)、text(原生)、date(String类型UTC毫秒)、bool(0或1的int类型)、enum(int类型,枚举项定义方法与bool类型定义0和1的方法相同)、struct(结构体类型,可包含前面7种类型,下面使用\"specs\":[{}]描述包含的对象)、array(数组类型,支持int、double、float、text、struct)") + private String dataType; + + @ApiModelProperty(value = "数据规格 { \"min\": \"参数最小值(int、float、double类型特有)。\", \"max\": \"参数最大值(int、float、double类型特有)。\", \"unit\": \"属性单位(int、float、double类型特有,非必填)。\", \"unitName\": \"单位名称(int、float、double类型特有,非必填)。\", \"size\": \"数组元素的个数,最大512(array类型特有)。\", \"step\": \"步长(text、enum类型无此参数)。\", \"length\": \"数据长度,最大10240(text类型特有)。\", \"0\": \"0的值(bool类型特有)。\", \"1\": \"1的值(bool类型特有)。\", \"arrayItemType\": \"数组元素的类型(array类型特有)。\" }") + private String specs; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/common/PageReq.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/common/PageReq.java new file mode 100644 index 00000000..4e295c2f --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/common/PageReq.java @@ -0,0 +1,29 @@ +package com.example.thingdemo.common; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +/** + * @author chenpq + * @since 2022/2/8 14:19 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class PageReq implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "页码,从 1 开始", example = "1") + private Integer pageNum = 1; + + @ApiModelProperty(value = "每页多少条", example = "10") + private Integer pageSize = 10; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/common/Result.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/common/Result.java new file mode 100644 index 00000000..1e611209 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/common/Result.java @@ -0,0 +1,79 @@ +package com.example.thingdemo.common; + +import java.io.Serializable; + +/** + * ResponseBody返回统一的数据结构 + * + * @author chenpq05 + * @since 2022/2/23 14:14 + */ +public class Result implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer code; + + private String message; + + @SuppressWarnings("squid:S1948") + private T data; + + public Result() { + } + + public Result(Integer code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + public static Result success() { + return new Result<>(200, "success", null); + } + + public static Result success(T data) { + return new Result<>(200, "success", data); + } + + public static Result fail(String message) { + return new Result<>(400, message, null); + } + + public static Result fail(Integer code, String message) { + return new Result<>(code, message, null); + } + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + @Override + public String toString() { + return "Result{" + + "code=" + code + + ", message='" + message + '\'' + + ", data=" + data + + '}'; + } +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/config/RedisConfig.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/config/RedisConfig.java new file mode 100644 index 00000000..6b335e91 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/config/RedisConfig.java @@ -0,0 +1,36 @@ +package com.example.thingdemo.config; + +import com.example.thingdemo.util.ObjectMapperUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(redisConnectionFactory); + + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); + ObjectMapper om = ObjectMapperUtil.newObjectMapper(); + jackson2JsonRedisSerializer.setObjectMapper(om); + + //key使用StringRedisSerializer + StringRedisSerializer strSerializer = new StringRedisSerializer(); + template.setKeySerializer(strSerializer); + //value使用Jackson2JsonRedisSerializer + template.setValueSerializer(jackson2JsonRedisSerializer); + + template.setHashKeySerializer(jackson2JsonRedisSerializer); + template.setHashValueSerializer(jackson2JsonRedisSerializer); + return template; + } + +} \ No newline at end of file diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/constant/RedisConstant.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/constant/RedisConstant.java new file mode 100644 index 00000000..156d86a4 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/constant/RedisConstant.java @@ -0,0 +1,19 @@ +package com.example.thingdemo.constant; + +/** + * @author chenpq + * @since 2022/2/8 14:19 + */ +public class RedisConstant { + + /** + * 物模型 + */ + public static final String THING = "thing-demo:thing:"; + + /** + * 物模型版本 + */ + public static final String THING_VERSION = "thing-demo:thing-version:"; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/constant/ThingConstant.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/constant/ThingConstant.java new file mode 100644 index 00000000..19f2bf2b --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/constant/ThingConstant.java @@ -0,0 +1,11 @@ +package com.example.thingdemo.constant; + +/** + * @author chenpq + * @since 2022/2/8 14:19 + */ +public class ThingConstant { + + public static final String VERSION_100 = "1.0.0"; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/constant/TopicConstant.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/constant/TopicConstant.java new file mode 100644 index 00000000..5a2d7e7e --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/constant/TopicConstant.java @@ -0,0 +1,49 @@ +package com.example.thingdemo.constant; + +/** + * @author chenpq + * @since 2022/2/8 14:19 + */ +public class TopicConstant { + + /** + * 设置设备属性,请求topic + */ + public static final String PROPERTY_SET = "sys/${productKey}/${deviceCode}/thing/property/set"; + + /** + * 设置设备属性,响应topic + */ + public static final String PROPERTY_SET_REPLY = "sys/${productKey}/${deviceCode}/thing/property/set_reply"; + + /** + * 设置设备上报,请求topic + */ + public static final String PROPERTY_POST = "sys/${productKey}/${deviceCode}/thing/property/post"; + + /** + * 设置设备上报,响应topic + */ + public static final String PROPERTY_POST_REPLY = "sys/${productKey}/${deviceCode}/thing/property/post_reply"; + + /** + * 事件上报,请求topic + */ + public static final String EVENT_POST = "sys/${productKey}/${deviceCode}/thing/event/${tsl.service.identifier}/post"; + + /** + * 事件上报,响应topic + */ + public static final String EVENT_POST_REPLY = "sys/${productKey}/${deviceCode}/thing/event/${tsl.service.identifier}/post_reply"; + + /** + * 服务调用,请求topic + */ + public static final String SERVICE = "sys/${productKey}/${deviceCode}/thing/service/${tsl.service.identifier}"; + + /** + * 服务调用,响应topic + */ + public static final String SERVICE_REPLY = "sys/${productKey}/${deviceCode}/thing/service/${tsl.service.identifier}_reply"; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/DeviceController.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/DeviceController.java new file mode 100644 index 00000000..0b0ea8fb --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/DeviceController.java @@ -0,0 +1,68 @@ +package com.example.thingdemo.controller; + +import com.example.thingdemo.ao.DeviceAddUpdateAo; +import com.example.thingdemo.common.Result; +import com.example.thingdemo.dto.DeviceDetailDto; +import com.example.thingdemo.service.DeviceService; +import com.example.thingdemo.vo.DeviceAddVo; +import com.example.thingdemo.vo.DevicePropertyUpdateVo; +import com.example.thingdemo.vo.DeviceUpdateVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + *

+ * 设备 前端控制器 + *

+ * + * @author chenpq + * @since 2023-05-07 21:52:39 + */ +@Slf4j +@RestController +@RequestMapping("/device") +@Api(value = "设备", tags = "设备-API") +public class DeviceController { + + @Autowired + private DeviceService deviceService; + + @PostMapping("/add") + @ApiOperation(value = "新增", notes = "返回id") + public Result add(@RequestBody @Valid DeviceAddVo addVo) { + DeviceAddUpdateAo addAo = new DeviceAddUpdateAo(); + BeanUtils.copyProperties(addVo, addAo); + Long id = deviceService.add(addAo); + return Result.success(id); + } + + @PostMapping("/update") + @ApiOperation(value = "修改") + public Result update(@RequestBody @Valid DeviceUpdateVo updateVo) { + DeviceAddUpdateAo updateAo = new DeviceAddUpdateAo(); + BeanUtils.copyProperties(updateVo, updateAo); + boolean b = deviceService.update(updateAo); + return Result.success(b); + } + + @DeleteMapping("/delete/{id}") + @ApiOperation(value = "删除", notes = "返回是否成功") + public Result delete(@PathVariable("id") Long id) { + boolean b = deviceService.delete(id); + return Result.success(b); + } + + @GetMapping("/detail/{id}") + @ApiOperation(value = "详情") + public Result detail(@PathVariable("id") Long id) { + DeviceDetailDto detail = deviceService.detail(id); + return Result.success(detail); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/DevicePropertiesShadowController.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/DevicePropertiesShadowController.java new file mode 100644 index 00000000..1fc5deff --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/DevicePropertiesShadowController.java @@ -0,0 +1,41 @@ +package com.example.thingdemo.controller; + +import com.example.thingdemo.common.Result; +import com.example.thingdemo.service.DevicePropertiesShadowService; +import com.example.thingdemo.vo.DevicePropertyUpdateVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +/** + *

+ * 设备属性影子 前端控制器 + *

+ * + * @author chenpq + * @since 2023-05-07 21:53:04 + */ +@Slf4j +@RestController +@RequestMapping("/device-properties-shadow") +@Api(value = "设备属性影子", tags = "设备属性影子-API") +public class DevicePropertiesShadowController { + + @Autowired + private DevicePropertiesShadowService devicePropertiesShadowService; + + @PostMapping("/update/expect-value") + @ApiOperation(value = "修改") + public Result update(@RequestBody @Valid DevicePropertyUpdateVo updateVo) { + boolean b = devicePropertiesShadowService.updateExpectValue(updateVo); + return Result.success(b); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/TestController.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/TestController.java new file mode 100644 index 00000000..bc9f597c --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/TestController.java @@ -0,0 +1,48 @@ +package com.example.thingdemo.controller; + +import com.example.thingdemo.cache.ThingCache; +import com.example.thingdemo.common.Result; +import com.example.thingdemo.mqtt.MqttProviderConfig; +import com.example.thingdemo.mqtt.MqttSender; +import com.example.thingdemo.mqtt.protocol.ThingResp; +import com.example.thingdemo.service.ThingService; +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.RestController; + +@Slf4j +@RestController +@RequestMapping("/test") +public class TestController { + + @Autowired + private MqttProviderConfig providerClient; + @Autowired + private ThingService thingService; + @Autowired + private MqttSender mqttSender; + + @GetMapping("/mqtt/sendMessage") + public Result sendMessage(int qos, String topic, String message) { + providerClient.publish(qos, topic, message); + return Result.success(); + } + + @GetMapping("/thing/cache") + public Result cache(String productKey) { + return Result.success(thingService.getThingCache(productKey)); + } + + @GetMapping("/thing/property/post/reply") + public Result propertyPostReply() { + ThingResp thingResp = new ThingResp(); + thingResp.setId("2142343"); + thingResp.setCode("200"); + thingResp.setVersion("1.0.0"); + mqttSender.propertyPostReply("83d011f5ac854aba9f5e09874b7dec20", "dev011", thingResp); + return Result.success(); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/ThingController.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/ThingController.java new file mode 100644 index 00000000..2744e800 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/ThingController.java @@ -0,0 +1,73 @@ +package com.example.thingdemo.controller; + +import com.example.thingdemo.ao.ThingAddUpdateAo; +import com.example.thingdemo.common.Result; +import com.example.thingdemo.constant.ThingConstant; +import com.example.thingdemo.dto.ThingDetailDto; +import com.example.thingdemo.service.ThingService; +import com.example.thingdemo.vo.ThingAddVo; +import com.example.thingdemo.vo.ThingUpdateVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + *

+ * 物模型概述 前端控制器 + *

+ * + * @author chenpq + * @since 2023-05-04 14:08:27 + */ +@Slf4j +@RestController +@RequestMapping("/thing") +@Api(value = "产品", tags = "产品-API") +public class ThingController { + + @Autowired + private ThingService thingService; + + @PostMapping("/add") + @ApiOperation(value = "新增", notes = "返回id") + public Result add(@RequestBody @Valid ThingAddVo addVo) { + ThingAddUpdateAo addAo = new ThingAddUpdateAo(); + BeanUtils.copyProperties(addVo, addAo); + if (StringUtils.isBlank(addAo.getVersion())) { + addAo.setVersion(ThingConstant.VERSION_100); + } + Long id = thingService.addUpdate(addAo); + return Result.success(id); + } + + @PostMapping("/update") + @ApiOperation(value = "修改", notes = "返回id") + public Result update(@RequestBody @Valid ThingUpdateVo updateVo) { + ThingAddUpdateAo updateAo = new ThingAddUpdateAo(); + BeanUtils.copyProperties(updateVo, updateAo); + Long id = thingService.addUpdate(updateAo); + return Result.success(id); + } + + @DeleteMapping("/delete/{id}") + @ApiOperation(value = "删除", notes = "返回是否成功") + public Result delete(@PathVariable("id") Long id) { + boolean b = thingService.delete(id); + return Result.success(b); + } + + @GetMapping("/detail/{id}") + @ApiOperation(value = "详情") + public Result detail(@PathVariable("id") Long id) { + ThingDetailDto detail = thingService.detail(id); + return Result.success(detail); + } + + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/ThingDimensionController.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/ThingDimensionController.java new file mode 100644 index 00000000..b86e6df8 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/controller/ThingDimensionController.java @@ -0,0 +1,77 @@ +package com.example.thingdemo.controller; + +import com.example.thingdemo.common.Result; +import com.example.thingdemo.dto.ThingDimensionDetailDto; +import com.example.thingdemo.exception.AppException; +import com.example.thingdemo.service.ThingDimensionService; +import com.example.thingdemo.vo.DimensionAddVo; +import com.example.thingdemo.vo.DimensionBatchAddVo; +import com.example.thingdemo.vo.DimensionUpdateVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * 物模型3维度类型,1(properties),2(event),3(action) 前端控制器 + *

+ * + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +@Slf4j +@RestController +@RequestMapping("/thing-dimension") +@Api(value = "物模型3维度", tags = "物模型3维度-API") +public class ThingDimensionController { + + @Autowired + private ThingDimensionService thingDimensionService; + + @PostMapping("/add/batch") + @ApiOperation(value = "新增(多个)") + public Result add(@RequestBody @Valid DimensionBatchAddVo addVo) { + List<@Valid DimensionAddVo> dimensionList = addVo.getDimensionList(); + for (DimensionAddVo dimensionAddVo : dimensionList) { + thingDimensionService.valid(dimensionAddVo); + } + ArrayList notRepeatList = new ArrayList<>(); + for (DimensionAddVo vo : dimensionList) { + String notRepeat = vo.getDimension() + "-" + vo.getIdentifier(); + if (notRepeatList.contains(notRepeat)) { + throw new AppException(vo.getIdentifier() + "重复,请修改。"); + } + notRepeatList.add(notRepeat); + } + thingDimensionService.addBatch(addVo.getThingId(), addVo.getDimensionList()); + return Result.success(); + } + + @PostMapping("/update") + @ApiOperation(value = "修改单个维度") + public Result update(@RequestBody @Valid DimensionUpdateVo updateVo) { + boolean b = thingDimensionService.update(updateVo); + return Result.success(b); + } + + @DeleteMapping("/delete") + @ApiOperation(value = "删除", notes = "返回是否成功") + public Result delete(@RequestParam("thingId") Long thingId, @RequestParam("id") Long id) { + boolean b = thingDimensionService.delete(thingId, id); + return Result.success(b); + } + + @GetMapping("/detail") + @ApiOperation(value = "详情") + public Result detail(@RequestParam("thingId") Long thingId, @RequestParam("id") Long id) { + ThingDimensionDetailDto detail = thingDimensionService.detail(thingId, id); + return Result.success(detail); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/DeviceEntity.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/DeviceEntity.java new file mode 100644 index 00000000..07ca8214 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/DeviceEntity.java @@ -0,0 +1,62 @@ +package com.example.thingdemo.domain; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * 设备 + *

+ * + * @author chenpq + * @since 2023-05-07 21:52:39 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@TableName("device") +@ApiModel(value = "DeviceEntity对象", description = "设备") +public class DeviceEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty(value = "设备名称") + private String deviceName; + + @ApiModelProperty(value = "设备编码") + private String deviceCode; + + @ApiModelProperty(value = "产品key") + private String productKey; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户id") + private Long createUser; + + @ApiModelProperty(value = "更新时间") + private LocalDateTime updateTime; + + @ApiModelProperty(value = "更新用户id") + private Long updateUser; + + @ApiModelProperty(value = "是否删除,0未删除,1删除") + @TableLogic + private Integer isDel; + + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/DevicePropertiesShadowEntity.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/DevicePropertiesShadowEntity.java new file mode 100644 index 00000000..fb89e2f8 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/DevicePropertiesShadowEntity.java @@ -0,0 +1,71 @@ +package com.example.thingdemo.domain; + +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * 设备属性影子 + *

+ * + * @author chenpq + * @since 2023-05-07 21:53:04 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@TableName("device_properties_shadow") +@ApiModel(value = "DevicePropertiesShadowEntity对象", description = "设备属性影子") +public class DevicePropertiesShadowEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty(value = "产品key") + private String productKey; + + @ApiModelProperty(value = "设备编码") + private String deviceCode; + + @ApiModelProperty(value = "properties唯一标识符") + private String identifier; + + @ApiModelProperty(value = "设备实际值") + private String currentValue; + + @ApiModelProperty(value = "期望值") + private String expectValue; + + @ApiModelProperty(value = "值数据类型") + private String valueDataType; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户id") + private Long createUser; + + @ApiModelProperty(value = "更新时间") + private LocalDateTime updateTime; + + @ApiModelProperty(value = "更新用户id") + private Long updateUser; + + @ApiModelProperty(value = "是否删除,0未删除,1删除") + @TableLogic + private Integer isDel; + + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/ThingDimensionEntity.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/ThingDimensionEntity.java new file mode 100644 index 00000000..12085a1d --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/ThingDimensionEntity.java @@ -0,0 +1,83 @@ +package com.example.thingdemo.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * 物模型3维度类型,1(properties),2(event),3(action) + *

+ * + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@TableName("thing_dimension") +@ApiModel(value = "ThingDimensionEntity对象", description = "物模型3维度类型,1(properties),2(event),3(action)") +public class ThingDimensionEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty(value = "物模型id") + private Long thingId; + + @ApiModelProperty(value = "物模型维度名称") + private String name; + + @ApiModelProperty(value = "物模型维度描述") + private String description; + + @ApiModelProperty(value = "维度类型,1(properties),2(event),3(action)") + private Integer dimension; + + @ApiModelProperty(value = "唯一标识符") + private String identifier; + + @ApiModelProperty(value = "属性读写类型:只读(r)或读写(rw)") + private String propertiesAccessMode; + + @ApiModelProperty(value = "action调用方式,1(同步调用)或2(异步调用)") + private Integer actionCallType; + + @ApiModelProperty(value = "事件类型 1信息,2告警,3故障") + private Integer eventType; + + @ApiModelProperty(value = "是否必选:是1,否0") + private Integer required; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户id") + private Long createUser; + + @ApiModelProperty(value = "更新时间") + private LocalDateTime updateTime; + + @ApiModelProperty(value = "更新用户id") + private Long updateUser; + + @ApiModelProperty(value = "是否删除,0未删除,1删除") + @TableLogic + private Integer isDel; + + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/ThingEntity.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/ThingEntity.java new file mode 100644 index 00000000..4614303f --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/ThingEntity.java @@ -0,0 +1,65 @@ +package com.example.thingdemo.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * 物模型概述 + *

+ * + * @author chenpq + * @since 2023-05-04 14:08:27 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@TableName("thing") +@ApiModel(value = "ThingEntity对象", description = "物模型概述") +public class ThingEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty(value = "产品名称") + private String name; + + @ApiModelProperty(value = "产品key") + private String productKey; + + @ApiModelProperty(value = "版本") + private String version; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户id") + private Long createUser; + + @ApiModelProperty(value = "更新时间") + private LocalDateTime updateTime; + + @ApiModelProperty(value = "更新用户id") + private Long updateUser; + + @ApiModelProperty(value = "是否删除,0未删除,1删除") + @TableLogic + private Integer isDel; + + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/ThingParamSpecEntity.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/ThingParamSpecEntity.java new file mode 100644 index 00000000..e746c910 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/domain/ThingParamSpecEntity.java @@ -0,0 +1,86 @@ +package com.example.thingdemo.domain; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + *

+ * 物模型3维度数据规格 + *

+ * + * @author chenpq + * @since 2023-05-05 17:00:52 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@TableName("thing_param_spec") +@ApiModel(value = "ThingParamSpecEntity对象", description = "物模型3维度数据规格") +public class ThingParamSpecEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @ApiModelProperty(value = "物模型id") + private Long thingId; + + @ApiModelProperty(value = "物模型维度id") + private Long thingDimensionId; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符") + private String paramIdentifier; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符名称") + private String paramIdentifierName; + + @ApiModelProperty(value = "父id。struct结构体的键的父id是struct。[{}]数组元素struct,struct的键的父id是数组。[]数组元素不是struct,数组元素没有父id。") + private Long parentId; + + @ApiModelProperty(value = "树id层级路径") + private String treePath; + + @ApiModelProperty(value = "1 inputData,2 outputData") + private Integer inOutData; + + @ApiModelProperty(value = "数据类型: int32(原生)、float(原生)、double(原生)、text(原生)、date(String类型UTC毫秒)、bool(0或1的int类型)、enum(int类型,枚举项定义方法与bool类型定义0和1的方法相同)、struct(结构体类型,可包含前面7种类型,下面使用\"specs\":[{}]描述包含的对象)、array(数组类型,支持int、double、float、text、struct)") + private String dataType; + + @ApiModelProperty(value = "数据规格 { \"min\": \"参数最小值(int、float、double类型特有)。\", \"max\": \"参数最大值(int、float、double类型特有)。\", \"unit\": \"属性单位(int、float、double类型特有,非必填)。\", \"unitName\": \"单位名称(int、float、double类型特有,非必填)。\", \"size\": \"数组元素的个数,最大512(array类型特有)。\", \"step\": \"步长(text、enum类型无此参数)。\", \"length\": \"数据长度,最大10240(text类型特有)。\", \"0\": \"0的值(bool类型特有)。\", \"1\": \"1的值(bool类型特有)。\", \"arrayItemType\": \"数组元素的类型(array类型特有)。\" }") + private String specs; + + @ApiModelProperty(value = "排序,小的在前面") + private Integer sort; + + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + @ApiModelProperty(value = "创建用户id") + private Long createUser; + + @ApiModelProperty(value = "更新时间") + private LocalDateTime updateTime; + + @ApiModelProperty(value = "更新用户id") + private Long updateUser; + + @ApiModelProperty(value = "是否删除,0未删除,1删除") + @TableLogic + private Integer isDel; + + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/DeviceDetailDto.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/DeviceDetailDto.java new file mode 100644 index 00000000..50eeba10 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/DeviceDetailDto.java @@ -0,0 +1,51 @@ +package com.example.thingdemo.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author chenpq + * @since 2023-05-07 21:52:39 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "设备-详情返回值") +public class DeviceDetailDto implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + private Long id; + + @ApiModelProperty(value = "设备名称") + private String deviceName; + + @ApiModelProperty(value = "设备编码") + private String deviceCode; + + @ApiModelProperty(value = "产品key") + private String productKey; + + @ApiModelProperty(value = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime createTime; + + @ApiModelProperty(value = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime updateTime; + + @ApiModelProperty(value = "设备属性影子属性") + private List shadowPropertiesList; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/DevicePropertiesShadowDto.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/DevicePropertiesShadowDto.java new file mode 100644 index 00000000..b4a97402 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/DevicePropertiesShadowDto.java @@ -0,0 +1,47 @@ +package com.example.thingdemo.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author chenpq + * @since 2023-05-07 21:53:04 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "设备属性影子-详情返回值") +public class DevicePropertiesShadowDto implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "properties唯一标识符") + private String identifier; + + @ApiModelProperty(value = "设备实际值") + private String currentValue; + + @ApiModelProperty(value = "期望值") + private String expectValue; + + @ApiModelProperty(value = "值数据类型") + private String valueDataType; + + @ApiModelProperty(value = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime createTime; + + @ApiModelProperty(value = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime updateTime; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingDetailDto.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingDetailDto.java new file mode 100644 index 00000000..9e03a7b2 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingDetailDto.java @@ -0,0 +1,50 @@ +package com.example.thingdemo.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author chenpq + * @since 2023-05-04 14:08:27 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "物模型概述-详情返回值") +public class ThingDetailDto implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "物模型id") + private Long id; + + @ApiModelProperty(value = "产品名称") + private String name; + + @ApiModelProperty(value = "产品key") + private String productKey; + + @ApiModelProperty(value = "版本") + private String version; + + @ApiModelProperty(value = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime createTime; + + @ApiModelProperty(value = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime updateTime; + + private List dimensionList; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingDimensionDetailDto.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingDimensionDetailDto.java new file mode 100644 index 00000000..653eae2c --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingDimensionDetailDto.java @@ -0,0 +1,69 @@ +package com.example.thingdemo.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "物模型3维度类型,1(properties),2(event),3(action)-详情返回值") +public class ThingDimensionDetailDto implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + private Long id; + + @ApiModelProperty(value = "物模型id") + private Long thingId; + + @ApiModelProperty(value = "物模型维度名称") + private String name; + + @ApiModelProperty(value = "物模型维度描述") + private String description; + + @ApiModelProperty(value = "维度类型,1(properties),2(event),3(action)") + private Integer dimension; + + @ApiModelProperty(value = "唯一标识符") + private String identifier; + + @ApiModelProperty(value = "属性读写类型:只读(r)或读写(rw)") + private String propertiesAccessMode; + + @ApiModelProperty(value = "action调用方式,1(同步调用)或2(异步调用)") + private Integer actionCallType; + + @ApiModelProperty(value = "事件类型 1信息,2告警,3故障") + private Integer eventType; + + @ApiModelProperty(value = "是否必选:是1,否0") + private Integer required; + + @ApiModelProperty(value = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime createTime; + + @ApiModelProperty(value = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime updateTime; + + @ApiModelProperty(value = "数据规则") + private List paramSpecList; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingParamSpecDetailDto.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingParamSpecDetailDto.java new file mode 100644 index 00000000..dae12288 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingParamSpecDetailDto.java @@ -0,0 +1,70 @@ +package com.example.thingdemo.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author chenpq + * @since 2023-05-05 17:00:52 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "物模型3维度数据规格-详情返回值") +public class ThingParamSpecDetailDto implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + private Long id; + + @ApiModelProperty(value = "物模型id") + private Long thingId; + + @ApiModelProperty(value = "物模型维度id") + private Long thingDimensionId; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符") + private String paramIdentifier; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符名称") + private String paramIdentifierName; + + @ApiModelProperty(value = "父id。struct结构体的键的父id是struct。[{}]数组元素struct,struct的键的父id是数组。[]数组元素不是struct,数组元素没有父id。") + private Long parentId; + + @ApiModelProperty(value = "1 inputData,2 outputData") + private Integer inOutData; + + @ApiModelProperty(value = "数据类型: int32(原生)、float(原生)、double(原生)、text(原生)、date(String类型UTC毫秒)、bool(0或1的int类型)、enum(int类型,枚举项定义方法与bool类型定义0和1的方法相同)、struct(结构体类型,可包含前面7种类型,下面使用\"specs\":[{}]描述包含的对象)、array(数组类型,支持int、double、float、text、struct)") + private String dataType; + + @ApiModelProperty(value = "数据规格 { \"min\": \"参数最小值(int、float、double类型特有)。\", \"max\": \"参数最大值(int、float、double类型特有)。\", \"unit\": \"属性单位(int、float、double类型特有,非必填)。\", \"unitName\": \"单位名称(int、float、double类型特有,非必填)。\", \"size\": \"数组元素的个数,最大512(array类型特有)。\", \"step\": \"步长(text、enum类型无此参数)。\", \"length\": \"数据长度,最大10240(text类型特有)。\", \"0\": \"0的值(bool类型特有)。\", \"1\": \"1的值(bool类型特有)。\", \"arrayItemType\": \"数组元素的类型(array类型特有)。\" }") + private String specs; + + @ApiModelProperty(value = "排序,小的在前面") + private Integer sort; + + @ApiModelProperty(value = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime createTime; + + @ApiModelProperty(value = "更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime updateTime; + + + @ApiModelProperty(value = "json对象key的数据规格") + private List jsonElemList; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingParamSpecJsonElemDto.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingParamSpecJsonElemDto.java new file mode 100644 index 00000000..759b1459 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/dto/ThingParamSpecJsonElemDto.java @@ -0,0 +1,42 @@ +package com.example.thingdemo.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; + +/** + * @author chenpq + * @since 2023-05-05 17:00:52 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "物模型3维度数据规格-详情返回值") +public class ThingParamSpecJsonElemDto implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符") + private String paramIdentifier; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符名称") + private String paramIdentifierName; + + @ApiModelProperty(value = "数据类型: int32(原生)、float(原生)、double(原生)、text(原生)、date(String类型UTC毫秒)、bool(0或1的int类型)、enum(int类型,枚举项定义方法与bool类型定义0和1的方法相同)、struct(结构体类型,可包含前面7种类型,下面使用\"specs\":[{}]描述包含的对象)、array(数组类型,支持int、double、float、text、struct)") + @NotEmpty(message = "数据类型不能为空") + private String dataType; + + @ApiModelProperty(value = "数据规格 { \"min\": \"参数最小值(int、float、double类型特有)。\", \"max\": \"参数最大值(int、float、double类型特有)。\", \"unit\": \"属性单位(int、float、double类型特有,非必填)。\", \"unitName\": \"单位名称(int、float、double类型特有,非必填)。\", \"size\": \"数组元素的个数,最大512(array类型特有)。\", \"step\": \"步长(text、enum类型无此参数)。\", \"length\": \"数据长度,最大10240(text类型特有)。\", \"0\": \"0的值(bool类型特有)。\", \"1\": \"1的值(bool类型特有)。\", \"arrayItemType\": \"数组元素的类型(array类型特有)。\" }") + private String specs; + + @ApiModelProperty(value = "排序,小的在前面") + private Integer sort; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/enums/DataTypeEnum.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/enums/DataTypeEnum.java new file mode 100644 index 00000000..d5830706 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/enums/DataTypeEnum.java @@ -0,0 +1,65 @@ +package com.example.thingdemo.enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author chenpq05 + * @since 2022-02-09 + */ +public enum DataTypeEnum { + + /** + * thing_data_spec#data_type + * 数据类型: int32(原生)、float(原生)、double(原生)、text(原生)、date(String类型UTC毫秒)、bool(0或1的int类型)、enum(int类型,枚举项定义方法与bool类型定义0和1的方法相同)、struct(结构体类型,可包含前面7种类型,下面使用"specs":[{}]描述包含的对象)、array(数组类型,支持int、double、float、text、struct) + */ + ARRAY("array", "数组"), + STRUCT("struct", "结构体"), + ; + + private final String code; + + private final String des; + + DataTypeEnum(String code, String des) { + this.code = code; + this.des = des; + } + + public String getCode() { + return code; + } + + public String getDes() { + return des; + } + + /** + * 获取code、des所有键值对 + * + * @return Map + */ + public static Map getCodeDes() { + HashMap map = new HashMap<>(); + DataTypeEnum[] values = DataTypeEnum.values(); + for (DataTypeEnum elem : values) { + map.put(elem.getCode(), elem.getDes()); + } + return map; + } + + /** + * 通过code获取des + * + * @param code code + * @return des + */ + public static String getDesByCode(String code) { + if (code == null) { + return ""; + } + Map map = getCodeDes(); + return map.get(code); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/enums/DimensionEnum.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/enums/DimensionEnum.java new file mode 100644 index 00000000..29da5787 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/enums/DimensionEnum.java @@ -0,0 +1,66 @@ +package com.example.thingdemo.enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author chenpq05 + * @since 2022-02-09 + */ +public enum DimensionEnum { + + /** + * thing_dimension#dimension + * 维度类型,1(properties),2(event),3(action) + */ + PROPERTIES(1, "属性"), + EVENT(2, "事件"), + ACTION(3, "动作"), + ; + + private final Integer code; + + private final String des; + + DimensionEnum(Integer code, String des) { + this.code = code; + this.des = des; + } + + public Integer getCode() { + return code; + } + + public String getDes() { + return des; + } + + /** + * 获取code、des所有键值对 + * + * @return Map + */ + public static Map getCodeDes() { + HashMap map = new HashMap<>(); + DimensionEnum[] values = DimensionEnum.values(); + for (DimensionEnum elem : values) { + map.put(elem.getCode(), elem.getDes()); + } + return map; + } + + /** + * 通过code获取des + * + * @param code code + * @return des + */ + public static String getDesByCode(Integer code) { + if (code == null) { + return ""; + } + Map map = getCodeDes(); + return map.get(code); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/enums/InoutDataEnum.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/enums/InoutDataEnum.java new file mode 100644 index 00000000..9269989b --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/enums/InoutDataEnum.java @@ -0,0 +1,65 @@ +package com.example.thingdemo.enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author chenpq05 + * @since 2022-02-09 + */ +public enum InoutDataEnum { + + /** + * thing_param_spec#in_out_data + * 1 inputData,2 outputData + */ + INPUT_DATA(1, "inputData"), + OUTPUT_DATA(2, "outputData"), + ; + + private final Integer code; + + private final String des; + + InoutDataEnum(Integer code, String des) { + this.code = code; + this.des = des; + } + + public Integer getCode() { + return code; + } + + public String getDes() { + return des; + } + + /** + * 获取code、des所有键值对 + * + * @return Map + */ + public static Map getCodeDes() { + HashMap map = new HashMap<>(); + InoutDataEnum[] values = InoutDataEnum.values(); + for (InoutDataEnum elem : values) { + map.put(elem.getCode(), elem.getDes()); + } + return map; + } + + /** + * 通过code获取des + * + * @param code code + * @return des + */ + public static String getDesByCode(Integer code) { + if (code == null) { + return ""; + } + Map map = getCodeDes(); + return map.get(code); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/exception/AppException.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/exception/AppException.java new file mode 100644 index 00000000..15df6b7a --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/exception/AppException.java @@ -0,0 +1,24 @@ +package com.example.thingdemo.exception; + + +public class AppException extends RuntimeException { + + private final Integer code; + private final String message; + + public AppException(String message) { + super(message); + this.code = 400; + this.message = message; + } + + public Integer getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } + +} \ No newline at end of file diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DeviceMapper.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DeviceMapper.java new file mode 100644 index 00000000..f3c9fed1 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DeviceMapper.java @@ -0,0 +1,16 @@ +package com.example.thingdemo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.thingdemo.domain.DeviceEntity; + +/** + *

+ * 设备 Mapper 接口 + *

+ * + * @author chenpq + * @since 2023-05-07 21:52:39 + */ +public interface DeviceMapper extends BaseMapper { + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DeviceMapper.xml b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DeviceMapper.xml new file mode 100644 index 00000000..e1b4bee1 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DeviceMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + id + , device_name, device_code, product_key, create_time, create_user, update_time, update_user, is_del + + + diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DevicePropertiesShadowMapper.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DevicePropertiesShadowMapper.java new file mode 100644 index 00000000..98aa9b1e --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DevicePropertiesShadowMapper.java @@ -0,0 +1,16 @@ +package com.example.thingdemo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.thingdemo.domain.DevicePropertiesShadowEntity; + +/** + *

+ * 设备属性影子 Mapper 接口 + *

+ * + * @author chenpq + * @since 2023-05-07 21:53:04 + */ +public interface DevicePropertiesShadowMapper extends BaseMapper { + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DevicePropertiesShadowMapper.xml b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DevicePropertiesShadowMapper.xml new file mode 100644 index 00000000..79bd4c09 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/DevicePropertiesShadowMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + id + , product_key, device_code, identifier, current_value, expect_value, value_data_type, create_time, create_user, update_time, update_user, is_del + + + diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingDimensionMapper.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingDimensionMapper.java new file mode 100644 index 00000000..4e4df06d --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingDimensionMapper.java @@ -0,0 +1,22 @@ +package com.example.thingdemo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.thingdemo.domain.ThingDimensionEntity; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + *

+ * 物模型3维度类型,1(properties),2(event),3(action) Mapper 接口 + *

+ * + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +public interface ThingDimensionMapper extends BaseMapper { + + List getDimensions(@Param("productKey") String productKey, + @Param("dimension") Integer dimension); + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingDimensionMapper.xml b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingDimensionMapper.xml new file mode 100644 index 00000000..2c34cf2b --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingDimensionMapper.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + id, thing_id, name, description, dimension, identifier, properties_access_mode, action_call_type, event_type, required, create_time, create_user, update_time, update_user, is_del + + + + + + diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingMapper.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingMapper.java new file mode 100644 index 00000000..5fd5f783 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingMapper.java @@ -0,0 +1,16 @@ +package com.example.thingdemo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.thingdemo.domain.ThingEntity; + +/** + *

+ * 物模型概述 Mapper 接口 + *

+ * + * @author chenpq + * @since 2023-05-04 14:08:27 + */ +public interface ThingMapper extends BaseMapper { + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingMapper.xml b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingMapper.xml new file mode 100644 index 00000000..3d198752 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + id, name, product_key, version, create_time, create_user, update_time, update_user, is_del + + + + diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingParamSpecMapper.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingParamSpecMapper.java new file mode 100644 index 00000000..8640456c --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingParamSpecMapper.java @@ -0,0 +1,17 @@ +package com.example.thingdemo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.thingdemo.domain.ThingParamSpecEntity; + +/** + *

+ * 物模型3维度数据规格 Mapper 接口 + *

+ * + * @author chenpq + * @since 2023-05-05 17:00:52 + */ +public interface ThingParamSpecMapper extends BaseMapper { + + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingParamSpecMapper.xml b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingParamSpecMapper.xml new file mode 100644 index 00000000..61474e5e --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mapper/ThingParamSpecMapper.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + id, thing_id, thing_dimension_id, param_identifier, param_identifier_name, parent_id, tree_path, in_out_data, data_type, specs, sort, create_time, create_user, update_time, update_user, is_del + + + diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttConsumerCallBack.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttConsumerCallBack.java new file mode 100644 index 00000000..3fae5948 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttConsumerCallBack.java @@ -0,0 +1,48 @@ +package com.example.thingdemo.mqtt; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import java.nio.charset.StandardCharsets; + +/** + * @author chenpq05 + * @since 2023/4/11 11:45 + *

+ * mqtt消费者回调 + */ +@Slf4j +public class MqttConsumerCallBack implements MqttCallback { + + /** + * 客户端断开连接的回调 + */ + @Override + public void connectionLost(Throwable throwable) { + log.info("与服务器断开连接", throwable); + } + + /** + * 消息到达的回调 + */ + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + log.info("接收消息主题={},Qos={}, 消息retained={}", topic, message.getQos(), message.isRetained()); + + String payload = new String(message.getPayload(), StandardCharsets.UTF_8); + log.info("接收消息内容={}", payload); + } + + /** + * 消息发布成功回调 + * + * @param iMqttDeliveryToken + */ + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + log.info("接收消息成功"); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttConsumerConfig.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttConsumerConfig.java new file mode 100644 index 00000000..941321a4 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttConsumerConfig.java @@ -0,0 +1,101 @@ +package com.example.thingdemo.mqtt; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +/** + * @author chenpq05 + * @since 2023/4/11 11:43 + *

+ * mqtt消费者配置 + * https://www.cnblogs.com/xct5622/p/15094017.html + */ +@Slf4j +@Configuration +public class MqttConsumerConfig { + + @Autowired + private MqttProperties mqttProperties; + + /** + * 客户端对象 + */ + private MqttClient client; + + /** + * 在bean初始化后连接到服务器 + */ + @PostConstruct + public void init() { + connect(); + } + + /** + * 客户端连接服务端 + */ + public void connect() { + try { + //创建MQTT客户端对象 + client = new MqttClient(mqttProperties.getHostUrl(), mqttProperties.getConsumerClientId(), new MemoryPersistence()); + //连接设置 + MqttConnectOptions options = new MqttConnectOptions(); + //是否清空session,设置为false表示服务器会保留客户端的连接记录,客户端重连之后能获取到服务器在客户端断开连接期间推送的消息 + //设置为true表示每次连接到服务端都是以新的身份 + options.setCleanSession(true); + //设置连接用户名 + options.setUserName(mqttProperties.getUsername()); + //设置连接密码 + options.setPassword(mqttProperties.getPassword().toCharArray()); + //设置超时时间,单位为秒 + options.setConnectionTimeout(mqttProperties.getTimeout()); + //设置心跳时间 单位为秒,表示服务器每隔1.5*20秒的时间向客户端发送心跳判断客户端是否在线 + options.setKeepAliveInterval(mqttProperties.getKeepalive()); + //设置遗嘱消息的话题,若客户端和服务器之间的连接意外断开,服务器将发布客户端的遗嘱信息 + options.setWill("willTopic", (mqttProperties.getConsumerClientId() + "与服务器断开连接").getBytes(), 0, false); + //设置回调 + client.setCallback(new MqttConsumerCallBack()); + client.connect(options); + + subscribe(); + + } catch (MqttException e) { + log.error("MQTT消费者创建异常", e); + } + } + + /** + * 订阅主题 + */ + public void subscribe() { + try { + //订阅主题 + //消息等级,和主题数组一一对应,服务端将按照指定等级给订阅了主题的客户端推送消息 + int[] qos = {1}; + // 监听所有物模型主题 + String[] topics = {"sys/+/+/thing/#"}; + //订阅主题 + client.subscribe(topics, qos); + } catch (MqttException e) { + log.error("MQTT订阅主题异常", e); + } + } + + /** + * 断开连接 + */ + public void disConnect() { + try { + client.disconnect(); + } catch (MqttException e) { + log.error("MQTT断开连接创建异常", e); + } + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttProperties.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttProperties.java new file mode 100644 index 00000000..e48e1950 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttProperties.java @@ -0,0 +1,58 @@ +package com.example.thingdemo.mqtt; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author caich + * @since 2021/9/15 + * mqtt属性 + */ +@Configuration +@ConfigurationProperties("spring.mqtt") +@Data +public class MqttProperties { + + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * 连接地址 + */ + private String hostUrl; + /** + * 生产者客户Id + */ + private String providerClientId; + /** + * 消费者客户Id + */ + private String consumerClientId; + /** + * 超时时间 + */ + private int timeout = 100; + /** + * 保持连接数 + */ + private int keepalive = 60; + + /** + * 入站适配器数量 + */ + //private int inboundCount; + + /** + * 出站适配器数量 + */ + //private int outboundCount; + + //private int triggerCount; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttProviderCallBack.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttProviderCallBack.java new file mode 100644 index 00000000..22753f5a --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttProviderCallBack.java @@ -0,0 +1,60 @@ +package com.example.thingdemo.mqtt; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +/** + * @author chenpq05 + * @since 2023/4/11 14:14 + *

+ * mqtt生产者回调 + */ +@Slf4j +public class MqttProviderCallBack implements MqttCallback { + + + /** + * 与服务器断开连接的回调 + * + * @param throwable + * @return void + * @author xct + * @date 2021/7/30 16:19 + */ + @Override + public void connectionLost(Throwable throwable) { + log.info("与服务器断开连接", throwable); + } + + /** + * 消息到达的回调 + * + * @param s + * @param mqttMessage + * @return void + * @author xct + * @date 2021/7/30 16:19 + */ + @Override + public void messageArrived(String s, MqttMessage mqttMessage) throws Exception { + log.info("消息到达的回调", s); + } + + /** + * 消息发布成功的回调 + * + * @param iMqttDeliveryToken + * @return void + * @author xct + * @date 2021/7/30 16:20 + */ + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + IMqttAsyncClient client = iMqttDeliveryToken.getClient(); + log.info(client.getClientId() + "发布消息成功!"); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttProviderConfig.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttProviderConfig.java new file mode 100644 index 00000000..476be7eb --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttProviderConfig.java @@ -0,0 +1,88 @@ +package com.example.thingdemo.mqtt; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +/** + * @author cpq + * @since 2023-04-11 + * mq连接工厂 + */ +@Slf4j +@Configuration +public class MqttProviderConfig { + + @Autowired + private MqttProperties mqttProperties; + + /** + * 客户端对象 + */ + private MqttClient client; + + /** + * 在bean初始化后连接到服务器 + */ + @PostConstruct + public void init() { + connect(); + } + + public void connect() { + try { + //创建MQTT客户端对象 + client = new MqttClient(mqttProperties.getHostUrl(), mqttProperties.getProviderClientId(), new MemoryPersistence()); + //连接设置 + MqttConnectOptions options = new MqttConnectOptions(); + //是否清空session,设置为false表示服务器会保留客户端的连接记录(订阅主题,qos),客户端重连之后能获取到服务器在客户端断开连接期间推送的消息 + //设置为true表示每次连接到服务端都是以新的身份 + options.setCleanSession(true); + //设置连接用户名 + options.setUserName(mqttProperties.getUsername()); + //设置连接密码 + options.setPassword(mqttProperties.getPassword().toCharArray()); + //设置超时时间,单位为秒 + options.setConnectionTimeout(100); + //设置心跳时间 单位为秒,表示服务器每隔1.5*20秒的时间向客户端发送心跳判断客户端是否在线 + options.setKeepAliveInterval(20); + //设置遗嘱消息的话题,若客户端和服务器之间的连接意外断开,服务器将发布客户端的遗嘱信息 + options.setWill("willTopic", (mqttProperties.getProviderClientId() + "与服务器断开连接").getBytes(), 0, false); + //设置回调 + client.setCallback(new MqttProviderCallBack()); + client.connect(options); + } catch (MqttException e) { + log.info("创建mqtt发布者异常", e); + } + } + + public MqttClient getClient() { + return client; + } + + + public void publish(int qos, String topic, String message) { + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setQos(qos); + mqttMessage.setRetained(false); + mqttMessage.setPayload(message.getBytes()); + //主题目的地,用于发布/订阅消息 + MqttTopic mqttTopic = client.getTopic(topic); + //提供一种机制来跟踪消息的传递进度。 + //用于在以非阻塞方式(在后台运行)执行发布时跟踪消息的传递进度 + MqttDeliveryToken token; + try { + //将指定消息发布到主题,但不等待消息传递完成。返回的token可用于跟踪消息的传递状态。 + //一旦此方法干净地返回,消息就已被客户端接受发布。当连接可用时,将在后台完成消息传递。 + token = mqttTopic.publish(mqttMessage); + token.waitForCompletion(); + } catch (MqttException e) { + log.info("发送mqtt消息异常", e); + } + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttSender.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttSender.java new file mode 100644 index 00000000..6624c208 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/MqttSender.java @@ -0,0 +1,95 @@ +package com.example.thingdemo.mqtt; + +import com.example.thingdemo.constant.TopicConstant; +import com.example.thingdemo.exception.AppException; +import com.example.thingdemo.mqtt.protocol.ThingReq; +import com.example.thingdemo.mqtt.protocol.ThingResp; +import com.example.thingdemo.service.ThingService; +import com.example.thingdemo.util.CommonUtil; +import com.example.thingdemo.util.ObjectMapperUtil; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.MqttTopic; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author cpq + * @since 2023-04-11 + * mq连接工厂 + */ +@Slf4j +@Component +public class MqttSender { + + @Autowired + private MqttProviderConfig mqttProviderConfig; + @Autowired + private ThingService thingService; + + private void publish(int qos, String topic, String message) { + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setQos(qos); + mqttMessage.setRetained(false); + mqttMessage.setPayload(message.getBytes()); + //主题目的地,用于发布/订阅消息 + MqttTopic mqttTopic = mqttProviderConfig.getClient().getTopic(topic); + //提供一种机制来跟踪消息的传递进度。 + //用于在以非阻塞方式(在后台运行)执行发布时跟踪消息的传递进度 + MqttDeliveryToken token; + //将指定消息发布到主题,但不等待消息传递完成。返回的token可用于跟踪消息的传递状态。 + //一旦此方法干净地返回,消息就已被客户端接受发布。当连接可用时,将在后台完成消息传递。 + try { + log.info("发送mqtt消息,topic={}, message={}", topic, message); + token = mqttTopic.publish(mqttMessage); + token.waitForCompletion(1000 * 60); + }catch (MqttException e) { + log.error("", e); + throw new AppException("发送数据到mqtt异常"); + } + + } + + /** + * 设置属性 + * @param productKey + * @param deviceCode + * @param params + * @return 消息id + */ + public String propertySet(String productKey, String deviceCode, Map params) { + String version = thingService.getThingVersionCache(productKey); + String topic = TopicConstant.PROPERTY_SET + .replace("${productKey}", productKey) + .replace("${deviceCode}", deviceCode); + final String id = CommonUtil.uuid32(); + + ThingReq thingReq = new ThingReq(id, version, + params); + String msg = ObjectMapperUtil.writeValueAsString(thingReq); + publish(1, topic, msg); + return id; + } + + /** + * 响应-设备上报 + * @param productKey + * @param deviceCode + * @param thingResp + * @return + */ + public void propertyPostReply(String productKey, String deviceCode, ThingResp thingResp) { + String topic = TopicConstant.PROPERTY_POST_REPLY + .replace("${productKey}", productKey) + .replace("${deviceCode}", deviceCode); + final String id = CommonUtil.uuid32(); + + String msg = ObjectMapperUtil.writeValueAsString(thingResp); + publish(1, topic, msg); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/protocol/ThingReq.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/protocol/ThingReq.java new file mode 100644 index 00000000..6c3501e2 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/protocol/ThingReq.java @@ -0,0 +1,63 @@ +package com.example.thingdemo.mqtt.protocol; + +import java.io.Serializable; + +/** + * @author chenpq05 + * @since 2022/2/23 14:14 + */ +public class ThingReq implements Serializable { + + private static final long serialVersionUID = 1L; + + // 消息id号,用户自定义,String类型的数字,长度限制不超过32位。 + private String id; + + // 物模型版本 + private String version; + + @SuppressWarnings("squid:S1948") + private T params; + + public ThingReq() { + } + + public ThingReq(String id, String version, T params) { + this.id = id; + this.version = version; + this.params = params; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public T getParams() { + return params; + } + + public void setParams(T params) { + this.params = params; + } + + @Override + public String toString() { + return "ThingReq{" + + "id='" + id + '\'' + + ", version='" + version + '\'' + + ", params=" + params + + '}'; + } +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/protocol/ThingResp.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/protocol/ThingResp.java new file mode 100644 index 00000000..0305e750 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/mqtt/protocol/ThingResp.java @@ -0,0 +1,86 @@ +package com.example.thingdemo.mqtt.protocol; + +import java.io.Serializable; + +/** + * @author chenpq05 + * @since 2022/2/23 14:14 + */ +public class ThingResp implements Serializable { + + private static final long serialVersionUID = 1L; + + // 消息id号,用户自定义,String类型的数字,长度限制不超过32位。 + private String id; + + // 200表示成功 + private String code; + + private String message; + + // 物模型版本 + private String version; + + @SuppressWarnings("squid:S1948") + private T data; + + public ThingResp() { + } + + public ThingResp(String code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + @Override + public String toString() { + return "ThingResp{" + + "id='" + id + '\'' + + ", code='" + code + '\'' + + ", message='" + message + '\'' + + ", version='" + version + '\'' + + ", data=" + data + + '}'; + } +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/DevicePropertiesShadowService.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/DevicePropertiesShadowService.java new file mode 100644 index 00000000..8025122a --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/DevicePropertiesShadowService.java @@ -0,0 +1,53 @@ +package com.example.thingdemo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.thingdemo.domain.DevicePropertiesShadowEntity; +import com.example.thingdemo.vo.DevicePropertyUpdateVo; +import com.example.thingdemo.vo.DevicePropertiesShadowInitVo; +import com.example.thingdemo.vo.DevicePropertiesShadowUpdateCurrentVo; + +import java.util.List; + +/** + *

+ * 设备属性影子 服务类 + *

+ * + * @author chenpq + * @since 2023-05-07 21:53:04 + */ +public interface DevicePropertiesShadowService extends IService { + + /** + * 获取设备属性影子 + * + * @param productKey + * @param deviceCode + * @return + */ + List getShadows(String productKey, String deviceCode); + + /** + * 初始化设备属性影子 + * + * @param initVo + */ + void initDevicePropertiesShadow(DevicePropertiesShadowInitVo initVo); + + /** + * 更新期望值 + * + * @param updateVo + * @return + */ + boolean updateExpectValue(DevicePropertyUpdateVo updateVo); + + /** + * 更新实际值 + * + * @param updateVo + * @return + */ + boolean updateCurrentValue(DevicePropertiesShadowUpdateCurrentVo updateVo); + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/DeviceService.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/DeviceService.java new file mode 100644 index 00000000..b8b2a196 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/DeviceService.java @@ -0,0 +1,61 @@ +package com.example.thingdemo.service; + +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.thingdemo.ao.DeviceAddUpdateAo; +import com.example.thingdemo.domain.DeviceEntity; +import com.example.thingdemo.dto.DeviceDetailDto; + +/** + *

+ * 设备 服务类 + *

+ * + * @author chenpq + * @since 2023-05-07 21:52:39 + */ +public interface DeviceService extends IService { + + /** + * 新增 + * + * @param addUpdateAo + * @return + */ + Long add(DeviceAddUpdateAo addUpdateAo); + + /** + * 更新 + * @param addUpdateAo + * @return + */ + boolean update(DeviceAddUpdateAo addUpdateAo); + + /** + * 删除 + * + * @param id id + * @return 是否成功 + */ + boolean delete(Long id); + + /** + * 详情 + * + * @param id id + * @return 详情 + */ + DeviceDetailDto detail(Long id); + + /** + * 是否重复 + * + * @param id + * @param productKey + * @param func + * @param value + * @return + */ + boolean isRepeat(Long id, String productKey, SFunction func, String value); + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/ThingDimensionService.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/ThingDimensionService.java new file mode 100644 index 00000000..c2fdc49f --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/ThingDimensionService.java @@ -0,0 +1,77 @@ +package com.example.thingdemo.service; + +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.thingdemo.domain.ThingDimensionEntity; +import com.example.thingdemo.dto.ThingDimensionDetailDto; +import com.example.thingdemo.vo.DimensionAddVo; +import com.example.thingdemo.vo.DimensionUpdateVo; + +import java.util.List; + +/** + *

+ * 物模型3维度类型,1(properties),2(event),3(action) 服务类 + *

+ * + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +public interface ThingDimensionService extends IService { + + /** + * 批量新增 + * + * @param thingId + * @param dimensionList + */ + void addBatch(Long thingId, List dimensionList); + + /** + * 更新单个维度 + * + * @param updateVo + * @return + */ + boolean update(DimensionUpdateVo updateVo); + + /** + * 参数校验 + * + * @param vo + */ + void valid(DimensionAddVo vo); + + + /** + * 删除 + * + * @param thingId + * @param id + * @return + */ + boolean delete(Long thingId, Long id); + + /** + * 详情 + * + * @param thingId + * @param id + * @return + */ + ThingDimensionDetailDto detail(Long thingId, Long id); + + + /** + * 是否重复 + * + * @param id + * @param thingId + * @param dimension + * @param func + * @param value + * @return + */ + boolean isRepeat(Long id, Long thingId, Integer dimension, SFunction func, String value); + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/ThingParamSpecService.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/ThingParamSpecService.java new file mode 100644 index 00000000..9327b996 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/ThingParamSpecService.java @@ -0,0 +1,37 @@ +package com.example.thingdemo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.thingdemo.domain.ThingParamSpecEntity; +import com.example.thingdemo.dto.ThingParamSpecDetailDto; +import com.example.thingdemo.vo.ThingParamSpecAddUpdateVo; + +import java.util.List; + +/** + *

+ * 物模型3维度数据规格 服务类 + *

+ * + * @author chenpq + * @since 2023-05-05 17:00:52 + */ +public interface ThingParamSpecService extends IService { + + /** + * 新增修改 + * + * @param thingId + * @param thingDimensionId + * @param addList + */ + void addUpdate(Long thingId, Long thingDimensionId, List addList); + + /** + * 详情 + * + * @param id id + * @return 详情 + */ + ThingParamSpecDetailDto detail(Long id); + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/ThingService.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/ThingService.java new file mode 100644 index 00000000..2b554f03 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/ThingService.java @@ -0,0 +1,73 @@ +package com.example.thingdemo.service; + +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.thingdemo.ao.ThingAddUpdateAo; +import com.example.thingdemo.cache.ThingCache; +import com.example.thingdemo.domain.ThingEntity; +import com.example.thingdemo.dto.ThingDetailDto; + +/** + *

+ * 物模型概述 服务类 + *

+ * + * @author chenpq + * @since 2023-05-04 14:08:27 + */ +public interface ThingService extends IService { + + /** + * 新增/修改 + * + * @param addUpdateAo 新增/修改 参数 + * @return id + */ + Long addUpdate(ThingAddUpdateAo addUpdateAo); + + + /** + * 删除 + * + * @param id id + * @return 是否成功 + */ + boolean delete(Long id); + + /** + * 详情 + * + * @param id id + * @return 详情 + */ + ThingDetailDto detail(Long id); + + /** + * 获取物模型缓存 + * @param productKey + * @return + */ + ThingCache getThingCache(String productKey); + + /** + * 删除物模型缓存 + * @param productKey + * @return + */ + boolean removeThingCache(String productKey); + + String getThingVersionCache(String productKey); + + boolean removeThingVersionCache(String productKey); + + /** + * 是否重复 + * + * @param id id + * @param func 列函数 + * @param value 列值 + * @return 是否重复 + */ + boolean isRepeat(Long id, SFunction func, String value); + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/DevicePropertiesShadowServiceImpl.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/DevicePropertiesShadowServiceImpl.java new file mode 100644 index 00000000..e42234f9 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/DevicePropertiesShadowServiceImpl.java @@ -0,0 +1,142 @@ +package com.example.thingdemo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.thingdemo.domain.DeviceEntity; +import com.example.thingdemo.domain.DevicePropertiesShadowEntity; +import com.example.thingdemo.domain.ThingDimensionEntity; +import com.example.thingdemo.domain.ThingParamSpecEntity; +import com.example.thingdemo.enums.DimensionEnum; +import com.example.thingdemo.mapper.DevicePropertiesShadowMapper; +import com.example.thingdemo.mapper.ThingDimensionMapper; +import com.example.thingdemo.mqtt.MqttSender; +import com.example.thingdemo.service.DeviceService; +import com.example.thingdemo.service.DevicePropertiesShadowService; +import com.example.thingdemo.service.ThingParamSpecService; +import com.example.thingdemo.util.CommonUtil; +import com.example.thingdemo.vo.DevicePropertyUpdateVo; +import com.example.thingdemo.vo.DevicePropertiesShadowInitVo; +import com.example.thingdemo.vo.DevicePropertiesShadowUpdateCurrentVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + *

+ * 设备属性影子 服务实现类 + *

+ * + * @author chenpq + * @since 2023-05-07 21:53:04 + */ +@Slf4j +@Service +public class DevicePropertiesShadowServiceImpl extends ServiceImpl implements + DevicePropertiesShadowService { + + @Autowired + private DevicePropertiesShadowMapper devicePropertiesShadowMapper; + @Autowired + private ThingDimensionMapper thingDimensionMapper; + + @Autowired + private DeviceService deviceService; + @Autowired + private ThingParamSpecService thingParamSpecService; + @Autowired + private MqttSender mqttProviderSender; + + @Override + public List getShadows(String productKey, String deviceCode) { + LambdaQueryWrapper lqwShadow = Wrappers.lambdaQuery(); + lqwShadow.eq(DevicePropertiesShadowEntity::getProductKey, productKey); + lqwShadow.eq(DevicePropertiesShadowEntity::getDeviceCode, deviceCode); + return super.list(lqwShadow); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void initDevicePropertiesShadow(DevicePropertiesShadowInitVo initVo) { + LambdaQueryWrapper lqwRemoveOld = Wrappers.lambdaQuery(); + lqwRemoveOld.eq(DevicePropertiesShadowEntity::getProductKey, initVo.getProductKey()); + lqwRemoveOld.eq(DevicePropertiesShadowEntity::getDeviceCode, initVo.getDeviceCode()); + super.remove(lqwRemoveOld); + + List properties = thingDimensionMapper.getDimensions(initVo.getProductKey(), DimensionEnum.PROPERTIES.getCode()); + if (CollectionUtils.isEmpty(properties)) { + return; + } + LambdaQueryWrapper lqwSpecs = Wrappers.lambdaQuery(); + lqwSpecs.eq(ThingParamSpecEntity::getThingId, properties.get(0).getThingId()); + List idList = properties.stream().map(ThingDimensionEntity::getId).collect(Collectors.toList()); + lqwSpecs.in(ThingParamSpecEntity::getThingDimensionId, idList); + List paramSpecList = thingParamSpecService.list(lqwSpecs); + + ArrayList shadowList = new ArrayList<>(); + for (ThingDimensionEntity p : properties) { + DevicePropertiesShadowEntity propertiesShadow = new DevicePropertiesShadowEntity(); + propertiesShadow.setProductKey(initVo.getProductKey()); + propertiesShadow.setDeviceCode(initVo.getDeviceCode()); + propertiesShadow.setIdentifier(p.getIdentifier()); + + paramSpecList.stream() + .filter(e -> Objects.equals(p.getThingId(), e.getThingId()) + && Objects.equals(p.getId(), e.getThingDimensionId())) + .findFirst() + .ifPresent(e -> propertiesShadow.setValueDataType(e.getDataType())); + shadowList.add(propertiesShadow); + } + // 新增 + if (CollectionUtils.isNotEmpty(shadowList)) { + super.saveBatch(shadowList); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateExpectValue(DevicePropertyUpdateVo updateVo) { + DeviceEntity deviceDb = deviceService.getById(updateVo.getDeviceId()); + if (deviceDb == null) { + return false; + } + + LambdaUpdateWrapper lwqUp = Wrappers.lambdaUpdate(); + lwqUp.eq(DevicePropertiesShadowEntity::getProductKey, deviceDb.getProductKey()); + lwqUp.eq(DevicePropertiesShadowEntity::getDeviceCode, deviceDb.getDeviceCode()); + lwqUp.eq(DevicePropertiesShadowEntity::getIdentifier, updateVo.getIdentifier()); + + lwqUp.set(DevicePropertiesShadowEntity::getExpectValue, CommonUtil.objectToString(updateVo.getExpectValue())); + boolean b = super.update(lwqUp); + + // 发送mqtt消息 + HashMap param = new HashMap<>(); + param.put(updateVo.getIdentifier(), updateVo.getExpectValue()); + mqttProviderSender.propertySet(deviceDb.getProductKey(), deviceDb.getDeviceCode(), param); + + return b; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateCurrentValue(DevicePropertiesShadowUpdateCurrentVo updateVo) { + LambdaUpdateWrapper lwqUp = Wrappers.lambdaUpdate(); + lwqUp.eq(DevicePropertiesShadowEntity::getProductKey, updateVo.getProductKey()); + lwqUp.eq(DevicePropertiesShadowEntity::getDeviceCode, updateVo.getDeviceCode()); + lwqUp.eq(DevicePropertiesShadowEntity::getIdentifier, updateVo.getIdentifier()); + + lwqUp.set(DevicePropertiesShadowEntity::getCurrentValue, updateVo.getCurrentValue()); + return super.update(lwqUp); + } + + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/DeviceServiceImpl.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/DeviceServiceImpl.java new file mode 100644 index 00000000..4959eb96 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/DeviceServiceImpl.java @@ -0,0 +1,134 @@ +package com.example.thingdemo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.thingdemo.ao.DeviceAddUpdateAo; +import com.example.thingdemo.domain.DeviceEntity; +import com.example.thingdemo.domain.DevicePropertiesShadowEntity; +import com.example.thingdemo.dto.DeviceDetailDto; +import com.example.thingdemo.dto.DevicePropertiesShadowDto; +import com.example.thingdemo.exception.AppException; +import com.example.thingdemo.mapper.DeviceMapper; +import com.example.thingdemo.service.DeviceService; +import com.example.thingdemo.service.DevicePropertiesShadowService; +import com.example.thingdemo.util.CopyUtils; +import com.example.thingdemo.vo.DevicePropertiesShadowInitVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; + +/** + *

+ * 设备 服务实现类 + *

+ * + * @author chenpq + * @since 2023-05-07 21:52:39 + */ +@Slf4j +@Service +public class DeviceServiceImpl extends ServiceImpl implements DeviceService { + + @Autowired + private DeviceMapper deviceMapper; + @Autowired + private DevicePropertiesShadowService devicePropertiesShadowService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long add(DeviceAddUpdateAo addUpdateAo) { + if (isRepeat(null, addUpdateAo.getProductKey(), + DeviceEntity::getDeviceCode, addUpdateAo.getDeviceCode())) { + throw new AppException("修改失败,设备编码已存在。请修改设备编码。"); + } + if (isRepeat(null, addUpdateAo.getProductKey(), + DeviceEntity::getDeviceName, addUpdateAo.getDeviceName())) { + throw new AppException("修改失败,设备名称已存在。请修改名称编码。"); + } + DeviceEntity deviceEntity = new DeviceEntity(); + BeanUtils.copyProperties(addUpdateAo, deviceEntity); + super.save(deviceEntity); + Long id = deviceEntity.getId(); + + // 初始化设备属性影子 + DevicePropertiesShadowInitVo initVo = new DevicePropertiesShadowInitVo(); + initVo.setProductKey(deviceEntity.getProductKey()); + initVo.setDeviceCode(deviceEntity.getDeviceCode()); + devicePropertiesShadowService.initDevicePropertiesShadow(initVo); + + return id; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean update(DeviceAddUpdateAo addUpdateAo) { + Long id = addUpdateAo.getId(); + DeviceEntity deviceDb = super.getById(id); + if (deviceDb == null) { + return false; + } + if (isRepeat(id, deviceDb.getProductKey(), + DeviceEntity::getDeviceName, deviceDb.getDeviceName())) { + throw new AppException("修改失败,设备名称已存在。请修改名称编码。"); + } + DeviceEntity deviceEntity = new DeviceEntity(); + BeanUtils.copyProperties(addUpdateAo, deviceEntity); + return super.updateById(deviceEntity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean delete(Long id) { + DeviceEntity deviceDb = super.getById(id); + if (deviceDb == null) { + return false; + } + // 删除 + boolean b = super.removeById(id); + + // 删除设备属性影子 + LambdaQueryWrapper lqwShadow = Wrappers.lambdaQuery(); + lqwShadow.eq(DevicePropertiesShadowEntity::getDeviceCode, deviceDb.getDeviceCode()); + lqwShadow.eq(DevicePropertiesShadowEntity::getProductKey, deviceDb.getProductKey()); + devicePropertiesShadowService.remove(lqwShadow); + + return b; + } + + @Override + public DeviceDetailDto detail(Long id) { + DeviceEntity deviceDb = super.getById(id); + if (deviceDb == null) { + return null; + } + DeviceDetailDto detailDto = new DeviceDetailDto(); + BeanUtils.copyProperties(deviceDb, detailDto); + + // 设备属性影子 + List shadowList = devicePropertiesShadowService.getShadows(deviceDb.getProductKey(), + deviceDb.getDeviceCode()); + List shadowDtoList = CopyUtils.listCopy(shadowList, DevicePropertiesShadowDto.class); + detailDto.setShadowPropertiesList(shadowDtoList); + return detailDto; + } + + @Override + public boolean isRepeat(Long id, String productKey, SFunction func, String value) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(DeviceEntity::getIsDel, 0); + lqw.eq(DeviceEntity::getProductKey, productKey); + lqw.eq(func, value); + if (Objects.nonNull(id)) { + lqw.ne(DeviceEntity::getId, id); + } + return super.count(lqw) > 0; + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/ThingDimensionServiceImpl.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/ThingDimensionServiceImpl.java new file mode 100644 index 00000000..81a850d0 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/ThingDimensionServiceImpl.java @@ -0,0 +1,234 @@ +package com.example.thingdemo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.thingdemo.domain.ThingDimensionEntity; +import com.example.thingdemo.domain.ThingEntity; +import com.example.thingdemo.domain.ThingParamSpecEntity; +import com.example.thingdemo.dto.ThingDimensionDetailDto; +import com.example.thingdemo.dto.ThingParamSpecDetailDto; +import com.example.thingdemo.dto.ThingParamSpecJsonElemDto; +import com.example.thingdemo.enums.DimensionEnum; +import com.example.thingdemo.exception.AppException; +import com.example.thingdemo.mapper.ThingDimensionMapper; +import com.example.thingdemo.service.ThingDimensionService; +import com.example.thingdemo.service.ThingParamSpecService; +import com.example.thingdemo.service.ThingService; +import com.example.thingdemo.util.CopyUtils; +import com.example.thingdemo.vo.DimensionAddVo; +import com.example.thingdemo.vo.DimensionUpdateVo; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + *

+ * 物模型3维度类型,1(properties),2(event),3(action) 服务实现类 + *

+ * + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +@Slf4j +@Service +public class ThingDimensionServiceImpl extends ServiceImpl implements ThingDimensionService { + + @Autowired + private ThingDimensionMapper thingDimensionMapper; + @Autowired + private ThingParamSpecService thingParamSpecService; + @Autowired + private ThingService thingService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void addBatch(Long thingId, List dimensionList) { + if (CollectionUtils.isEmpty(dimensionList)) { + throw new AppException("dimensionList不能为空。"); + } + + // 校验Identifier是否重复 + for (DimensionAddVo vo : dimensionList) { + isRepeat(null, thingId, vo.getDimension(), ThingDimensionEntity::getIdentifier, vo.getIdentifier()); + } + + // 新增维度 + for (DimensionAddVo addVo : dimensionList) { + ThingDimensionEntity thingDimensionEntity = new ThingDimensionEntity(); + BeanUtils.copyProperties(addVo, thingDimensionEntity); + thingDimensionEntity.setThingId(thingId); + super.save(thingDimensionEntity); + Long dimensionId = thingDimensionEntity.getId(); + + // 新增数据规格 + thingParamSpecService.addUpdate(thingId, dimensionId, addVo.getParamSpecList()); + } + + ThingEntity thingDb = thingService.getById(thingId); + if (thingDb != null) { + // 删除缓存 + thingService.removeThingCache(thingDb.getProductKey()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean update(DimensionUpdateVo updateVo) { + ThingDimensionEntity dimensionDb = super.getById(updateVo.getId()); + if (dimensionDb == null) { + return false; + } + + isRepeat(updateVo.getId(), dimensionDb.getThingId(), dimensionDb.getDimension(), ThingDimensionEntity::getIdentifier, updateVo.getIdentifier()); + + // 校验 + DimensionAddVo dimensionAddVo = new DimensionAddVo(); + BeanUtils.copyProperties(updateVo, dimensionAddVo); + valid(dimensionAddVo); + + ThingDimensionEntity thingDimensionEntity = new ThingDimensionEntity(); + BeanUtils.copyProperties(updateVo, thingDimensionEntity); + + if (DimensionEnum.PROPERTIES.getCode().equals(thingDimensionEntity.getDimension())) { + thingDimensionEntity.setEventType(null); + thingDimensionEntity.setActionCallType(null); + } + if (DimensionEnum.EVENT.getCode().equals(thingDimensionEntity.getDimension())) { + thingDimensionEntity.setPropertiesAccessMode(""); + thingDimensionEntity.setActionCallType(null); + } + if (DimensionEnum.ACTION.getCode().equals(thingDimensionEntity.getDimension())) { + thingDimensionEntity.setPropertiesAccessMode(""); + thingDimensionEntity.setEventType(null); + } + + super.updateById(thingDimensionEntity); + Long dimensionId = dimensionDb.getId(); + Long thingId = dimensionDb.getThingId(); + + // 新增数据规格 + thingParamSpecService.addUpdate(thingId, dimensionId, updateVo.getParamSpecList()); + + ThingEntity thingDb = thingService.getById(thingId); + if (thingDb != null) { + // 删除缓存 + thingService.removeThingCache(thingDb.getProductKey()); + } + + return true; + } + + @Override + public void valid(DimensionAddVo vo) { + if (vo == null) { + return; + } + if (DimensionEnum.PROPERTIES.getCode().equals(vo.getDimension())) { + if (StringUtils.isBlank(vo.getPropertiesAccessMode())) { + throw new AppException("属性的读写类型不能为空"); + } + vo.setEventType(null); + vo.setActionCallType(null); + } + if (DimensionEnum.EVENT.getCode().equals(vo.getDimension())) { + if (vo.getEventType() == null) { + throw new AppException("事件的事件类型不能为空"); + } + vo.setPropertiesAccessMode(""); + vo.setActionCallType(null); + } + if (DimensionEnum.ACTION.getCode().equals(vo.getDimension())) { + if (vo.getActionCallType() == null) { + throw new AppException("动作的调用方式不能为空"); + } + vo.setPropertiesAccessMode(""); + vo.setEventType(null); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean delete(Long thingId, Long id) { + ThingEntity thingDb = thingService.getById(thingId); + + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(ThingDimensionEntity::getThingId, thingId); + lqw.eq(ThingDimensionEntity::getId, id); + boolean b = super.remove(lqw); + + LambdaQueryWrapper paramSpecLqw = Wrappers.lambdaQuery(); + paramSpecLqw.eq(ThingParamSpecEntity::getThingId, thingId); + paramSpecLqw.eq(ThingParamSpecEntity::getThingDimensionId, id); + thingParamSpecService.remove(paramSpecLqw); + + if (thingDb != null) { + // 删除缓存 + thingService.removeThingCache(thingDb.getProductKey()); + } + + return b; + } + + @Override + public ThingDimensionDetailDto detail(Long thingId, Long id) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(ThingDimensionEntity::getThingId, thingId); + lqw.eq(ThingDimensionEntity::getId, id); + ThingDimensionEntity dbEntity = super.getOne(lqw, false); + if (dbEntity == null) { + return null; + } + ThingDimensionDetailDto detailDto = new ThingDimensionDetailDto(); + BeanUtils.copyProperties(dbEntity, detailDto); + + // 数据规格 + LambdaQueryWrapper paramSpecLqw = Wrappers.lambdaQuery(); + paramSpecLqw.eq(ThingParamSpecEntity::getThingDimensionId, id); + List paramSpecAllDbList = thingParamSpecService.list(paramSpecLqw); + + List parentParamSpecDbList = paramSpecAllDbList.stream() + .filter(e -> e.getParentId() == null).collect(Collectors.toList()); + List childParamSpecDbList = paramSpecAllDbList.stream() + .filter(e -> e.getParentId() != null).collect(Collectors.toList()); + + List parentDetailList = CopyUtils.listCopy(parentParamSpecDbList, + ThingParamSpecDetailDto.class); + // 数据规格子级 + if (CollectionUtils.isNotEmpty(childParamSpecDbList)) { + parentDetailList.forEach(parent -> { + List childList = childParamSpecDbList.stream() + .filter(child -> Objects.equals(parent.getId(), child.getParentId())) + .collect(Collectors.toList()); + List jsonElemList = CopyUtils.listCopy( + childList, ThingParamSpecJsonElemDto.class); + parent.setJsonElemList(jsonElemList); + }); + } + detailDto.setParamSpecList(parentDetailList); + return detailDto; + } + + @Override + public boolean isRepeat(Long id, Long thingId, Integer dimension, SFunction func, String value) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(ThingDimensionEntity::getIsDel, 0); + lqw.eq(func, value); + lqw.ne(ThingDimensionEntity::getThingId, thingId); + lqw.ne(ThingDimensionEntity::getDimension, dimension); + if (Objects.nonNull(id)) { + lqw.ne(ThingDimensionEntity::getId, id); + } + return super.count(lqw) > 0; + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/ThingParamSpecServiceImpl.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/ThingParamSpecServiceImpl.java new file mode 100644 index 00000000..a47ad69f --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/ThingParamSpecServiceImpl.java @@ -0,0 +1,105 @@ +package com.example.thingdemo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.thingdemo.domain.ThingParamSpecEntity; +import com.example.thingdemo.dto.ThingParamSpecDetailDto; +import com.example.thingdemo.mapper.ThingParamSpecMapper; +import com.example.thingdemo.service.ThingParamSpecService; +import com.example.thingdemo.util.CopyUtils; +import com.example.thingdemo.vo.ThingParamSpecAddUpdateVo; +import com.example.thingdemo.vo.ThingParamSpecJsonElemVo; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * 物模型3维度数据规格 服务实现类 + *

+ * + * @author chenpq + * @since 2023-05-05 17:00:52 + */ +@Slf4j +@Service +public class ThingParamSpecServiceImpl extends ServiceImpl implements ThingParamSpecService { + + @Autowired + private ThingParamSpecMapper thingParamSpecMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void addUpdate(Long thingId, Long thingDimensionId, List addList) { + // 删除旧数据 + LambdaQueryWrapper delLqw = Wrappers.lambdaQuery(); + delLqw.eq(ThingParamSpecEntity::getThingId, thingId); + delLqw.eq(ThingParamSpecEntity::getThingDimensionId, thingDimensionId); + super.remove(delLqw); + + if (CollectionUtils.isEmpty(addList)) { + return; + } + + // 无子级的数据 + List addEntityList = new ArrayList<>(); + addList.stream() + .filter(e -> CollectionUtils.isEmpty(e.getJsonElemList())) + .forEach(e -> { + ThingParamSpecEntity addEntity = new ThingParamSpecEntity(); + BeanUtils.copyProperties(e, addEntity); + addEntity.setThingId(thingId); + addEntity.setThingDimensionId(thingDimensionId); + addEntity.setThingDimensionId(thingDimensionId); + addEntityList.add(addEntity); + }); + if (CollectionUtils.isNotEmpty(addEntityList)) { + super.saveBatch(addEntityList); + } + + // 有子级的数据 + addList.stream() + .filter(e -> CollectionUtils.isNotEmpty(e.getJsonElemList())) + .forEach(hasChild -> { + // 保存父级 + ThingParamSpecEntity parent = new ThingParamSpecEntity(); + BeanUtils.copyProperties(hasChild, parent); + parent.setThingId(thingId); + parent.setThingDimensionId(thingDimensionId); + super.save(parent); + + // 保存子级 + List jsonList = hasChild.getJsonElemList(); + List childList = CopyUtils.listCopy(jsonList, + ThingParamSpecEntity.class); + childList.forEach(child -> { + child.setThingId(thingId); + child.setThingDimensionId(thingDimensionId); + child.setParentId(parent.getId()); + }); + if (CollectionUtils.isNotEmpty(childList)) { + super.saveBatch(childList); + } + }); + } + + @Override + public ThingParamSpecDetailDto detail(Long id) { + ThingParamSpecEntity thingParamSpecEntity = super.getById(id); + if (thingParamSpecEntity == null) { + return null; + } + ThingParamSpecDetailDto detailDto = new ThingParamSpecDetailDto(); + BeanUtils.copyProperties(thingParamSpecEntity, detailDto); + return detailDto; + } + + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/ThingServiceImpl.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/ThingServiceImpl.java new file mode 100644 index 00000000..c769637d --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/service/impl/ThingServiceImpl.java @@ -0,0 +1,321 @@ +package com.example.thingdemo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.thingdemo.ao.ThingAddUpdateAo; +import com.example.thingdemo.cache.ThingActionCache; +import com.example.thingdemo.cache.ThingCache; +import com.example.thingdemo.cache.ThingEventCache; +import com.example.thingdemo.cache.ThingParamSpecCache; +import com.example.thingdemo.cache.ThingParamSpecJsonElemCache; +import com.example.thingdemo.cache.ThingProfileCache; +import com.example.thingdemo.cache.ThingPropertiesCache; +import com.example.thingdemo.constant.RedisConstant; +import com.example.thingdemo.domain.ThingDimensionEntity; +import com.example.thingdemo.domain.ThingEntity; +import com.example.thingdemo.domain.ThingParamSpecEntity; +import com.example.thingdemo.dto.ThingDetailDto; +import com.example.thingdemo.dto.ThingDimensionDetailDto; +import com.example.thingdemo.dto.ThingParamSpecDetailDto; +import com.example.thingdemo.dto.ThingParamSpecJsonElemDto; +import com.example.thingdemo.enums.DimensionEnum; +import com.example.thingdemo.enums.InoutDataEnum; +import com.example.thingdemo.exception.AppException; +import com.example.thingdemo.mapper.ThingMapper; +import com.example.thingdemo.service.ThingDimensionService; +import com.example.thingdemo.service.ThingParamSpecService; +import com.example.thingdemo.service.ThingService; +import com.example.thingdemo.util.CommonUtil; +import com.example.thingdemo.util.CopyUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + *

+ * 物模型概述 服务实现类 + *

+ * + * @author chenpq + * @since 2023-05-04 14:08:27 + */ +@Slf4j +@Service +public class ThingServiceImpl extends ServiceImpl implements ThingService { + + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private ThingMapper thingMapper; + @Autowired + private ThingDimensionService thingDimensionService; + @Autowired + private ThingParamSpecService thingParamSpecService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long addUpdate(ThingAddUpdateAo addUpdateAo) { + // id 为空,新增;id 不为空,修改 + Long id = addUpdateAo.getId(); + if (isRepeat(addUpdateAo.getId(), ThingEntity::getName, addUpdateAo.getName())) { + throw new AppException("修改失败,产品名称已存在。请修改产品名称。"); + } + ThingEntity thingEntity = new ThingEntity(); + BeanUtils.copyProperties(addUpdateAo, thingEntity); + if (Objects.isNull(id)) { + // 新增 + thingEntity.setProductKey(CommonUtil.uuid32()); + super.save(thingEntity); + id = thingEntity.getId(); + } else { + // 修改 + super.updateById(thingEntity); + } + + // 删除缓存 + ThingEntity thingDb = super.getById(id); + removeThingCache(thingDb.getProductKey()); + + return id; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean delete(Long id) { + ThingEntity thingDb = super.getById(id); + if (thingDb == null) { + return false; + } + // 删除 + boolean b = super.removeById(id); + + LambdaQueryWrapper dimensionLqw = Wrappers.lambdaQuery(); + dimensionLqw.eq(ThingDimensionEntity::getThingId, id); + thingDimensionService.remove(dimensionLqw); + + LambdaQueryWrapper paramSpecLqw = Wrappers.lambdaQuery(); + paramSpecLqw.eq(ThingParamSpecEntity::getThingId, id); + thingParamSpecService.remove(paramSpecLqw); + + // 删除缓存 + removeThingCache(thingDb.getProductKey()); + return b; + } + + @Override + public ThingDetailDto detail(Long id) { + ThingEntity thingEntity = super.getById(id); + if (thingEntity == null) { + return null; + } + ThingDetailDto thingDetail = new ThingDetailDto(); + BeanUtils.copyProperties(thingEntity, thingDetail); + + // 维度 + LambdaQueryWrapper dimensionLqw = Wrappers.lambdaQuery(); + dimensionLqw.eq(ThingDimensionEntity::getThingId, id); + List dimensionList = thingDimensionService.list(dimensionLqw); + List dimensionDetailList = CopyUtils.listCopy(dimensionList, + ThingDimensionDetailDto.class); + thingDetail.setDimensionList(dimensionDetailList); + + if (CollectionUtils.isEmpty(dimensionList)) { + return thingDetail; + } + + // 数据规格 + List dimensionIdList = dimensionList.stream().map(ThingDimensionEntity::getId) + .collect(Collectors.toList()); + LambdaQueryWrapper paramSpecLqw = Wrappers.lambdaQuery(); + paramSpecLqw.eq(ThingParamSpecEntity::getThingId, id); + paramSpecLqw.in(ThingParamSpecEntity::getThingDimensionId, dimensionIdList); + List paramSpecAllDbList = thingParamSpecService.list(paramSpecLqw); + + List parentParamSpecDbList = paramSpecAllDbList.stream() + .filter(e -> e.getParentId() == null).collect(Collectors.toList()); + List childParamSpecDbList = paramSpecAllDbList.stream() + .filter(e -> e.getParentId() != null).collect(Collectors.toList()); + + dimensionDetailList.forEach(dimension -> { + List parentList = parentParamSpecDbList.stream() + .filter(paramSpec -> Objects.equals(dimension.getId(), paramSpec.getThingDimensionId())) + .collect(Collectors.toList()); + List parentDetailList = CopyUtils.listCopy(parentList, + ThingParamSpecDetailDto.class); + // 数据规格子级 + if (CollectionUtils.isNotEmpty(parentDetailList)) { + parentDetailList.forEach(parent -> { + List childList = childParamSpecDbList.stream() + .filter(child -> Objects.equals(parent.getId(), child.getParentId())) + .collect(Collectors.toList()); + List jsonElemList = CopyUtils.listCopy( + childList, ThingParamSpecJsonElemDto.class); + parent.setJsonElemList(jsonElemList); + }); + dimension.setParamSpecList(parentDetailList); + } + }); + + return thingDetail; + } + + @Override + public ThingCache getThingCache(String productKey) { + String redisKey = RedisConstant.THING + productKey; + Object o = redisTemplate.opsForValue().get(redisKey); + if (o instanceof ThingCache) { + ThingCache thingCache = (ThingCache) o; + return thingCache; + } + + LambdaQueryWrapper thingLqw = Wrappers.lambdaQuery(); + thingLqw.eq(ThingEntity::getProductKey, productKey); + ThingEntity thingDb = super.getOne(thingLqw, false); + if (thingDb == null) { + return null; + } + ThingCache thingCache = new ThingCache(); + ThingProfileCache profile = new ThingProfileCache(); + BeanUtils.copyProperties(thingDb, profile); + thingCache.setProfile(profile); + + ArrayList properties = new ArrayList<>(); + ArrayList events = new ArrayList<>(); + ArrayList actions = new ArrayList<>(); + thingCache.setProperties(properties); + thingCache.setEvents(events); + thingCache.setActions(actions); + + // 维度 + LambdaQueryWrapper dimensionLqw = Wrappers.lambdaQuery(); + dimensionLqw.eq(ThingDimensionEntity::getThingId, thingDb.getId()); + List dimensionList = thingDimensionService.list(dimensionLqw); + + // 数据规格 + List dimensionIdList = dimensionList.stream().map(ThingDimensionEntity::getId) + .collect(Collectors.toList()); + LambdaQueryWrapper paramSpecLqw = Wrappers.lambdaQuery(); + paramSpecLqw.eq(ThingParamSpecEntity::getThingId, thingDb.getId()); + paramSpecLqw.in(ThingParamSpecEntity::getThingDimensionId, dimensionIdList); + List paramSpecAllDbList = thingParamSpecService.list(paramSpecLqw); + + List parentParamSpecDbList = paramSpecAllDbList.stream() + .filter(e -> e.getParentId() == null).collect(Collectors.toList()); + List childParamSpecDbList = paramSpecAllDbList.stream() + .filter(e -> e.getParentId() != null).collect(Collectors.toList()); + + // 维度遍历 + dimensionList.forEach(dimension -> { + // 属性 + if (DimensionEnum.PROPERTIES.getCode().equals(dimension.getDimension())) { + ThingPropertiesCache property = new ThingPropertiesCache(); + BeanUtils.copyProperties(dimension, property); + parentParamSpecDbList.stream() + .filter(paramSpec -> Objects.equals(dimension.getId(), paramSpec.getThingDimensionId())) + .findFirst() + .ifPresent(e -> { + property.setDataType(e.getDataType()); + property.setSpecs(e.getSpecs()); + }); + properties.add(property); + } else { + // 事件、动作 + List parentDimensionParamSpecList = parentParamSpecDbList.stream() + .filter(paramSpec -> Objects.equals(dimension.getId(), paramSpec.getThingDimensionId())) + .collect(Collectors.toList()); + List inputData = new ArrayList<>(); + List outputData = new ArrayList<>(); + // 每个维度的数据规则 + parentDimensionParamSpecList.forEach(parentEntity -> { + ThingParamSpecCache paramSpecParentCache = new ThingParamSpecCache(); + BeanUtils.copyProperties(parentEntity, paramSpecParentCache); + // 判断是inputData还是outputData + if (InoutDataEnum.INPUT_DATA.getCode().equals(parentEntity.getInOutData())) { + inputData.add(paramSpecParentCache); + } else if (InoutDataEnum.OUTPUT_DATA.getCode().equals(parentEntity.getInOutData())) { + outputData.add(paramSpecParentCache); + } + // 设置子级数据规格 + List childList = childParamSpecDbList.stream() + .filter(child -> Objects.equals(parentEntity.getId(), child.getParentId())) + .collect(Collectors.toList()); + List jsonElemList = CopyUtils.listCopy( + childList, ThingParamSpecJsonElemCache.class); + paramSpecParentCache.setJsonElemList(jsonElemList); + }); + + if (DimensionEnum.EVENT.getCode().equals(dimension.getDimension())) { + ThingEventCache event = new ThingEventCache(); + BeanUtils.copyProperties(dimension, event); + event.setOutputData(outputData); + events.add(event); + } else if (DimensionEnum.ACTION.getCode().equals(dimension.getDimension())) { + ThingActionCache action = new ThingActionCache(); + BeanUtils.copyProperties(dimension, action); + action.setInputData(inputData); + action.setOutputData(outputData); + actions.add(action); + } + } + }); + redisTemplate.opsForValue().set(redisKey, thingCache, 10, TimeUnit.DAYS); + return thingCache; + } + + @Override + public boolean removeThingCache(String productKey) { + Boolean delete = redisTemplate.delete(RedisConstant.THING + productKey); + removeThingVersionCache(productKey); + return delete; + } + + @Override + public String getThingVersionCache(String productKey) { + String redisKey = RedisConstant.THING_VERSION + productKey; + Object o = redisTemplate.opsForValue().get(redisKey); + String version = CommonUtil.objectToString(o); + if (StringUtils.isNotBlank(version)) { + return version; + } + + LambdaQueryWrapper thingLqw = Wrappers.lambdaQuery(); + thingLqw.eq(ThingEntity::getProductKey, productKey); + ThingEntity thingDb = super.getOne(thingLqw, false); + if (thingDb == null) { + return null; + } + version = thingDb.getVersion(); + redisTemplate.opsForValue().set(redisKey, version, 10, TimeUnit.DAYS); + return version; + } + + @Override + public boolean removeThingVersionCache(String productKey) { + return redisTemplate.delete(RedisConstant.THING_VERSION + productKey); + } + + @Override + public boolean isRepeat(Long id, SFunction func, String value) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(ThingEntity::getIsDel, 0); + lqw.eq(func, value); + if (Objects.nonNull(id)) { + lqw.ne(ThingEntity::getId, id); + } + return super.count(lqw) > 0; + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/CommonUtil.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/CommonUtil.java new file mode 100644 index 00000000..b5aa8f78 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/CommonUtil.java @@ -0,0 +1,168 @@ +package com.example.thingdemo.util; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.regex.Pattern; + +/** + * 公共方法 + * + * @author chenpq05 + * @since 2022-02-09 + */ +@Slf4j +public class CommonUtil { + + /** + * 结尾符号 + */ + public static final String END_CHAR = ".*[,.,。、]$"; + + private CommonUtil() { + } + + /** + * 返回32位UUID + * + * @return String + */ + public static String uuid32() { + return UUID.randomUUID().toString().replace("-", ""); + } + + /** + * 所有字符串都不是 null或""或" " + * + * @param css + * @return + */ + public static boolean isAllNotBlank(final CharSequence... css) { + return !StringUtils.isAllBlank(css); + } + + /** + * object转string 若 object == null 返回 "" + * + * @param obj Object + * @return String + */ + public static String objectToString(Object obj) { + if (obj == null) { + return ""; + } else if (obj instanceof String) { + return (String) obj; + } else { + return String.valueOf(obj); + } + } + + /** + * Object转Long + * + * @param object Object + * @return Long + */ + public static Long parseLong(Object object) { + String str = objectToString(object); + Long num = null; + if (!StringUtils.isEmpty(str)) { + try { + num = Long.parseLong(str); + } catch (Exception ex) { + log.error("字符串转Long异常", ex); + } + } + return num; + } + + /** + * 字符串转Integer + * + * @param object Object + * @return Integer + */ + public static Integer parseInteger(Object object) { + String str = objectToString(object); + Integer num = null; + if (!StringUtils.isEmpty(str)) { + try { + num = Integer.parseInt(str); + } catch (Exception ex) { + log.error("字符串转Integer异常", ex); + } + } + return num; + } + + /** + * 将字符串分割为List + * + * @param str 字符串 + * @param separatorChar 分隔符 + * @param clazz 集合元素类型 + * @param + * @return List + */ + public static List split(String str, String separatorChar, Class clazz) { + str = StringUtils.trim(str); + String[] arr = StringUtils.split(str, separatorChar); + if (arr == null || arr.length == 0) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (int i = 0; i < arr.length; i++) { + if (clazz.isAssignableFrom(Integer.class)) { + result.add((E) parseInteger(arr[i])); + } else if (clazz.isAssignableFrom(Long.class)) { + result.add((E) parseLong(arr[i])); + } else if (clazz.isAssignableFrom(String.class)) { + result.add((E) arr[i]); + } + } + return result; + } + + /** + * null转空字符串串 + * + * @param str + * @return + */ + public String nullToEmptyStr(String str) { + return str == null ? "" : str; + } + + + /** + * 字符串结尾是否符合正则表达式 + * + * @param str 字符串 + * @param regex 正则 例如: ".*[,.,。、]$" + * @return + */ + public static boolean isEndWith(String str, String regex) { + if (StringUtils.isAnyBlank(str, regex)) { + return false; + } + return Pattern.matches(regex, str); + } + + /** + * 字符串不相等,实现方式 !StringUtils.equals(str1, str2) + * + * @param str1 + * @param str2 + * @return + */ + public static boolean strNotEquals(String str1, String str2) { + return !StringUtils.equals(str1, str2); + } + +} + + + diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/CopyUtils.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/CopyUtils.java new file mode 100644 index 00000000..cfa7eaf1 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/CopyUtils.java @@ -0,0 +1,47 @@ +package com.example.thingdemo.util; + +import com.example.thingdemo.exception.AppException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author chenpq05 + * @since 2022/3/14 14:01 + */ +@Slf4j +public class CopyUtils { + + private CopyUtils() { + } + + /** + * 集合拷贝 + * + * @param sources 原集合 + * @param targetClazz 目标集合元素类型 + * @param + * @param + * @return 目标集合 + * @throws AppException + */ + public static List listCopy(Collection sources, Class targetClazz) + throws AppException { + ArrayList result = new ArrayList<>(); + for (E source : sources) { + try { + T t = targetClazz.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(source, t); + result.add(t); + } catch (Exception e) { + log.error("异常", e); + throw new AppException(e.getMessage()); + } + } + return result; + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/DatePatternUtil.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/DatePatternUtil.java new file mode 100644 index 00000000..d891b4ab --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/DatePatternUtil.java @@ -0,0 +1,135 @@ +package com.example.thingdemo.util; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 时间字符串转时间 + * + * @author cpq + * @since 2022-03-17 11:28:55 + */ +@Slf4j +public class DatePatternUtil { + + /** + * 格式化 + */ + private static final Map PATTERN_MAP = new HashMap<>(); + private static final List PATTERN_LIST = new ArrayList<>(32); + private static final Pattern YYYY_MM_DD_MIDDLE = Pattern.compile("(\\d{4}-\\d{1,2}-\\d{1,2})"); + private static final Pattern YYYY_MM_DD_HH_MM_MIDDLE = Pattern.compile("(\\d{4}-\\d{1,2}-\\d{1,2} \\d{1,2}:\\d{1,2})"); + private static final Pattern YYYY_MM_DD_HH_MM_SS_MIDDLE = Pattern.compile("\\d{4}-\\d{1,2}-\\d{1,2}\\s\\d{1,2}:\\d{1,2}:\\d{1,2}"); + private static final Pattern YYYY_MM_DD_HH_MM_SS_SSS_MIDDLE = Pattern + .compile("\\d{4}-\\d{1,2}-\\d{1,2}\\s\\d{1,2}:\\d{1,2}:\\d{1,2}\\.\\d+"); + private static final Pattern YYYY_MM_DD_SLASH = Pattern.compile("\\d{4}/\\d{1,2}/\\d{1,2}"); + private static final Pattern YYYY_MM_DD_HH_MM_SS_SLASH = Pattern.compile("\\d{4}/\\d{1,2}/\\d{1,2}\\s\\d{1,2}:\\d{1,2}:\\d{1,2}"); + + static { + PATTERN_MAP.put(YYYY_MM_DD_MIDDLE, "yyyy-MM-dd"); + PATTERN_MAP.put(YYYY_MM_DD_HH_MM_MIDDLE, "yyyy-MM-dd HH:mm"); + PATTERN_MAP.put(YYYY_MM_DD_HH_MM_SS_MIDDLE, "yyyy-MM-dd HH:mm:ss"); + PATTERN_MAP.put(YYYY_MM_DD_HH_MM_SS_SSS_MIDDLE, "yyyy-MM-dd HH:mm:ss.SSS"); + PATTERN_MAP.put(YYYY_MM_DD_SLASH, "yyyy/MM/dd"); + PATTERN_MAP.put(YYYY_MM_DD_HH_MM_SS_SLASH, "yyyy/MM/dd HH:mm:ss"); + + // 添加pattern + PATTERN_LIST.add(YYYY_MM_DD_MIDDLE); + PATTERN_LIST.add(YYYY_MM_DD_HH_MM_MIDDLE); + PATTERN_LIST.add(YYYY_MM_DD_HH_MM_SS_MIDDLE); + PATTERN_LIST.add(YYYY_MM_DD_HH_MM_SS_SSS_MIDDLE); + PATTERN_LIST.add(YYYY_MM_DD_SLASH); + PATTERN_LIST.add(YYYY_MM_DD_HH_MM_SS_SLASH); + } + + private DatePatternUtil() { + // sonar检测 + throw new IllegalStateException("不允许实例化"); + } + + /** + * 获取需要反序列化为正确格式的日期 + * + * @param strDateValue 字符串类型的日期值 + * @return Date + */ + public static LocalDateTime strToLocalDateTime(String strDateValue) { + if (StringUtils.isBlank(strDateValue)) { + return null; + } + // 解决字符串被自动转码导致的问题,在此将转码后的字符串还原。 + char ch = '%'; + if (strDateValue.indexOf(ch) >= 0) { + try { + strDateValue = URLDecoder.decode(strDateValue, "UTF-8"); + } catch (UnsupportedEncodingException e) { + log.error("时间转换编码异常", e); + } + } + + LocalDateTime result = null; + String format = getMatchFormat(strDateValue); + if (format == null) { + // 如果以上8种时间格式都无法匹配,校验是否是时间戳格式,如果是就直接转换为Date,否则直接抛出异常 + String regex = "[-]?\\d+"; + Matcher matcher = Pattern.compile(regex).matcher(strDateValue); + boolean isMatch = matcher.matches(); + if (isMatch) { + result = DateUtil.toLocalDateTime(CommonUtil.parseLong(strDateValue)); + } + } else if (YYYY_MM_DD_MIDDLE.matcher(strDateValue).matches()) { + DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd[['T'HH][:mm][:ss]]") + .parseDefaulting(ChronoField.HOUR_OF_DAY, 0) + .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0) + .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0) + .parseDefaulting(ChronoField.MILLI_OF_SECOND, 0) + .toFormatter(); + result = LocalDateTime.parse(strDateValue, formatter); + } else if (YYYY_MM_DD_SLASH.matcher(strDateValue).matches()) { + DateTimeFormatter formatter = new DateTimeFormatterBuilder() + .appendPattern("yyyy/MM/dd[['T'HH][:mm][:ss]]") + .parseDefaulting(ChronoField.HOUR_OF_DAY, 0) + .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0) + .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0) + .parseDefaulting(ChronoField.MILLI_OF_SECOND, 0) + .toFormatter(); + result = LocalDateTime.parse(strDateValue, formatter); + } else { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(format); + result = LocalDateTime.parse(strDateValue, dtf); + } + + return result; + } + + /** + * 根据值获取合适的格式 + * + * @param value 数据 + * @return 格式 + */ + private static String getMatchFormat(final String value) { + Pattern pattern; + for (Iterator iterator = PATTERN_LIST.iterator(); iterator.hasNext(); ) { + pattern = iterator.next(); + Matcher matcher = pattern.matcher(value); + boolean isMatch = matcher.matches(); + if (isMatch) { + return PATTERN_MAP.get(pattern); + } + } + return null; + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/DateUtil.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/DateUtil.java new file mode 100644 index 00000000..b5f98d8d --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/DateUtil.java @@ -0,0 +1,303 @@ +package com.example.thingdemo.util; + +import org.apache.commons.lang3.StringUtils; + +import java.text.SimpleDateFormat; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.Objects; + +/** + * 时间工具类 + * + * @author chenpq05 + * @since 2022/2/23 14:14 + */ +public class DateUtil { + + public static final ZoneId ZONE_ID_DEFAULT = ZoneId.systemDefault(); + public static final ZoneOffset ZONE_OFFSET_8 = ZoneOffset.of("+8"); + public static final String TIME_FIRST_STR = " 00:00:00"; + public static final String TIME_END_STR = " 23:59:59"; + public static final String YMD_FORMAT_STR = "yyyy-MM-dd"; + public static final String YMDHMS_FORMAT_STR = "yyyy-MM-dd HH:mm:ss"; + public static final String YMDHM_FORMAT_STR = "yyyy-MM-dd HH:mm"; + public static final String YMDHMS_FORMAT_STR_2 = "yyyyMMddHHmmss"; + public static final String YMDHMSSSS_FORMAT_STR = "yyyy-MM-dd HH:mm:ss.SSS"; + + private DateUtil() { + // sonar检测 + throw new IllegalStateException("不允许实例化"); + } + + /** + * localDate转时间戳 + * + * @param localDate + * @return + */ + public static Long toTimestamp(LocalDate localDate) { + if (Objects.isNull(localDate)) { + return null; + } + return localDate.atStartOfDay(ZONE_OFFSET_8).toInstant().toEpochMilli(); + } + + /** + * localdatetime转时间戳 + * + * @param localDateTime + * @return + */ + public static Long toTimestamp(LocalDateTime localDateTime) { + if (Objects.isNull(localDateTime)) { + return null; + } + return localDateTime.toInstant(ZONE_OFFSET_8).toEpochMilli(); + } + + /** + * localDate转时间戳秒 + * + * @param localDateTime + * @return + */ + public static Long toTimestampSecond(LocalDateTime localDateTime) { + if (Objects.isNull(localDateTime)) { + return null; + } + long timestamp = localDateTime.toInstant(ZONE_OFFSET_8).toEpochMilli() / 1000; + return CommonUtil.parseLong(timestamp); + } + + /** + * offsetDateTime转时间戳 + * + * @param offsetDateTime + * @return + */ + public static Long toTimestamp(OffsetDateTime offsetDateTime) { + if (Objects.isNull(offsetDateTime)) { + return null; + } + return offsetDateTime.toInstant().toEpochMilli(); + } + + /** + * 时间戳转localDatetime + * + * @param timestamp + * @return + */ + public static LocalDate toLocalDate(Long timestamp) { + if (Objects.isNull(timestamp)) { + return null; + } + return Instant.ofEpochMilli(timestamp).atZone(ZONE_ID_DEFAULT).toLocalDate(); + } + + /** + * 时间戳转localDatetime + * + * @param timestamp + * @return + */ + public static LocalDateTime toLocalDateTime(Long timestamp) { + if (Objects.isNull(timestamp)) { + return null; + } + return Instant.ofEpochMilli(timestamp).atZone(ZONE_ID_DEFAULT).toLocalDateTime(); + } + + /** + * 时间戳转OffsetDateTime + * + * @param timestamp + * @return + */ + public static OffsetDateTime toOffsetDateTime(Long timestamp) { + if (Objects.isNull(timestamp)) { + return null; + } + return Instant.ofEpochMilli(timestamp).atZone(ZONE_ID_DEFAULT).toOffsetDateTime(); + } + + /** + * 字符串转LocalDatetime + * + * @param timeString 例如 2018-06-01 23:59:59 + * @param df 例如 DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + * @return LocalDateTime + */ + public static LocalDateTime toLocalDateTime(String timeString, DateTimeFormatter df) { + if (StringUtils.isBlank(timeString) || Objects.isNull(df)) { + return null; + } + return LocalDateTime.parse(timeString, df); + } + + /** + * Date转LocalDateTime + * + * @param date + * @return + */ + public static LocalDateTime toLocalDateTime(Date date) { + if (date == null) { + return null; + } + Instant instant = date.toInstant(); + return LocalDateTime.ofInstant(instant, ZONE_ID_DEFAULT); + } + + /** + * Date转LocalDate + * + * @param date + * @return LocalDate + */ + public static LocalDate toLocalDate(Date date) { + if (date == null) { + return null; + } + return toLocalDateTime(date).toLocalDate(); + } + + /** + * Date转LocalTime + * + * @param date + * @return LocalTime + */ + public static LocalTime toLocalTime(Date date) { + if (date == null) { + return null; + } + return toLocalDateTime(date).toLocalTime(); + } + + /** + * LocalDateTime 转 OffsetDateTime + * + * @param localDateTime + * @return + */ + public static OffsetDateTime toOffsetDateTime(LocalDateTime localDateTime) { + if (localDateTime == null) { + return null; + } + return localDateTime.atOffset(ZONE_OFFSET_8); + } + + /** + * offsetDateTime 转 date + * + * @param offsetDateTime + * @return Date + */ + public static Date toDate(OffsetDateTime offsetDateTime) { + if (offsetDateTime == null) { + return null; + } + return Date.from(offsetDateTime.atZoneSameInstant(ZONE_ID_DEFAULT).toInstant()); + } + + /** + * localDateTime 转 date + * + * @param localDateTime + * @return Date + */ + public static Date toDate(LocalDateTime localDateTime) { + if (localDateTime == null) { + return null; + } + return Date.from(localDateTime.toInstant(ZONE_OFFSET_8)); + } + + /** + * LocalDate 转 date + * + * @param localDate + * @return Date + */ + public static Date toDate(LocalDate localDate) { + if (localDate == null) { + return null; + } + return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + + /** + * 生成日期时间字符串 + * + * @param dateTime 日期时间 + * @param pattern 日期时间样式 + * @return string 日期时间字符串 + */ + public static String toDateTimeString(LocalDateTime dateTime, String pattern) { + if (dateTime == null || StringUtils.isBlank(pattern)) { + return ""; + } + DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern); + return dateTime.format(fmt); + } + + /** + * dateTime 转 yyyy-MM-dd HH:mm:ss + * + * @param dateTime + * @return + */ + public static String toYyyyMMddHHmmss(LocalDateTime dateTime) { + if (dateTime == null) { + return ""; + } + DateTimeFormatter fmt = DateTimeFormatter.ofPattern(YMDHMS_FORMAT_STR); + return dateTime.format(fmt); + } + + /** + * dateTime 转 yyyy-MM-dd + * + * @param dateTime + * @return + */ + public static String toYyyyMMdd(LocalDateTime dateTime) { + if (dateTime == null) { + return ""; + } + DateTimeFormatter fmt = DateTimeFormatter.ofPattern(YMD_FORMAT_STR); + return dateTime.format(fmt); + } + + /** + * dateTime 转 yyyy-MM-dd + * + * @param date + * @return + */ + public static String toYyyyMMdd(LocalDate date) { + if (date == null) { + return ""; + } + DateTimeFormatter fmt = DateTimeFormatter.ofPattern(YMD_FORMAT_STR); + return date.format(fmt); + } + + /** + * 格式化时间 + * + * @param date date + * @return yyyy-MM-dd HH:mm:ss + */ + public static String toYyyyMmDdHmMmSs(Date date) { + if (date == null) { + return null; + } + SimpleDateFormat f = new SimpleDateFormat(YMDHMS_FORMAT_STR); + return f.format(date); + } + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/ObjectMapperUtil.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/ObjectMapperUtil.java new file mode 100644 index 00000000..c70d4a37 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/util/ObjectMapperUtil.java @@ -0,0 +1,144 @@ +package com.example.thingdemo.util; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +/** + * @author chenpq05 + * @since 2022/2/21 15:02 + */ +@Slf4j +public class ObjectMapperUtil { + + private ObjectMapperUtil() { + } + + private final static ObjectMapper OBJECT_MAPPER = newObjectMapper(); + + /** + * 创建一个新的ObjectMapper + * + * @return + */ + public static ObjectMapper newObjectMapper() { + + ObjectMapper objectMapper = new ObjectMapper(); + + //序列化的时候序列对象的所有属性 + objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS); + //反序列化的时候如果多了其他属性,不抛出异常 + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + //如果是空对象的时候,不抛异常 + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + + // 时间转换为时间戳。 + JavaTimeModule javaTimeModule = new JavaTimeModule(); + objectMapper.registerModule(javaTimeModule); + objectMapper.registerModule(new Jdk8Module()); + return objectMapper; + } + + + /** + * 字符串转对象 + * + * @param content content + * @param valueType valueType + * @param T + * @return T + */ + public static T readValue(String content, Class valueType) { + if (StringUtils.isBlank(content)) { + return null; + } + if (String.class.equals(valueType)) { + return (T) content; + } + try { + return OBJECT_MAPPER.readValue(content, valueType); + } catch (Exception e) { + log.error("异常", e); + } + return null; + } + + /** + * 字节数组转对象 + * + * @param src + * @param valueType + * @param + * @return + */ + public static T readValue(byte[] src, Class valueType) { + if (Objects.isNull(src)) { + return null; + } + if (String.class.equals(valueType)) { + return (T) new String(src, StandardCharsets.UTF_8); + } + try { + return OBJECT_MAPPER.readValue(src, valueType); + } catch (Exception e) { + log.error("异常", e); + } + return null; + } + + /** + * 字符串转对象 + * + * @param content content + * @param valueTypeRef valueTypeRef + * @param T + * @return T + */ + public static T readValue(String content, TypeReference valueTypeRef) { + if (StringUtils.isBlank(content)) { + return null; + } + try { + return OBJECT_MAPPER.readValue(content, valueTypeRef); + } catch (Exception e) { + log.error("异常", e); + } + return null; + } + + /** + * 对象转string + * + * @param value value + * @return String + */ + public static String writeValueAsString(Object value) { + if (Objects.isNull(value)) { + return null; + } else if (value instanceof String) { + return (String) value; + } + try { + return OBJECT_MAPPER.writeValueAsString(value); + } catch (Exception e) { + log.error("异常", e); + } + return ""; + } + + + + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DeviceAddVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DeviceAddVo.java new file mode 100644 index 00000000..bbf88537 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DeviceAddVo.java @@ -0,0 +1,38 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; + +/** + * @author chenpq + * @since 2023-05-07 21:52:39 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "设备-新增请求体") +public class DeviceAddVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "设备名称") + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + + @ApiModelProperty(value = "设备编码") + @NotEmpty(message = "设备编码不能为空") + private String deviceCode; + + @ApiModelProperty(value = "产品key") + @NotEmpty(message = "产品不能为空") + private String productKey; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertiesShadowInitVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertiesShadowInitVo.java new file mode 100644 index 00000000..3f81e2f1 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertiesShadowInitVo.java @@ -0,0 +1,31 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +/** + * @author chenpq + * @since 2023-05-07 21:53:04 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "设备属性影子-新增请求体") +public class DevicePropertiesShadowInitVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "产品key") + private String productKey; + + @ApiModelProperty(value = "设备编码") + private String deviceCode; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertiesShadowUpdateCurrentVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertiesShadowUpdateCurrentVo.java new file mode 100644 index 00000000..48055afe --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertiesShadowUpdateCurrentVo.java @@ -0,0 +1,37 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import java.io.Serializable; + +/** + * @author chenpq + * @since 2023-05-07 21:53:04 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "设备属性影子-更新实际值请求体") +public class DevicePropertiesShadowUpdateCurrentVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "产品key") + private String productKey; + + @ApiModelProperty(value = "设备编码") + private String deviceCode; + + @ApiModelProperty(value = "properties唯一标识符") + private String identifier; + + @ApiModelProperty(value = "设备实际值") + private String currentValue; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertiesShadowUpdateExpectVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertiesShadowUpdateExpectVo.java new file mode 100644 index 00000000..63421689 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertiesShadowUpdateExpectVo.java @@ -0,0 +1,42 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; + +/** + * @author chenpq + * @since 2023-05-07 21:53:04 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "设备属性影子-更新期望值请求体") +public class DevicePropertiesShadowUpdateExpectVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "产品key") + @NotEmpty(message = "产品key不能为空") + private String productKey; + + @ApiModelProperty(value = "设备编码") + @NotEmpty(message = "设备编码不能为空") + private String deviceCode; + + @ApiModelProperty(value = "properties唯一标识符") + @NotEmpty(message = "identifier不能为空") + private String identifier; + + @ApiModelProperty(value = "期望值") + @NotEmpty(message = "期望值不能为空") + private String expectValue; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertyUpdateVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertyUpdateVo.java new file mode 100644 index 00000000..bb7b6783 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DevicePropertyUpdateVo.java @@ -0,0 +1,38 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.io.Serializable; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * @author chenpq + * @since 2023-05-07 21:53:04 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "设备属性影子-更新期望值请求体") +public class DevicePropertyUpdateVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "设备id") + @NotNull(message = "设备id不能为空") + private Long deviceId; + + @ApiModelProperty(value = "properties唯一标识符") + @NotEmpty(message = "identifier不能为空") + private String identifier; + + @ApiModelProperty(value = "期望值") + @NotNull(message = "期望值不能为空") + private Object expectValue; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DeviceUpdateVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DeviceUpdateVo.java new file mode 100644 index 00000000..1168aa1f --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DeviceUpdateVo.java @@ -0,0 +1,35 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +/** + * @author chenpq + * @since 2023-05-07 21:52:39 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "设备-修改请求体") +public class DeviceUpdateVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键") + @NotNull(message = "主键不能为空") + private Long id; + + @ApiModelProperty(value = "设备名称") + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DimensionAddVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DimensionAddVo.java new file mode 100644 index 00000000..560da2c3 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DimensionAddVo.java @@ -0,0 +1,65 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.Valid; +import javax.validation.constraints.*; +import java.io.Serializable; +import java.util.List; + +/** + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "物模型3维度类型,1(properties),2(event),3(action)-新增请求体") +public class DimensionAddVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "物模型维度名称", required = true) + @NotEmpty(message = "物模型维度名称不能为空") + private String name; + + @ApiModelProperty(value = "物模型维度描述") + private String description; + + @ApiModelProperty(value = "维度类型,1(properties),2(event),3(action)") + @NotNull(message = "维度类型不能为空") + @Min(value = 1, message = "维度类型只能是1、2、3") + @Max(value = 3, message = "维度类型只能是1、2、3") + private Integer dimension; + + @ApiModelProperty(value = "唯一标识符") + @NotEmpty(message = "唯一标识符不能为空") + private String identifier; + + @ApiModelProperty(value = "属性读写类型:只读(r)或读写(rw)") + @Pattern(regexp = "^(r|rw)$", message = "属性读写类型错误,只能是r、rw") + private String propertiesAccessMode; + + @ApiModelProperty(value = "action调用方式,1(同步调用)或2(异步调用)") + @Min(value = 1, message = "action调用方式错误,只能是1、2") + @Max(value = 2, message = "action调用方式错误,只能是1、2") + private Integer actionCallType; + + @ApiModelProperty(value = "事件类型 1信息,2告警,3故障") + @Min(value = 1, message = "事件类型错误,只能是1、2、3") + @Max(value = 3, message = "事件类型错误,只能是1、2、3") + private Integer eventType; + + @ApiModelProperty(value = "数据规格") + @Valid + @NotNull(message = "数据规格不能为空") + private List paramSpecList; + + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DimensionBatchAddVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DimensionBatchAddVo.java new file mode 100644 index 00000000..c45570c2 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DimensionBatchAddVo.java @@ -0,0 +1,38 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.List; + +/** + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "物模型-新增请求体") +public class DimensionBatchAddVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "物模型id", required = true) + @NotNull(message = "物模型id不能为空") + private Long thingId; + + @ApiModelProperty(value = "物模型维度", required = true) + @NotNull(message = "维度不能为空") + @Size(min = 1, message = "维度不能为空") + private List<@Valid DimensionAddVo> dimensionList; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DimensionUpdateVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DimensionUpdateVo.java new file mode 100644 index 00000000..39ed63ea --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/DimensionUpdateVo.java @@ -0,0 +1,62 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.Valid; +import javax.validation.constraints.*; +import java.io.Serializable; +import java.util.List; + +/** + * @author chenpq + * @since 2023-05-04 14:09:09 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "物模型3维度类型修改请求体") +public class DimensionUpdateVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "维度id", required = true) + @NotNull(message = "维度id不能为空") + private Long id; + + @ApiModelProperty(value = "物模型维度名称", required = true) + @NotEmpty(message = "物模型维度名称不能为空") + private String name; + + @ApiModelProperty(value = "物模型维度描述") + private String description; + + @ApiModelProperty(value = "唯一标识符") + @NotEmpty(message = "唯一标识符不能为空") + private String identifier; + + @ApiModelProperty(value = "属性读写类型:只读(r)或读写(rw)") + @Pattern(regexp = "^(r|rw)$", message = "属性读写类型错误,只能是r、rw") + private String propertiesAccessMode; + + @ApiModelProperty(value = "action调用方式,1(同步调用)或2(异步调用)") + @Min(value = 1, message = "action调用方式错误,只能是1、2") + @Max(value = 2, message = "action调用方式错误,只能是1、2") + private Integer actionCallType; + + @ApiModelProperty(value = "事件类型 1信息,2告警,3故障") + @Min(value = 1, message = "事件类型错误,只能是1、2、3") + @Max(value = 3, message = "事件类型错误,只能是1、2、3") + private Integer eventType; + + @ApiModelProperty(value = "数据规格") + @Valid + @NotNull(message = "数据规格不能为空") + private List paramSpecList; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingAddVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingAddVo.java new file mode 100644 index 00000000..52b0baf4 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingAddVo.java @@ -0,0 +1,35 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @author chenpq + * @since 2023-05-04 14:08:27 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "物模型概述-新增请求体") +public class ThingAddVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "产品名称", required = true) + @NotEmpty(message = "产品名称不能为空") + private String name; + + @ApiModelProperty(value = "版本,版本号必须是纯数字和英文点号", example = "1.0.0") + @Pattern(regexp = "^[1-9]\\d?(\\.([1-9]?\\d)){2}$", message = "版本号必须是纯数字和英文点号,例如:1.0.0") + private String version; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingParamSpecAddUpdateVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingParamSpecAddUpdateVo.java new file mode 100644 index 00000000..1ce0330d --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingParamSpecAddUpdateVo.java @@ -0,0 +1,51 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.List; + +/** + * @author chenpq + * @since 2023-05-05 17:00:52 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "物模型3维度数据规格-新增修改ao") +public class ThingParamSpecAddUpdateVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符") + private String paramIdentifier; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符名称") + private String paramIdentifierName; + + @ApiModelProperty(value = "1 inputData,2 outputData") + private Integer inOutData; + + @ApiModelProperty(value = "数据类型: int32(原生)、float(原生)、double(原生)、text(原生)、date(String类型UTC毫秒)、bool(0或1的int类型)、enum(int类型,枚举项定义方法与bool类型定义0和1的方法相同)、struct(结构体类型,可包含前面7种类型,下面使用\"specs\":[{}]描述包含的对象)、array(数组类型,支持int、double、float、text、struct)") + @NotEmpty(message = "数据类型不能为空") + private String dataType; + + @ApiModelProperty(value = "数据规格 { \"min\": \"参数最小值(int、float、double类型特有)。\", \"max\": \"参数最大值(int、float、double类型特有)。\", \"unit\": \"属性单位(int、float、double类型特有,非必填)。\", \"unitName\": \"单位名称(int、float、double类型特有,非必填)。\", \"size\": \"数组元素的个数,最大512(array类型特有)。\", \"step\": \"步长(text、enum类型无此参数)。\", \"length\": \"数据长度,最大10240(text类型特有)。\", \"0\": \"0的值(bool类型特有)。\", \"1\": \"1的值(bool类型特有)。\", \"arrayItemType\": \"数组元素的类型(array类型特有)。\" }") + private String specs; + + @ApiModelProperty(value = "排序,小的在前面") + private Integer sort; + + @ApiModelProperty(value = "json对象key的数据规格") + //@ApiModelProperty(value = "{\"size\":100,\"arrayItemType\":\"struct\"}") + //@ApiModelProperty(value = "{\"min\":1,\"max\":200,\"step\":1,\"unit\":\"°C\",\"unitName\":\"摄氏度\"}") + private List jsonElemList; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingParamSpecJsonElemVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingParamSpecJsonElemVo.java new file mode 100644 index 00000000..2cc34b09 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingParamSpecJsonElemVo.java @@ -0,0 +1,46 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; + +/** + * @author chenpq + * @since 2023-05-05 17:00:52 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "json数据") +public class ThingParamSpecJsonElemVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符") + private String paramIdentifier; + + @ApiModelProperty(value = "event、action输出输出参数唯一标识符名称") + private String paramIdentifierName; + + @ApiModelProperty(value = "数据类型: int32(原生)、float(原生)、double(原生)、text(原生)、date(String类型UTC毫秒)、bool(0或1的int类型)、enum(int类型,枚举项定义方法与bool类型定义0和1的方法相同)、struct(结构体类型,可包含前面7种类型,下面使用\"specs\":[{}]描述包含的对象)、array(数组类型,支持int、double、float、text、struct)") + @NotEmpty(message = "数据类型不能为空") + private String dataType; + + @ApiModelProperty(value = "数据规格 { \"min\": \"参数最小值(int、float、double类型特有)。\", \"max\": \"参数最大值(int、float、double类型特有)。\", \"unit\": \"属性单位(int、float、double类型特有,非必填)。\", \"unitName\": \"单位名称(int、float、double类型特有,非必填)。\", \"size\": \"数组元素的个数,最大512(array类型特有)。\", \"step\": \"步长(text、enum类型无此参数)。\", \"length\": \"数据长度,最大10240(text类型特有)。\", \"0\": \"0的值(bool类型特有)。\", \"1\": \"1的值(bool类型特有)。\", \"arrayItemType\": \"数组元素的类型(array类型特有)。\" }") + private String specs; + + @ApiModelProperty(value = "排序,小的在前面") + private Integer sort; + + @ApiModelProperty(value = "json对象key的数据规格") + private List jsonElemList; + +} diff --git a/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingUpdateVo.java b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingUpdateVo.java new file mode 100644 index 00000000..d374e19e --- /dev/null +++ b/project/thing-model/thing-demo/src/main/java/com/example/thingdemo/vo/ThingUpdateVo.java @@ -0,0 +1,40 @@ +package com.example.thingdemo.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.io.Serializable; + +/** + * @author chenpq + * @since 2023-05-04 14:08:27 + */ +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(value = "物模型概述-修改请求体") +public class ThingUpdateVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "主键", required = true) + @NotNull(message = "主键不能为空") + private Long id; + + @ApiModelProperty(value = "产品名称", required = true) + @NotEmpty(message = "产品名称不能为空") + private String name; + + @ApiModelProperty(value = "版本,版本号必须是纯数字和英文点号", example = "1.0.0") + @Pattern(regexp = "^[1-9]\\d?(\\.([1-9]?\\d)){2}$", message = "版本号必须是纯数字和英文点号,例如:1.0.0") + private String version; + +} diff --git a/project/thing-model/thing-demo/src/main/resources/application.properties b/project/thing-model/thing-demo/src/main/resources/application.properties new file mode 100644 index 00000000..867322c5 --- /dev/null +++ b/project/thing-model/thing-demo/src/main/resources/application.properties @@ -0,0 +1,43 @@ +server.port=8888 +server.servlet.context-path=/thing-demo + +spring.application.name=thing-demo +spring.main.allow-circular-references=true + +spring.datasource.url=jdbc:mysql://127.0.0.1:3306/thing?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false +spring.datasource.username=root +spring.datasource.password=cpq..123 +spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.hikarimaximum-pool-size=200 +spring.datasource.minimum-idle=20 +spring.datasource.idle-timeout=300000 +spring.datasource.connection-timeout=20000 +spring.datasource.connection-test-query=select 1 + +spring.redis.host=127.0.0.1 +spring.redis.port=6379 +spring.redis.database=0 +#spring.redis.password=passwd123 +spring.redis.timeout=10000ms +spring.redis.lettuce.pool.max-active=100 +spring.redis.lettuce.pool.max-idle=100 +spring.redis.lettuce.pool.min-idle=2 +spring.redis.lettuce.pool.max-wait=10000ms +spring.redis.lettuce.shutdown-timeout=100ms + +spring.mqtt.username=user01 +spring.mqtt.password=user01 +spring.mqtt.host-url=tcp://127.0.0.1:1883 +# \u5BA2\u6237\u7AEFId\uFF0C\u6BCF\u4E2A\u542F\u52A8\u7684id\u8981\u4E0D\u540C +spring.mqtt.providerClientId=thing-demo-provider +spring.mqtt.consumerClientId=thing-demo-consumer +# \u8D85\u65F6\u65F6\u95F4 +spring.mqtt.timeout=100 +spring.mqtt.keepalive=60 + + +knife4j.enable=true +knife4j.openapi.title=\u7269\u6A21\u578B + +logging.level.com.example.thingdemo=debug + diff --git a/project/thing-model/thing-demo/src/test/java/com/example/thingdemo/ThingDemoApplicationTests.java b/project/thing-model/thing-demo/src/test/java/com/example/thingdemo/ThingDemoApplicationTests.java new file mode 100644 index 00000000..8649f3bc --- /dev/null +++ b/project/thing-model/thing-demo/src/test/java/com/example/thingdemo/ThingDemoApplicationTests.java @@ -0,0 +1,15 @@ +package com.example.thingdemo; + +import org.junit.jupiter.api.Test; +import org.mybatis.spring.annotation.MapperScan; +import org.mybatis.spring.annotation.MapperScans; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ThingDemoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/project/web-whole/src/main/webapp/static/page/file-upload.html b/project/web-whole/src/main/webapp/static/page/file-upload.html index 79bf4d6e..f0faa62f 100644 --- a/project/web-whole/src/main/webapp/static/page/file-upload.html +++ b/project/web-whole/src/main/webapp/static/page/file-upload.html @@ -56,7 +56,7 @@

formdata上传表单

formdata上传表单,多文件

- +
@@ -67,9 +67,9 @@

文件下载

- - - + + + + + diff --git a/project/xxl-job-2.4.0/.github/FUNDING.yml b/project/xxl-job-2.4.0/.github/FUNDING.yml new file mode 100644 index 00000000..22cdaff4 --- /dev/null +++ b/project/xxl-job-2.4.0/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: ['https://www.xuxueli.com/page/donate.html'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/project/xxl-job-2.4.0/.github/ISSUE_TEMPLATE b/project/xxl-job-2.4.0/.github/ISSUE_TEMPLATE new file mode 100644 index 00000000..bed3a337 --- /dev/null +++ b/project/xxl-job-2.4.0/.github/ISSUE_TEMPLATE @@ -0,0 +1,11 @@ +Please answer some questions before submitting your issue. Thanks! + +### Which version of XXL-JOB do you using? + +### Expected behavior + +### Actual behavior + +### Steps to reproduce the behavior + +### Other information \ No newline at end of file diff --git a/project/xxl-job-2.4.0/.github/PULL_REQUEST_TEMPLATE b/project/xxl-job-2.4.0/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 00000000..cadcc886 --- /dev/null +++ b/project/xxl-job-2.4.0/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,14 @@ +**What kind of change does this PR introduce?** (check at least one) + +- [ ] Bugfix +- [ ] Feature +- [ ] Code style update +- [ ] Refactor +- [ ] Build-related changes +- [ ] Other, please describe: + + +**The description of the PR:** + + +**Other information:** \ No newline at end of file diff --git a/project/xxl-job-2.4.0/.github/workflows/maven.yml b/project/xxl-job-2.4.0/.github/workflows/maven.yml new file mode 100644 index 00000000..91106d3f --- /dev/null +++ b/project/xxl-job-2.4.0/.github/workflows/maven.yml @@ -0,0 +1,17 @@ +name: Java CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Build with Maven + run: mvn -B package --file pom.xml diff --git a/project/xxl-job-2.4.0/.gitignore b/project/xxl-job-2.4.0/.gitignore new file mode 100644 index 00000000..52c1b008 --- /dev/null +++ b/project/xxl-job-2.4.0/.gitignore @@ -0,0 +1,7 @@ +.idea +.classpath +.project +*.iml +target/ +.DS_Store +.gitattributes diff --git a/project/xxl-job-2.4.0/LICENSE b/project/xxl-job-2.4.0/LICENSE new file mode 100644 index 00000000..9cecc1d4 --- /dev/null +++ b/project/xxl-job-2.4.0/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/project/xxl-job-2.4.0/NOTICE b/project/xxl-job-2.4.0/NOTICE new file mode 100644 index 00000000..91fed501 --- /dev/null +++ b/project/xxl-job-2.4.0/NOTICE @@ -0,0 +1,32 @@ +Copyright (c) 2015-present, xuxueli. + +Dependencies: +================================================================ + +Spring: + + * LICENSE: + * http://www.apache.org/licenses/LICENSE-2.0 (Apache License 2.0) + * HOMEPAGE: + * http://www.springsource.org + +Netty: + + * LICENSE: + * http://www.apache.org/licenses/LICENSE-2.0 (Apache License 2.0) + * HOMEPAGE: + * https://github.com/netty/netty + +Hessian: + + * LICENSE: + * http://www.apache.org/licenses/LICENSE-2.0 (Apache License 2.0) + * HOMEPAGE: + * http://hessian.caucho.com + +SLF4J: + + * LICENSE: + * http://www.apache.org/licenses/LICENSE-2.0 (Apache License 2.0) + * HOMEPAGE: + * http://www.slf4j.org diff --git a/project/xxl-job-2.4.0/README.md b/project/xxl-job-2.4.0/README.md new file mode 100644 index 00000000..27bbf245 --- /dev/null +++ b/project/xxl-job-2.4.0/README.md @@ -0,0 +1,755 @@ +

+ +

XXL-JOB

+

+ XXL-JOB, a distributed task scheduling framework. +
+ -- Home Page -- +
+
+ + + + + + + + + + + + + + + + + + + + + +

+

+ + +## Introduction +XXL-JOB is a distributed task scheduling framework. +It's core design goal is to develop quickly and learn simple, lightweight, and easy to expand. +Now, it's already open source, and many companies use it in production environments, real "out-of-the-box". + +XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 + + +## Documentation +- [中文文档](https://www.xuxueli.com/xxl-job/) +- [English Documentation](https://www.xuxueli.com/xxl-job/en/) + + +## Communication +- [社区交流](https://www.xuxueli.com/page/community.html) + + +## Features +- 1、简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手; +- 2、动态:支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效; +- 3、调度中心HA(中心式):调度采用中心式设计,“调度中心”自研调度组件并支持集群部署,可保证调度中心HA; +- 4、执行器HA(分布式):任务分布式执行,任务"执行器"支持集群部署,可保证任务执行HA; +- 5、注册中心: 执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行。同时,也支持手动录入执行器地址; +- 6、弹性扩容缩容:一旦有新执行器机器上线或者下线,下次调度时将会重新分配任务; +- 7、触发策略:提供丰富的任务触发策略,包括:Cron触发、固定间隔触发、固定延时触发、API(事件)触发、人工触发、父子任务触发; +- 8、调度过期策略:调度中心错过调度时间的补偿处理策略,包括:忽略、立即补偿触发一次等; +- 9、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度; +- 10、任务超时控制:支持自定义任务超时时间,任务运行超时将会主动中断任务; +- 11、任务失败重试:支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;其中分片任务支持分片粒度的失败重试; +- 12、任务失败告警;默认提供邮件方式失败告警,同时预留扩展接口,可方便的扩展短信、钉钉等告警方式; +- 13、路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等; +- 14、分片广播任务:执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数开发分片任务; +- 15、动态分片:分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。 +- 16、故障转移:任务路由策略选择"故障转移"情况下,如果执行器集群中某一台机器故障,将会自动Failover切换到一台正常的执行器发送调度请求。 +- 17、任务进度监控:支持实时监控任务进度; +- 18、Rolling实时日志:支持在线查看调度结果,并且支持以Rolling方式实时查看执行器输出的完整的执行日志; +- 19、GLUE:提供Web IDE,支持在线开发任务逻辑代码,动态发布,实时编译生效,省略部署上线的过程。支持30个版本的历史版本回溯。 +- 20、脚本任务:支持以GLUE模式开发和运行脚本任务,包括Shell、Python、NodeJS、PHP、PowerShell等类型脚本; +- 21、命令行任务:原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可; +- 22、任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行, 多个子任务用逗号分隔; +- 23、一致性:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行; +- 24、自定义任务参数:支持在线配置调度任务入参,即时生效; +- 25、调度线程池:调度系统多线程触发调度运行,确保调度精确执行,不被堵塞; +- 26、数据加密:调度中心和执行器之间的通讯进行数据加密,提升调度信息安全性; +- 27、邮件报警:任务失败时支持邮件报警,支持配置多邮件地址群发报警邮件; +- 28、推送maven中央仓库: 将会把最新稳定版推送到maven中央仓库, 方便用户接入和使用; +- 29、运行报表:支持实时查看运行数据,如任务数量、调度次数、执行器数量等;以及调度报表,如调度日期分布图,调度成功分布图等; +- 30、全异步:任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰,理论上支持任意时长任务的运行; +- 31、跨语言:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。除此之外,还提供了 “多任务模式”和“httpJobHandler”等其他跨语言方案; +- 32、国际化:调度中心支持国际化设置,提供中文、英文两种可选语言,默认为中文; +- 33、容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现产品开箱即用; +- 34、线程池隔离:调度线程池进行隔离拆分,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性; +- 35、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色; +- 36、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作; + + +## Development +于2015年中,我在github上创建XXL-JOB项目仓库并提交第一个commit,随之进行系统结构设计,UI选型,交互设计…… + +于2015-11月,XXL-JOB终于RELEASE了第一个大版本V1.0, 随后我将之发布到OSCHINA,XXL-JOB在OSCHINA上获得了@红薯的热门推荐,同期分别达到了OSCHINA的“热门动弹”排行第一和git.oschina的开源软件月热度排行第一,在此特别感谢红薯,感谢大家的关注和支持。 + +于2015-12月,我将XXL-JOB发表到我司内部知识库,并且得到内部同事认可。 + +于2016-01月,我司展开XXL-JOB的内部接入和定制工作,在此感谢袁某和尹某两位同事的贡献,同时也感谢内部其他给与关注与支持的同事。 + +于2017-05-13,在上海举办的 "[第62期开源中国源创会](https://www.oschina.net/event/2236961)" 的 "放码过来" 环节,我登台对XXL-JOB做了演讲,台下五百位在场观众反响热烈([图文回顾](https://www.oschina.net/question/2686220_2242120) )。 + +于2017-10-22,又拍云 Open Talk 联合 Spring Cloud 中国社区举办的 "[进击的微服务实战派上海站](https://opentalk.upyun.com/303.html)",我登台对XXL-JOB做了演讲,现场观众反响热烈并在会后与XXL-JOB用户热烈讨论交流。 + +于2017-12-11,XXL-JOB有幸参会《[InfoQ ArchSummit全球架构师峰会](http://bj2017.archsummit.com/)》,并被拍拍贷架构总监"杨波老师"在专题 "[微服务原理、基础架构和开源实践](http://bj2017.archsummit.com/training/2)" 中现场介绍。 + +于2017-12-18,XXL-JOB参与"[2017年度最受欢迎中国开源软件](http://www.oschina.net/project/top_cn_2017?sort=1)"评比,在当时已录入的约九千个国产开源项目中角逐,最终进入了前30强。 + +于2018-01-15,XXL-JOB参与"[2017码云最火开源项目](https://www.oschina.net/news/92438/2017-mayun-top-50)"评比,在当时已录入的约六千五百个码云项目中角逐,最终进去了前20强。 + +于2018-04-14,iTechPlus在上海举办的 "[2018互联网开发者大会](http://www.itdks.com/eventlist/detail/2065)",我登台对XXL-JOB做了演讲,现场观众反响热烈并在会后与XXL-JOB用户热烈讨论交流。 + +于2018-05-27,在上海举办的 "[第75期开源中国源创会](https://www.oschina.net/event/2278742)" 的 "架构" 主题专场,我登台进行“基础架构与中间件图谱”主题演讲,台下上千位在场观众反响热烈([图文回顾](https://www.oschina.net/question/3802184_2280606) )。 + +于2018-12-05,XXL-JOB参与"[2018年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2018?sort=1)"评比,在当时已录入的一万多个开源项目中角逐,最终排名第19名。 + +于2019-12-10,XXL-JOB参与"[2019年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2019)"评比,在当时已录入的一万多个开源项目中角逐,最终排名"开发框架和基础组件类"第9名。 + +于2020-11-16,XXL-JOB参与"[2020年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2020)"评比,在当时已录入的一万多个开源项目中角逐,最终排名"开发框架和基础组件类"第8名。 + +于2021-12-06,XXL-JOB参与"[2021年度OSC中国开源项目评选](https://www.oschina.net/project/top_cn_2021) "评比,在当时已录入的一万多个开源项目中角逐,最终当选"最受欢迎项目"。 + +> 我司大众点评目前已接入XXL-JOB,内部别名《Ferrari》(Ferrari基于XXL-JOB的V1.1版本定制而成,新接入应用推荐升级最新版本)。 +据最新统计, 自2016-01-21接入至2017-12-01期间,该系统已调度约100万次,表现优异。新接入应用推荐使用最新版本,因为经过数十个版本的更新,系统的任务模型、UI交互模型以及底层调度通讯模型都有了较大的优化和提升,核心功能更加稳定高效。 + +至今,XXL-JOB已接入多家公司的线上产品线,接入场景如电商业务,O2O业务和大数据作业等,截止最新统计时间为止,XXL-JOB已接入的公司包括不限于: + + - 1、大众点评【美团点评】 + - 2、山东学而网络科技有限公司; + - 3、安徽慧通互联科技有限公司; + - 4、人人聚财金服; + - 5、上海棠棣信息科技股份有限公司 + - 6、运满满【运满满】 + - 7、米其林 (中国区)【米其林】 + - 8、妈妈联盟 + - 9、九樱天下(北京)信息技术有限公司 + - 10、万普拉斯科技有限公司【一加手机】 + - 11、上海亿保健康管理有限公司 + - 12、海尔馨厨【海尔】 + - 13、河南大红包电子商务有限公司 + - 14、成都顺点科技有限公司 + - 15、深圳市怡亚通 + - 16、深圳麦亚信科技股份有限公司 + - 17、上海博莹科技信息技术有限公司 + - 18、中国平安科技有限公司【中国平安】 + - 19、杭州知时信息科技有限公司 + - 20、博莹科技(上海)有限公司 + - 21、成都依能股份有限责任公司 + - 22、湖南高阳通联信息技术有限公司 + - 23、深圳市邦德文化发展有限公司 + - 24、福建阿思可网络教育有限公司 + - 25、优信二手车【优信】 + - 26、上海悠游堂投资发展股份有限公司【悠游堂】 + - 27、北京粉笔蓝天科技有限公司 + - 28、中秀科技(无锡)有限公司 + - 29、武汉空心科技有限公司 + - 30、北京蚂蚁风暴科技有限公司 + - 31、四川互宜达科技有限公司 + - 32、钱包行云(北京)科技有限公司 + - 33、重庆欣才集团 + - 34、咪咕互动娱乐有限公司【中国移动】 + - 35、北京诺亦腾科技有限公司 + - 36、增长引擎(北京)信息技术有限公司 + - 37、北京英贝思科技有限公司 + - 38、刚泰集团 + - 39、深圳泰久信息系统股份有限公司 + - 40、随行付支付有限公司 + - 41、广州瀚农网络科技有限公司 + - 42、享点科技有限公司 + - 43、杭州比智科技有限公司 + - 44、圳临界线网络科技有限公司 + - 45、广州知识圈网络科技有限公司 + - 46、国誉商业上海有限公司 + - 47、海尔消费金融有限公司,嗨付、够花【海尔】 + - 48、广州巴图鲁信息科技有限公司 + - 49、深圳市鹏海运电子数据交换有限公司 + - 50、深圳市亚飞电子商务有限公司 + - 51、上海趣医网络有限公司 + - 52、聚金资本 + - 53、北京父母邦网络科技有限公司 + - 54、中山元赫软件科技有限公司 + - 55、中商惠民(北京)电子商务有限公司 + - 56、凯京集团 + - 57、华夏票联(北京)科技有限公司 + - 58、拍拍贷【拍拍贷】 + - 59、北京尚德机构在线教育有限公司 + - 60、任子行股份有限公司 + - 61、北京时态电子商务有限公司 + - 62、深圳卷皮网络科技有限公司 + - 63、北京安博通科技股份有限公司 + - 64、未来无线网 + - 65、厦门瓷禧网络有限公司 + - 66、北京递蓝科软件股份有限公司 + - 67、郑州创海软件科技公司 + - 68、北京国槐信息科技有限公司 + - 69、浪潮软件集团 + - 70、多立恒(北京)信息技术有限公司 + - 71、广州极迅客信息科技有限公司 + - 72、赫基(中国)集团股份有限公司 + - 73、海投汇 + - 74、上海润益创业孵化器管理股份有限公司 + - 75、汉纳森(厦门)数据股份有限公司 + - 76、安信信托 + - 77、岚儒财富 + - 78、捷道软件 + - 79、湖北享七网络科技有限公司 + - 80、湖南创发科技责任有限公司 + - 81、深圳小安时代互联网金融服务有限公司 + - 82、湖北享七网络科技有限公司 + - 83、钱包行云(北京)科技有限公司 + - 84、360金融【360】 + - 85、易企秀 + - 86、摩贝(上海)生物科技有限公司 + - 87、广东芯智慧科技有限公司 + - 88、联想集团【联想】 + - 89、怪兽充电 + - 90、行圆汽车 + - 91、深圳店店通科技邮箱公司 + - 92、京东【京东】 + - 93、米庄理财 + - 94、咖啡易融 + - 95、梧桐诚选 + - 96、恒大地产【恒大】 + - 97、昆明龙慧 + - 98、上海涩瑶软件 + - 99、易信【网易】 + - 100、铜板街 + - 101、杭州云若网络科技有限公司 + - 102、特百惠(中国)有限公司 + - 103、常山众卡运力供应链管理有限公司 + - 104、深圳立创电子商务有限公司 + - 105、杭州智诺科技股份有限公司 + - 106、北京云漾信息科技有限公司 + - 107、深圳市多银科技有限公司 + - 108、亲宝宝 + - 109、上海博卡软件科技有限公司 + - 110、智慧树在线教育平台 + - 111、米族金融 + - 112、北京辰森世纪 + - 113、云南滇医通 + - 114、广州市分领网络科技有限责任公司 + - 115、浙江微能科技有限公司 + - 116、上海馨飞电子商务有限公司 + - 117、上海宝尊电子商务有限公司 + - 118、直客通科技技术有限公司 + - 119、科度科技有限公司 + - 120、上海数慧系统技术有限公司 + - 121、我的医药网 + - 122、多粉平台 + - 123、铁甲二手机 + - 124、上海海新得数据技术有限公司 + - 125、深圳市珍爱网信息技术有限公司【珍爱网】 + - 126、小蜜蜂 + - 127、吉荣数科技 + - 128、上海恺域信息科技有限公司 + - 129、广州荔支网络有限公司【荔枝FM】 + - 130、杭州闪宝科技有限公司 + - 131、北京互联新网科技发展有限公司 + - 132、誉道科技 + - 133、山西兆盛房地产开发有限公司 + - 134、北京蓝睿通达科技有限公司 + - 135、月亮小屋(中国)有限公司【蓝月亮】 + - 136、青岛国瑞信息技术有限公司 + - 137、博雅云计算(北京)有限公司 + - 138、华泰证券香港子公司 + - 139、杭州东方通信软件技术有限公司 + - 140、武汉博晟安全技术股份有限公司 + - 141、深圳市六度人和科技有限公司 + - 142、杭州趣维科技有限公司(小影) + - 143、宁波单车侠之家科技有限公司【单车侠】 + - 144、丁丁云康信息科技(北京)有限公司 + - 145、云钱袋 + - 146、南京中兴力维 + - 147、上海矽昌通信技术有限公司 + - 148、深圳萨科科技 + - 149、中通服创立科技有限责任公司 + - 150、深圳市对庄科技有限公司 + - 151、上证所信息网络有限公司 + - 152、杭州火烧云科技有限公司【婚礼纪】 + - 153、天津青芒果科技有限公司【芒果头条】 + - 154、长飞光纤光缆股份有限公司 + - 155、世纪凯歌(北京)医疗科技有限公司 + - 156、浙江霖梓控股有限公司 + - 157、江西腾飞网络技术有限公司 + - 158、安迅物流有限公司 + - 159、肉联网 + - 160、北京北广梯影广告传媒有限公司 + - 161、上海数慧系统技术有限公司 + - 162、大志天成 + - 163、上海云鹊医 + - 164、上海云鹊医 + - 165、墨迹天气【墨迹天气】 + - 166、上海逸橙信息科技有限公司 + - 167、沅朋物联 + - 168、杭州恒生云融网络科技有限公司 + - 169、绿米联创 + - 170、重庆易宠科技有限公司 + - 171、安徽引航科技有限公司(乐职网) + - 172、上海数联医信企业发展有限公司 + - 173、良彬建材 + - 174、杭州求是同创网络科技有限公司 + - 175、荷马国际 + - 176、点雇网 + - 177、深圳市华星光电技术有限公司 + - 178、厦门神州鹰软件科技有限公司 + - 179、深圳市招商信诺人寿保险有限公司 + - 180、上海好屋网信息技术有限公司 + - 181、海信集团【海信】 + - 182、信凌可信息科技(上海)有限公司 + - 183、长春天成科技发展有限公司 + - 184、用友金融信息技术股份有限公司【用友】 + - 185、北京咖啡易融有限公司 + - 186、国投瑞银基金管理有限公司 + - 187、晋松(上海)网络信息技术有限公司 + - 188、深圳市随手科技有限公司【随手记】 + - 189、深圳水务科技有限公司 + - 190、易企秀【易企秀】 + - 191、北京磁云科技 + - 192、南京蜂泰互联网科技有限公司 + - 193、章鱼直播 + - 194、奖多多科技 + - 195、天津市神州商龙科技股份有限公司 + - 196、岩心科技 + - 197、车码科技(北京)有限公司 + - 198、贵阳市投资控股集团 + - 199、康旗股份 + - 200、龙腾出行 + - 201、杭州华量软件 + - 202、合肥顶岭医疗科技有限公司 + - 203、重庆表达式科技有限公司 + - 204、上海米道信息科技有限公司 + - 205、北京益友会科技有限公司 + - 206、北京融贯电子商务有限公司 + - 207、中国外汇交易中心 + - 208、中国外运股份有限公司 + - 209、中国上海晓圈教育科技有限公司 + - 210、普联软件股份有限公司 + - 211、北京科蓝软件股份有限公司 + - 212、江苏斯诺物联科技有限公司 + - 213、北京搜狐-狐友【搜狐】 + - 214、新大陆网商金融 + - 215、山东神码中税信息科技有限公司 + - 216、河南汇顺网络科技有限公司 + - 217、北京华夏思源科技发展有限公司 + - 218、上海东普信息科技有限公司 + - 219、上海鸣勃网络科技有限公司 + - 220、广东学苑教育发展有限公司 + - 221、深圳强时科技有限公司 + - 222、上海云砺信息科技有限公司 + - 223、重庆愉客行网络有限公司 + - 224、数云 + - 225、国家电网运检部 + - 226、杭州找趣 + - 227、浩鲸云计算科技股份有限公司 + - 228、科大讯飞【科大讯飞】 + - 229、杭州行装网络科技有限公司 + - 230、即有分期金融 + - 231、深圳法司德信息科技有限公司 + - 232、上海博复信息科技有限公司 + - 233、杭州云嘉云计算有限公司 + - 234、有家民宿(有家美宿) + - 235、北京赢销通软件技术有限公司 + - 236、浙江聚有财金融服务外包有限公司 + - 237、易族智汇(北京)科技有限公司 + - 238、合肥顶岭医疗科技开发有限公司 + - 239、车船宝(深圳)旭珩科技有限公司) + - 240、广州富力地产有限公司 + - 241、氢课(上海)教育科技有限公司 + - 242、武汉氪细胞网络技术有限公司 + - 243、杭州有云科技有限公司 + - 244、上海仙豆智能机器人有限公司 + - 245、拉卡拉支付股份有限公司【拉卡拉】 + - 246、虎彩印艺股份有限公司 + - 247、北京数微科技有限公司 + - 248、广东智瑞科技有限公司 + - 249、找钢网 + - 250、九机网 + - 251、杭州跑跑网络科技有限公司 + - 252、深圳未来云集 + - 253、杭州每日给力科技有限公司 + - 254、上海齐犇信息科技有限公司 + - 255、滴滴出行【滴滴】 + - 256、合肥云诊信息科技有限公司 + - 257、云知声智能科技股份有限公司 + - 258、南京坦道科技有限公司 + - 259、爱乐优(二手平台) + - 260、猫眼电影(私有化部署)【猫眼电影】 + - 261、美团大象(私有化部署)【美团大象】 + - 262、作业帮教育科技(北京)有限公司【作业帮】 + - 263、北京小年糕互联网技术有限公司 + - 264、山东矩阵软件工程股份有限公司 + - 265、陕西国驿软件科技有限公司 + - 266、君开信息科技 + - 267、村鸟网络科技有限责任公司 + - 268、云南国际信托有限公司 + - 269、金智教育 + - 270、珠海市筑巢科技有限公司 + - 271、上海百胜软件股份有限公司 + - 272、深圳市科盾科技有限公司 + - 273、哈啰出行【哈啰】 + - 274、途虎养车【途虎】 + - 275、卡思优派人力资源集团 + - 276、南京观为智慧软件科技有限公司 + - 277、杭州城市大脑科技有限公司 + - 278、猿辅导【猿辅导】 + - 279、洛阳健创网络科技有限公司 + - 280、魔力耳朵 + - 281、亿阳信通 + - 282、上海招鲤科技有限公司 + - 283、四川商旅无忧科技服务有限公司 + - 284、UU跑腿 + - 285、北京老虎证券【老虎证券】 + - 286、悠活省吧(北京)网络科技有限公司 + - 287、F5未来商店 + - 288、深圳环阳通信息技术有限公司 + - 289、遠傳電信 + - 290、作业帮(北京)教育科技有限公司【作业帮】 + - 291、成都科鸿智信科技有限公司 + - 292、北京木屋时代科技有限公司 + - 293、大学通(哈尔滨)科技有限责任公司 + - 294、浙江华坤道威数据科技有限公司 + - 295、吉祥航空【吉祥航空】 + - 296、南京圆周网络科技有限公司 + - 297、广州市洋葱omall电子商务 + - 298、天津联物科技有限公司 + - 299、跑哪儿科技(北京)有限公司 + - 300、深圳市美西西餐饮有限公司(喜茶) + - 301、平安不动产有限公司【平安】 + - 302、江苏中海昇物联科技有限公司 + - 303、湖南牙医帮科技有限公司 + - 304、重庆民航凯亚信息技术有限公司(易通航) + - 305、递易(上海)智能科技有限公司 + - 306、亚朵 + - 307、浙江新课堂教育股份有限公司 + - 308、北京蜂创科技有限公司 + - 309、德一智慧城市信息系统有限公司 + - 310、北京翼点科技有限公司 + - 311、湖南智数新维度信息科技有限公司 + - 312、北京玖扬博文文化发展有限公司 + - 313、上海宇珩信息科技有限公司 + - 314、全景智联(武汉)科技有限公司 + - 315、天津易客满国际物流有限公司 + - 316、南京爱福路汽车科技有限公司 + - 317、我房旅居集团 + - 318、湛江亲邻科技有限公司 + - 319、深圳市姜科网络有限公司 + - 320、青岛日日顺物流有限公司 + - 321、南京太川信息技术有限公司 + - 322、美图之家科技有限公司【美图】 + - 323、南京太川信息技术有限公司 + - 324、众薪科技(北京)有限公司 + - 325、武汉安安物联科技有限公司 + - 326、北京智客朗道网络科技有限公司 + - 327、深圳市超级猩猩健身管理管理有限公司 + - 328、重庆达志科技有限公司 + - 329、上海享评信息科技有限公司 + - 330、薪得付信息科技 + - 331、跟谁学 + - 332、中道(苏州)旅游网络科技有限公司 + - 333、广州小卫科技有限公司 + - 334、上海非码网络科技有限公司 + - 335、途家网网络技术(北京)有限公司【途家】 + - 336、广州辉凡信息科技有限公司 + - 337、天维尔信息科技股份有限公司 + - 338、上海极豆科技有限公司 + - 339、苏州触达信息技术有限公司 + - 340、北京热云科技有限公司 + - 341、中智企服(北京)科技有限公司 + - 342、易联云计算(杭州)有限责任公司 + - 343、青岛航空股份有限公司【青岛航空】 + - 344、山西博睿通科技有限公司 + - 345、网易杭州网络有限公司【网易】 + - 346、北京果果乐学科技有限公司 + - 347、百望股份有限公司 + - 348、中保金服(深圳)科技有限公司 + - 349、天津运友物流科技股份有限公司 + - 350、广东创能科技股份有限公司 + - 351、上海倚博信息科技有限公司 + - 352、深圳百果园实业(集团)股份有限公司 + - 353、广州细刻网络科技有限公司 + - 354、武汉鸿业众创科技有限公司 + - 355、金锡科技(广州)有限公司 + - 356、易瑞国际电子商务有限公司 + - 357、奇点云 + - 358、中视信息科技有限公司 + - 359、开源项目:datax-web + - 360、云知声智能科技股份有限公司 + - 361、开源项目:bboss + - 362、成都深驾科技有限公司 + - 363、FunPlus【趣加】 + - 364、杭州创匠信科技有限公司 + - 365、龙匠(北京)科技发展有限公司 + - 366、广州一链通互联网科技有限公司 + - 367、上海星艾网络科技有限公司 + - 368、虎博网络技术(上海)有限公司 + - 369、青岛优米信息技术有限公司 + - 370、八维通科技有限公司 + - 371、烟台合享智星数据科技有限公司 + - 372、东吴证券股份有限公司 + - 373、中通云仓股份有限公司【中通】 + - 374、北京加菲猫科技有限公司 + - 375、北京匠心演绎科技有限公司 + - 376、宝贝走天下 + - 377、厦门众库科技有限公司 + - 378、海通证券数据中心 + - 389、湖南快乐通宝小额贷款有限公司 + - 380、浙江大华技术股份有限公司 + - 381、杭州魔筷科技有限公司 + - 382、青岛掌讯通区块链科技有限公司 + - 383、新大陆金融科技 + - 384、常州玺拓软件科技有限公司 + - 385、北京正保网格教育科技有限公司 + - 386、统一企业(中国)投资有限公司【统一】 + - 387、微革网络科技有限公司 + - 388、杭州融易算科技有限公司 + - 399、青岛上啥班网络科技有限公司 + - 390、京东酒世界 + - 391、杭州爱博仕科技有限公司 + - 392、五星金服控股有限公司 + - 393、福建乐摩物联科技有限公司 + - 394、百炼智能科技有限公司 + - 395、山东能源数智云科技有限公司 + - 396、招商局能源运输股份有限公司 + - 397、三一集团【三一】 + - 398、东巴文(深圳)健康管理有限公司 + - 399、索易软件 + - 400、深圳市宁远科技有限公司 + - 401、熙牛医疗 + - 402、南京智鹤电子科技有限公司 + - 403、嘀嗒出行【嘀嗒出行】 + - 404、广州虎牙信息科技有限公司【虎牙】 + - 405、广州欧莱雅百库网络科技有限公司【欧莱雅】 + - 406、微微科技有限公司 + - 407、我爱我家房地产经纪有限公司【我爱我家】 + - 408、九号发现 + - 409、薪人薪事 + - 410、武汉氪细胞网络技术有限公司 + - 411、广州市斯凯奇商业有限公司 + - 412、微淼商学院 + - 413、杭州车盛科技有限公司 + - 414、深兰科技(上海)有限公司 + - 415、安徽中科美络信息技术有限公司 + - 416、比亚迪汽车工业有限公司【比亚迪】 + - 417、湖南小桔信息技术有限公司 + - 418、安徽科大国创软件科技有限公司 + - 419、克而瑞 + - 420、陕西云基华海信息技术有限公司 + - 421、安徽深宁科技有限公司 + - 422、广东康爱多数字健康有限公司 + - 423、嘉里电子商务 + - 424、上海时代光华教育发展有限公司 + - 425、CityDo + - 426、上海禹知信息科技有限公司 + - 427、广东智瑞科技有限公司 + - 428、西安爱铭网络科技有限公司 + - 429、心医国际数字医疗系统(大连)有限公司 + - 430、乐其电商 + - 431、锐达科技 + - 432、天津长城滨银汽车金融有限公司 + - 433、代码网 + - 434、东莞市东城乔伦软件开发工作室 + - 435、浙江百应科技有限公司 + - 436、上海力爱帝信息技术有限公司(Red E) + - 437、云徙科技有限公司 + - 438、北京康智乐思网络科技有限公司【大姨吗APP】 + - 439、安徽开元瞬视科技有限公司 + - 440、立方 + - 441、厦门纵行科技 + - 442、乐山-菲尼克斯半导体有限公司 + - 443、武汉光谷联合集团有限公司 + - 444、上海金仕达软件科技有限公司 + - 445、深圳易世通达科技有限公司 + - 446、爱动超越人工智能科技(北京)有限责任公司 + - 447、迪普信(北京)科技有限公司 + - 448、掌站科技(北京)有限公司 + - 449、深圳市华云中盛股份有限公司 + - 450、上海原圈科技有限公司 + - 451、广州赞赏信息科技有限公司 + - 452、Amber Group + - 453、德威国际货运代理(上海)公司 + - 454、浙江杰夫兄弟智慧科技有限公司 + - 455、信也科技 + - 456、开思时代科技(深圳)有限公司 + - 457、大连槐德科技有限公司 + - 458、同程生活 + - 459、松果出行 + - 460、企鹅杏仁集团 + - 461、宁波科云信息科技有限公司 + - 462、上海格蓝威驰信息科技有限公司 + - 463、杭州趣淘鲸科技有限公司 + - 464、湖州市数字惠民科技有限公司 + - 465、乐普(北京)医疗器械股份有限公司 + - 466、广州市晴川高新技术开发有限公司 + - 467、山西缇客科技有限公司 + - 468、徐州卡西穆电子商务有限公司 + - 469、格创东智科技有限公司 + - 470、世纪龙信息网络有限责任公司 + - 471、邦道科技有限公司 + - 472、河南中盟新云科技股份有限公司 + - 473、横琴人寿保险有限公司 + - 474、上海海隆华钟信息技术有限公司 + - 475、上海久湛 + - 476、上海仙豆智能机器人有限公司 + - 477、广州汇尚网络科技有限公司 + - 478、深圳市阿卡索资讯股份有限公司 + - 479、青岛佳家康健康管理有限责任公司 + - 480、蓝城兄弟 + - 481、成都天府通金融服务股份有限公司 + - 482、深圳云镖网络科技有限公司 + - 483、上海影创科技 + - 484、成都艾拉物联 + - 485、北京客邻尚品网络技术有限公司 + - 486、IT实战联盟 + - 487、杭州尤拉夫科技有限公司 + - 488、中大检测(湖南)股份有限公司 + - 489、江苏电老虎工业互联网股份有限公司 + - 490、上海助通信息科技有限公司 + - 491、北京符节科技有限公司 + - 492、杭州英祐科技有限公司 + - 493、江苏电老虎工业互联网股份有限公司 + - 494、深圳市点猫科技有限公司 + - 495、杭州天音 + - 496、深圳市二十一科技互联网有限公司 + - 497、海南海口翎度科技 + - 498、北京小趣智品科技有限公司 + - 499、广州石竹计算机软件有限公司 + - 500、深圳市惟客数据科技有限公司 + - 501、中国医疗器械有限公司 + - 502、上海云谦科技有限公司 + - 503、上海磐农信息科技有限公司 + - 504、广州领航食品有限公司 + - 505、青岛掌讯通区块链科技有限公司 + - 506、北京新网数码信息技术有限公司 + - 507、超体信息科技(深圳)有限公司 + - 508、长沙店帮手信息科技有限公司 + - 509、上海助弓装饰工程有限公司 + - 510、杭州寻联网络科技有限公司 + - 511、成都大淘客科技有限公司 + - 512、松果出行 + - 513、深圳市唤梦科技有限公司 + - 514、上汽集团商用车技术中心 + - 515、北京中航讯科技股份有限公司 + - 516、北龙中网(北京)科技有限责任公司 + - 517、前海超级前台(深圳)信息技术有限公司 + - 518、上海中商网络股份有限公司 + - 519、上海助通信息科技有限公司 + - 520、宁波聚臻智能科技有限公司 + - 521、上海零动数码科技股份有限公司 + - 522、浙江学海教育科技有限公司 + - 523、聚学云(山东)信息技术有限公司 + - 524、多氟多新材料股份有限公司 + - 525、智慧眼科技股份有限公司 + - 526、广东智通人才连锁股份有限公司 + - 527、世纪开元智印互联科技集团股份有限公司 + - 528、北京理想汽车【理想汽车】 + - 529、巽逸科技(重庆)有限公司 + - 530、义乌购电子商务有限公司 + - 531、深圳市珂莱蒂尔服饰有限公司 + - 532、江西国泰利民信息科技有限公司 + - 533、广西广电大数据科技有限公司 + - 534、杭州艾麦科技有限公司 + - 535、广州小滴科技有限公司 + - 536、佳缘科技股份有限公司 + - 537、上海深擎信息科技有限公司 + - 538、武商网 + - 539、福建民本信息科技有限公司 + - 540、杭州惠合信息科技有限公司 + - 541、厦门爱立得科技有限公司 + - 542、成都拟合未来科技有限公司 + - 543、宁波聚臻智能科技有限公司 + - 544、广东百慧科技有限公司 + - 545、笨马网络 + - 546、深圳市信安数字科技有限公司 + - 547、深圳市思乐数据技术有限公司 + - 548、四川绿源集科技有限公司 + - 549、湖南云医链生物科技有限公司 + - 550、杭州源诚科技有限公司 + - 551、北京开课吧科技有限公司 + - 552、北京多来点信息技术有限公司 + - 553、JEECG BOOT低代码开发平台 + - 554、苏州同元软控信息技术有限公司 + - 555、江苏大泰信息技术有限公司 + - 556、北京大禹汇智 + - 557、北京盛哲科技有限公司 + - 558、广州钛动科技有限公司 + - 559、北京大禹汇智科技有限公司 + - 560、湖南鼎翰文化股份有限公司 + - 561、苏州安软信息科技有限公司 + - 562、芒果tv + - 563、上海艺赛旗软件股份有限公司 + - 564、中盈优创资讯科技有限公司 + - 565、乐乎公寓 + - 566、启明信息 + - 567、苏州安软 + - 568、南京富金的软件科技有限公司 + - 569、深圳市新科聚合网络技术有限公司 + - 570、你好现在(北京)科技股份有限公司 + - 571、360考试宝典 + - 572、北京一零科技有限公司 + - 573、厦门星纵信息 + - 574、Dalligent Solusi Indonesia + - 575、深圳华普物联科技有限公司 + - 576、深圳行健自动化股份有限公司 + - 577、深圳市富融信息科技服务有限公司 + - 578、蓝鸟云 + - 579、上海澎博财经资讯有限公司 + - 580、北京小鸦科技有限公司 + - 581、杭州盈泉云科技有限公司 + - 582、惟客数据 + - 583、GOSO香蜜闺秀 + - 584、普乐师(上海)数字科技有限公司 + - 585、西安市雁塔区咖北堂网络科技部 + - 586、宁波聚臻智能科技有限公司 + - 587、普乐师数字科技有限公司 + - 588、江苏蟹联网科技有限公司 + - 589、杭州未智科技有限公司 + - 590、安吉智行物流有限公司 + - 591、华生大家居集团有限公司 + - 592、美心食品(广州)有限公司 + - 593、货拉拉【货拉拉APP】 + - 594、杭州思韬瑞科技有限公司 + - 595、杭州玖融科技有限公司 + - 596、北京优海网络科技有限公司 + - 597、浙江大维高新技术股份有限公司 + - 598、粤港澳大湾区数字经济研究院 + - 599、普康(杭州)健康科技有限公司 + - 600、华西证券股份有限公司【华西证券】 + - 601、杭州海康机器人股份有限公司【海康】 + - 602、河南宸邦信息技术有限公司 + - 603、成都次元节点网络科技有限公司 + - …… + +> 更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。 + +欢迎大家的关注和使用,XXL-JOB也将拥抱变化,持续发展。 + + +## Contributing +Contributions are welcome! Open a pull request to fix a bug, or open an [Issue](https://github.com/xuxueli/xxl-job/issues/) to discuss a new feature or change. + +欢迎参与项目贡献!比如提交PR修复一个bug,或者新建 [Issue](https://github.com/xuxueli/xxl-job/issues/) 讨论新特性或者变更。 + + +## Copyright and License +This product is open source and free, and will continue to provide free community technical support. Individual or enterprise users are free to access and use. + +- Licensed under the GNU General Public License (GPL) v3. +- Copyright (c) 2015-present, xuxueli. + +产品开源免费,并且将持续提供免费的社区技术支持。个人或企业内部可自由的接入和使用。如有需要可邮件联系作者免费获取项目授权。 + + +## Donate +No matter how much the donation amount is enough to express your thought, thank you very much :) [To donate](https://www.xuxueli.com/page/donate.html ) + +无论捐赠金额多少都足够表达您这份心意,非常感谢 :) [前往捐赠](https://www.xuxueli.com/page/donate.html ) diff --git a/project/xxl-job-2.4.0/doc/XXL-JOB-English-Documentation.md b/project/xxl-job-2.4.0/doc/XXL-JOB-English-Documentation.md new file mode 100644 index 00000000..792f3ead --- /dev/null +++ b/project/xxl-job-2.4.0/doc/XXL-JOB-English-Documentation.md @@ -0,0 +1,1247 @@ +## 《Distributed task scheduling framework XXL-JOB》 + +[![Actions Status](https://github.com/xuxueli/xxl-job/workflows/Java%20CI/badge.svg)](https://github.com/xuxueli/xxl-job/actions) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/) +[![GitHub release](https://img.shields.io/github/release/xuxueli/xxl-job.svg)](https://github.com/xuxueli/xxl-job/releases) +[![GitHub stars](https://img.shields.io/github/stars/xuxueli/xxl-job)](https://github.com/xuxueli/xxl-job/) +[![Docker Status](https://img.shields.io/docker/pulls/xuxueli/xxl-job-admin)](https://hub.docker.com/r/xuxueli/xxl-job-admin/) +[![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](http://www.gnu.org/licenses/gpl-3.0.html) +[![donate](https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat)](https://www.xuxueli.com/page/donate.html) + +[TOCM] + +[TOC] + +## 1. Brief introduction + +### 1.1 Overview +XXL-JOB is a distributed task scheduling framework, the core design goal is to develop quickly, learning simple, lightweight, easy to expand. Is now open source and access to a number of companies online product line, download and use it now. + +> English document update slightly delayed, Please check the Chinese version for the latest document. + +### 1.2 Features +- 1.Simple: support through the Web page on the task CRUD operation, simple operation, a minute to get started; +- 2.Dynamic: support dynamic modification of task status, pause / resume tasks, and termination of running tasks,immediate effect; +- 3.Dispatch center HA (center type): Dispatch with central design, "dispatch center" based on the cluster of Quartz implementation, can guarantee the scheduling - center HA; +- 4.Executor HA (Distributed): Task Distributed Execution, Task " Executer " supports cluster deployment to ensure that tasks perform HA; +- 5.Task Failover: Deploy the Excutor cluster,tasks will be smooth to switch excuter when the strategy of the router choose ‘failover’; +- 6.Consistency: "Dispatch Center" through the DB lock to ensure the consistency of cluster distributed scheduling,one task excuted for once; +- 7.Custom task parameters: support online configuration scheduling tasks into the parameters, immediate effect; +- 8.Scheduling thread pool: scheduling system multi-threaded trigger scheduling operation, to ensure accurate scheduling, not blocked; +- 9.Elastic expansion capacity: once the new executor machine on the line or off the assembly line, the next time scheduling will be re-assigned tasks; +- 10.Mail alarm: the task fails to support e-mail alarm, support configuring multiple email addresses to send bulk alert messages; +- 11.Status monitoring: support real-time monitoring of the progress of the task; +- 12.Rolling execution log: support online view scheduling results, and support Rolling real-time view of the executer output of the complete implementation of the log; +- 13.GLUE: provide Web IDE, support online development task logic code, dynamic release, real-time compiler effective, omit the deployment of the on-line process. Supports historical versions of 30 versions back; +- 14.Data Encryption: The communication between the dispatching center and the executor is used for data encryption, Enhancing the security of dispatching information; +- 15.Task Dependency: Support configuration subtask dependencies, When the parent task executed end and after the success of the implementation will take the initiative to trigger a second task execution, multiple sub tasks are separated by commas; +- 16.Push the Maven central warehouse: The latest stable version will be sent to the Maven central warehouse to facilitate user access and use; +- 17.Task registration: The executor automatically registers tasks periodically, and the dispatch center automatically finds the registered tasks and triggers execution. It also supports manual input of executor address; +- 18.Router strategy: A rich routing strategy is provided when the executor cluster is deployed, these include: first, last, poll, random, consistent HASH, least frequently used, least recently used, failover, busy over, sharding broadcast,etc.; +- 19.Report monitor: Support real-time view of running data, such as the number of tasks, the number of dispatch, the number of executors, etc .; and scheduling reports, such as scheduling date distribution, scheduling success map; +- 20.Script task: Support the development and operation of script tasks in GLUE mode, including shell, Python and other types of script; +- 21.Blocking handling strategy: The scheduling is too dense and the executor is too late to handle. The strategy includes: single machine serial (default), discarding the following scheduling, and Override the previous scheduling; +- 22.Failure handling strategy:Handling strategy when scheduling fails, the strategy includes: failure alarm (default), failure retry; +- 23.Sharding broadcast task: When an executor cluster is deployed, task routing strategy select "sharding broadcast", a task schedule will broadcast all the actuators in the cluster to perform it once, you can develop sharding tasks based on sharding parameters; +- 24.Dynamic sharding: The sharding broadcast task is sharded by the executors to support the dynamic expansion of the executor cluster to dynamically increase the number of shardings and cooperate with the business handle; In the large amount of data operations can significantly improve the task processing capacity and speed. +- 25、Event trigger:In addition to "Cron" and "Task Dependency" to trigger tasks, support event-based triggering tasks. The dispatch center provides API service that triggers a single execution of the task, it can be triggered flexibly according to business events. + + +### 1.3 Development +In 2015, I created the XXL-JOB project repository on github and submitted the first commit, followed by the system structure design, UI selection, interactive design ... +In 2015 - November, XXL-JOB finally RELEASE the first big version of V1.0, then I will be released to OSCHINA, XXL-JOB OSCHINA won the popular recommendation of @红薯, the same period reached OSCHINA's " Popular move "ranked first and git.oschina open source software monthly heat ranked first, especially thanks for @红薯, thank you for the attention and support. +In 2015 - December, I will XXL-JOB published to our internal knowledge base, and get internal colleagues recognized. +In 2016 - 01 months, my company started XXL-JOB internal access and custom work, in this thank Yuan and Yin two colleagues contribution, but also to thank the internal other attention and support colleagues. +In 2017-05-13, the link of "let the code run" in "[the 62nd source of open source China Genesis](https://www.oschina.net/event/2236961)" held in Shanghai,, I stepped on and made a speech about the XXL-JOB, five hundred spectators in the audience reacted enthusiastically ([pictorial review](https://www.oschina.net/question/2686220_2242120)). +> Our company have access to XXL-JOB, internal alias "Ferrari" (Ferrari based on XXL-JOB V1.1 version customization, new access application recommended to upgrade the latest version). +According to the latest statistics, from 2016-01-21 to 2017-07-07 period, the system has been scheduled about 600,000 times, outstanding performance. New access applications recommend the latest version, because after several major updates, the system's task model, UI interaction model and the underlying scheduling communication model has a greater optimization and upgrading, the core function more stable and efficient. + +So far, XXL-JOB has access to a number of companies online product line, access to scenes such as electronic commerce, O2O business and large data operations, as of 2016-07-19, XXL-JOB has access to the company But not limited to: + + - 1、大众点评【美团点评】 + - 2、山东学而网络科技有限公司; + - 3、安徽慧通互联科技有限公司; + - 4、人人聚财金服; + - 5、上海棠棣信息科技股份有限公司 + - 6、运满满【运满满】 + - 7、米其林 (中国区)【米其林】 + - 8、妈妈联盟 + - 9、九樱天下(北京)信息技术有限公司 + - 10、万普拉斯科技有限公司【一加手机】 + - 11、上海亿保健康管理有限公司 + - 12、海尔馨厨【海尔】 + - 13、河南大红包电子商务有限公司 + - 14、成都顺点科技有限公司 + - 15、深圳市怡亚通 + - 16、深圳麦亚信科技股份有限公司 + - 17、上海博莹科技信息技术有限公司 + - 18、中国平安科技有限公司【中国平安】 + - 19、杭州知时信息科技有限公司 + - 20、博莹科技(上海)有限公司 + - 21、成都依能股份有限责任公司 + - 22、湖南高阳通联信息技术有限公司 + - 23、深圳市邦德文化发展有限公司 + - 24、福建阿思可网络教育有限公司 + - 25、优信二手车【优信】 + - 26、上海悠游堂投资发展股份有限公司【悠游堂】 + - 27、北京粉笔蓝天科技有限公司 + - 28、中秀科技(无锡)有限公司 + - 29、武汉空心科技有限公司 + - 30、北京蚂蚁风暴科技有限公司 + - 31、四川互宜达科技有限公司 + - 32、钱包行云(北京)科技有限公司 + - 33、重庆欣才集团 + - 34、咪咕互动娱乐有限公司【中国移动】 + - 35、北京诺亦腾科技有限公司 + - 36、增长引擎(北京)信息技术有限公司 + - 37、北京英贝思科技有限公司 + - 38、刚泰集团 + - 39、深圳泰久信息系统股份有限公司 + - 40、随行付支付有限公司 + - 41、广州瀚农网络科技有限公司 + - 42、享点科技有限公司 + - 43、杭州比智科技有限公司 + - 44、圳临界线网络科技有限公司 + - 45、广州知识圈网络科技有限公司 + - 46、国誉商业上海有限公司 + - 47、海尔消费金融有限公司,嗨付、够花【海尔】 + - 48、广州巴图鲁信息科技有限公司 + - 49、深圳市鹏海运电子数据交换有限公司 + - 50、深圳市亚飞电子商务有限公司 + - 51、上海趣医网络有限公司 + - 52、聚金资本 + - 53、北京父母邦网络科技有限公司 + - 54、中山元赫软件科技有限公司 + - 55、中商惠民(北京)电子商务有限公司 + - 56、凯京集团 + - 57、华夏票联(北京)科技有限公司 + - 58、拍拍贷【拍拍贷】 + - 59、北京尚德机构在线教育有限公司 + - 60、任子行股份有限公司 + - 61、北京时态电子商务有限公司 + - 62、深圳卷皮网络科技有限公司 + - 63、北京安博通科技股份有限公司 + - 64、未来无线网 + - 65、厦门瓷禧网络有限公司 + - 66、北京递蓝科软件股份有限公司 + - 67、郑州创海软件科技公司 + - 68、北京国槐信息科技有限公司 + - 69、浪潮软件集团 + - 70、多立恒(北京)信息技术有限公司 + - 71、广州极迅客信息科技有限公司 + - 72、赫基(中国)集团股份有限公司 + - 73、海投汇 + - 74、上海润益创业孵化器管理股份有限公司 + - 75、汉纳森(厦门)数据股份有限公司 + - 76、安信信托 + - 77、岚儒财富 + - 78、捷道软件 + - 79、湖北享七网络科技有限公司 + - 80、湖南创发科技责任有限公司 + - 81、深圳小安时代互联网金融服务有限公司 + - 82、湖北享七网络科技有限公司 + - 83、钱包行云(北京)科技有限公司 + - 84、360金融【360】 + - 85、易企秀 + - 86、摩贝(上海)生物科技有限公司 + - 87、广东芯智慧科技有限公司 + - 88、联想集团【联想】 + - 89、怪兽充电 + - 90、行圆汽车 + - 91、深圳店店通科技邮箱公司 + - 92、京东【京东】 + - 93、米庄理财 + - 94、咖啡易融 + - 95、梧桐诚选 + - 96、恒大地产【恒大】 + - 97、昆明龙慧 + - 98、上海涩瑶软件 + - 99、易信【网易】 + - 100、铜板街 + - 101、杭州云若网络科技有限公司 + - 102、特百惠(中国)有限公司 + - 103、常山众卡运力供应链管理有限公司 + - 104、深圳立创电子商务有限公司 + - 105、杭州智诺科技股份有限公司 + - 106、北京云漾信息科技有限公司 + - 107、深圳市多银科技有限公司 + - 108、亲宝宝 + - 109、上海博卡软件科技有限公司 + - 110、智慧树在线教育平台 + - 111、米族金融 + - 112、北京辰森世纪 + - 113、云南滇医通 + - 114、广州市分领网络科技有限责任公司 + - 115、浙江微能科技有限公司 + - 116、上海馨飞电子商务有限公司 + - 117、上海宝尊电子商务有限公司 + - 118、直客通科技技术有限公司 + - 119、科度科技有限公司 + - 120、上海数慧系统技术有限公司 + - 121、我的医药网 + - 122、多粉平台 + - 123、铁甲二手机 + - 124、上海海新得数据技术有限公司 + - 125、深圳市珍爱网信息技术有限公司【珍爱网】 + - 126、小蜜蜂 + - 127、吉荣数科技 + - 128、上海恺域信息科技有限公司 + - 129、广州荔支网络有限公司【荔枝FM】 + - 130、杭州闪宝科技有限公司 + - 131、北京互联新网科技发展有限公司 + - 132、誉道科技 + - 133、山西兆盛房地产开发有限公司 + - 134、北京蓝睿通达科技有限公司 + - 135、月亮小屋(中国)有限公司【蓝月亮】 + - 136、青岛国瑞信息技术有限公司 + - 137、博雅云计算(北京)有限公司 + - 138、华泰证券香港子公司 + - 139、杭州东方通信软件技术有限公司 + - 140、武汉博晟安全技术股份有限公司 + - 141、深圳市六度人和科技有限公司 + - 142、杭州趣维科技有限公司(小影) + - 143、宁波单车侠之家科技有限公司【单车侠】 + - 144、丁丁云康信息科技(北京)有限公司 + - 145、云钱袋 + - 146、南京中兴力维 + - 147、上海矽昌通信技术有限公司 + - 148、深圳萨科科技 + - 149、中通服创立科技有限责任公司 + - 150、深圳市对庄科技有限公司 + - 151、上证所信息网络有限公司 + - 152、杭州火烧云科技有限公司【婚礼纪】 + - 153、天津青芒果科技有限公司【芒果头条】 + - 154、长飞光纤光缆股份有限公司 + - 155、世纪凯歌(北京)医疗科技有限公司 + - 156、浙江霖梓控股有限公司 + - 157、江西腾飞网络技术有限公司 + - 158、安迅物流有限公司 + - 159、肉联网 + - 160、北京北广梯影广告传媒有限公司 + - 161、上海数慧系统技术有限公司 + - 162、大志天成 + - 163、上海云鹊医 + - 164、上海云鹊医 + - 165、墨迹天气【墨迹天气】 + - 166、上海逸橙信息科技有限公司 + - 167、沅朋物联 + - 168、杭州恒生云融网络科技有限公司 + - 169、绿米联创 + - 170、重庆易宠科技有限公司 + - 171、安徽引航科技有限公司(乐职网) + - 172、上海数联医信企业发展有限公司 + - 173、良彬建材 + - 174、杭州求是同创网络科技有限公司 + - 175、荷马国际 + - 176、点雇网 + - 177、深圳市华星光电技术有限公司 + - 178、厦门神州鹰软件科技有限公司 + - 179、深圳市招商信诺人寿保险有限公司 + - 180、上海好屋网信息技术有限公司 + - 181、海信集团【海信】 + - 182、信凌可信息科技(上海)有限公司 + - 183、长春天成科技发展有限公司 + - 184、用友金融信息技术股份有限公司【用友】 + - 185、北京咖啡易融有限公司 + - 186、国投瑞银基金管理有限公司 + - 187、晋松(上海)网络信息技术有限公司 + - 188、深圳市随手科技有限公司【随手记】 + - 189、深圳水务科技有限公司 + - 190、易企秀【易企秀】 + - 191、北京磁云科技 + - 192、南京蜂泰互联网科技有限公司 + - 193、章鱼直播 + - 194、奖多多科技 + - 195、天津市神州商龙科技股份有限公司 + - 196、岩心科技 + - 197、车码科技(北京)有限公司 + - 198、贵阳市投资控股集团 + - 199、康旗股份 + - 200、龙腾出行 + - 201、杭州华量软件 + - 202、合肥顶岭医疗科技有限公司 + - 203、重庆表达式科技有限公司 + - 204、上海米道信息科技有限公司 + - 205、北京益友会科技有限公司 + - 206、北京融贯电子商务有限公司 + - 207、中国外汇交易中心 + - 208、中国外运股份有限公司 + - 209、中国上海晓圈教育科技有限公司 + - 210、普联软件股份有限公司 + - 211、北京科蓝软件股份有限公司 + - 212、江苏斯诺物联科技有限公司 + - 213、北京搜狐-狐友【搜狐】 + - 214、新大陆网商金融 + - 215、山东神码中税信息科技有限公司 + - 216、河南汇顺网络科技有限公司 + - 217、北京华夏思源科技发展有限公司 + - 218、上海东普信息科技有限公司 + - 219、上海鸣勃网络科技有限公司 + - 220、广东学苑教育发展有限公司 + - 221、深圳强时科技有限公司 + - 222、上海云砺信息科技有限公司 + - 223、重庆愉客行网络有限公司 + - 224、数云 + - 225、国家电网运检部 + - 226、杭州找趣 + - 227、浩鲸云计算科技股份有限公司 + - 228、科大讯飞【科大讯飞】 + - 229、杭州行装网络科技有限公司 + - 230、即有分期金融 + - 231、深圳法司德信息科技有限公司 + - 232、上海博复信息科技有限公司 + - 233、杭州云嘉云计算有限公司 + - 234、有家民宿(有家美宿) + - 235、北京赢销通软件技术有限公司 + - 236、浙江聚有财金融服务外包有限公司 + - 237、易族智汇(北京)科技有限公司 + - 238、合肥顶岭医疗科技开发有限公司 + - 239、车船宝(深圳)旭珩科技有限公司) + - 240、广州富力地产有限公司 + - 241、氢课(上海)教育科技有限公司 + - 242、武汉氪细胞网络技术有限公司 + - 243、杭州有云科技有限公司 + - 244、上海仙豆智能机器人有限公司 + - 245、拉卡拉支付股份有限公司【拉卡拉】 + - 246、虎彩印艺股份有限公司 + - 247、北京数微科技有限公司 + - 248、广东智瑞科技有限公司 + - 249、找钢网 + - 250、九机网 + - 251、杭州跑跑网络科技有限公司 + - 252、深圳未来云集 + - 253、杭州每日给力科技有限公司 + - 254、上海齐犇信息科技有限公司 + - 255、滴滴出行【滴滴】 + - 256、合肥云诊信息科技有限公司 + - 257、云知声智能科技股份有限公司 + - 258、南京坦道科技有限公司 + - 259、爱乐优(二手平台) + - 260、猫眼电影(私有化部署)【猫眼电影】 + - 261、美团大象(私有化部署)【美团大象】 + - 262、作业帮教育科技(北京)有限公司【作业帮】 + - 263、北京小年糕互联网技术有限公司 + - 264、山东矩阵软件工程股份有限公司 + - 265、陕西国驿软件科技有限公司 + - 266、君开信息科技 + - 267、村鸟网络科技有限责任公司 + - 268、云南国际信托有限公司 + - 269、金智教育 + - 270、珠海市筑巢科技有限公司 + - 271、上海百胜软件股份有限公司 + - 272、深圳市科盾科技有限公司 + - 273、哈啰出行 + - 274、途虎养车 + - 275、卡思优派人力资源集团 + - 276、南京观为智慧软件科技有限公司 + - 277、杭州城市大脑科技有限公司 + - 278、猿辅导 + - …… + +> The company that access and use this product is welcome to register at the [address](https://github.com/xuxueli/xxl-job/issues/1 ), only for product promotion. + +Welcome everyone's attention and use, XXL-JOB will also embrace changes, sustainable development. + +### 1.4 Download + +#### Documentation +- [中文文档](https://www.xuxueli.com/xxl-job/) +- [English Documentation](https://www.xuxueli.com/xxl-job/en/) + +#### Source repository address (The latest code will be released in the two git warehouse in the same time) + +Source repository address | Release Download +--- | --- +[https://github.com/xuxueli/xxl-job](https://github.com/xuxueli/xxl-job) | [Download](https://github.com/xuxueli/xxl-job/releases) +[http://gitee.com/xuxueli0323/xxl-job](http://gitee.com/xuxueli0323/xxl-job) | [Download](http://gitee.com/xuxueli0323/xxl-job/releases) + +#### Center repository address (The latest Release version:1.8.1) +``` + + + com.xuxueli + xxl-job-core + 1.8.2 + +``` + +#### Technical exchange group +- [社区交流](https://www.xuxueli.com/page/community.html) +- [Gitter](https://gitter.im/xuxueli/xxl-job) + +### 1.5 Environment +- JDK:1.7+ +- Servlet/JSP Spec:3.1/2.3 +- Tomcat:8.5.x/Jetty9.2.x +- Spring-boot:1.5.x/Spring4.x +- Mysql:5.6+ +- Maven:3+ + + +## 2. Quick Start + +### 2.1 Init database +Please download project source code,get db scripts and execute, it will generate 16 tables if succeed. + +The relative path of db scripts is as follows: + + /xxl-job/doc/db/tables_xxl_job.sql + +The xxl-job-admin can be deployed as a cluster,all nodes of the cluster must connect to the same mysql instance. + +If mysql instances is deployed in master-slave mode,all nodes of the cluster must connect to master instace. + +### 2.2 Compile +Source code is organized by maven,unzip it and structure is as follows: + + xxl-job-admin:schedule admin center + xxl-job-core:public common dependent library + xxl-job-executor:executor Sample(Select appropriate version of executor,Can be used directly,You can also refer to it and transform existing projects into executors) + :xxl-job-executor-sample-spring:Spring version,executors managed by Spring,general and recommend; + :xxl-job-executor-sample-springboot:Springboot version,executors managed by Springboot; + +### 2.3 Configure and delploy "Schedule Center" + + schedule center project:xxl-job-admin + target:Centralized management、Schedule and trigger task + +#### Step 1:Configure Schedule Center +Configure file’s path of schedule center is as follows: + + /xxl-job/xxl-job-admin/src/main/resources/application.properties + + +The concrete contet describe as follows: + + ### JDBC connection info of schedule center:keep Consistent with chapter 2.1 + xxl.job.db.driverClass=com.mysql.jdbc.Driver + xxl.job.db.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai + xxl.job.db.user=root + xxl.job.db.password=root_pwd + + ### Alarm mailbox + xxl.job.mail.host=smtp.163.com + xxl.job.mail.port=25 + xxl.job.mail.username=ovono802302@163.com + xxl.job.mail.password=asdfzxcv + xxl.job.mail.sendFrom=ovono802302@163.com + xxl.job.mail.sendNick=《任务调度平台XXL-JOB》 + + ### Login account + xxl.job.login.username=admin + xxl.job.login.password=123456 + + ### TOKEN used for communication between the executor and schedule center, enabled if it’s not null + xxl.job.accessToken= + + ### Internationalized Settings, the default is Chinese version,Switch to English when the value is "en". + xxl.job.i18n=en + +#### Step 2:Deploy: +If you has finished step 1,then you can compile the project in maven and deploy the war package to tomcat. +the url to visit is :http://localhost:8080/xxl-job-admin (this address will be used by executor and use it as callback url),the index page after login in is as follow + +![index page after login in](https://www.xuxueli.com/doc/static/xxl-job/images/img_6yC0.png "index page after login in") + +Now,the “xxl-job-admin” project is deployed success. + +#### Step3:schedule center Cluster(Option): +xxl-job-admin can be deployed as a cluster to improve system availability. + +Prerequisites for cluster is to keep all node configuration(db and login account info) consistent with each other. Different xxl-job-admin cluster distinguish with each other by db configuration. + +xxl-job-admin can be visited through nginx proxy and configure a domain for nginx,and the domain url can be configured as the executor’s callback url. + +### 2.4 Configur and Deploy "xxl-job-executor-example" + + Executor Project:xxl-job-executor-example (if you want to create new executor project you can refer this demo); + Target:receive xxl-job-admin’s schedule command and execute it; + +#### Step 1:import maven dependence +Pleast confirm import xxl-job-core jar in pom.xml; + +#### Step 2:Executor Configuration +Relative path of the executor configuration file is as follows: + + /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-spring/src/main/resources/xxl-job-executor.properties + +The concret content of configuration file as follows: + + ### xxl-job admin address list:xxl-job-admin address list: Multiple addresses are separated by commas,this address is used for "heart beat and register" and "task execution result callback" between the executor and xxl-job-admin. + xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin + + ### xxl.job.executor.appname is used to group by executors + xxl.job.executor.appname=xxl-job-executor-sample + ### xxl.job.executor.ip :1,used to register with xxl-job-admin;2,xxl-job-admin dispatch task to executor through it;3,if it is blank executor will get ip automatically, multi network card need to be configured. + xxl.job.executor.ip= + ### xxl.job.executor.port :the port of the executor runned by,if multiple executor instance run on the same computer the port must different with each other + xxl.job.executor.port=9999 + + ### xxl-job log path:runtime log path of the job instance + xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler/ + + ### xxl-job, access token:xxl-job access token,enabled if it not blank + xxl.job.accessToken= + + +#### Step 3:executor configuration + +configure file path of executor: + + /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-spring/src/main/resources/applicationcontext-xxl-job.xml + +Concrete contet describe as follows: + +``` + + + + + + + + + + + + + + + + + + +``` + +#### Step 4:deploy executor project +You can compile and package the project If have done all the steps above successfully,the project supply two executor demo projects,you can choose any one to deploy: + + xxl-job-executor-sample-spring:compile and package in WAR,can be deployed to tomcat; + xxl-job-executor-sample-springboot:compile and package in JAR,and run in springboot mode; + +Now you have deployed the executor project. + +#### Step 5:executor cluster(optional) +In order to improve system availability and job process capacity,executor project can be deployed as cluster. + +Prerequisites:keep all node’s configuration item "xxl.job.admin.addresses" exactly the same with each other,all executors can be register automatically. + + +### 2.5 Start first job "Hello World" +Now let’s create a "GLUE模式(Java)" job,if you want to learn more about it , please see “chapter 3:Task details”。( "GLUE模式(Java)"'s code is maintained online through xxl-job-admin,compare with "Bean模式任务" it’s not need to develop, deploy the code on the executor and it’s not need to restart the executor, so it’s lightweight) + +#### Prerequisites:please confirm xxl-job-admin and executor project has been deployed successfully. + +#### Step 1:Create new job +Login in xxl-job-admin,click on the"新建任务" button, configure the job params as follows and click "保存" button to save the job info. + +![task management](https://www.xuxueli.com/doc/static/xxl-job/images/img_o8HQ.png "task management") + +![create task](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "create task") + +#### Step 2:develop “GLUE模式(Java)” job +Click “GLUE” button on the right of the job to go to GLUE editor view as shown below。“GLUE模式(Java)” mode task has been inited with default task code for printing Hello World。 ( “GLUE模式(Java)” mode task is a java code fragment implements IJobHandler interface,it will be executed in executor,you can use @Resource/@Autowire to inject other java bean instance,if you want to see more info please go to chapter 3) + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Fgql.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题") + +#### Step 3:trigger task +If you want to run the job manually please click "执行" button on the right of the job(usually we trigger job by Cron expression) + +#### Step 4:view log +Click “日志” button on the right side of the task you will go to the task log list ,you will see the schedule history records of the task and the schedule detail info,execution info and execution params.If you click the “执行日志” button on the right side of the task log record,you will go to log console and view the execute log in the course of task execution. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_inc8.png "在这里输入图片标题") + +On the log console,you can view task execution log on the executor immediately after it dump to log file,so you can monitor the task execution process by Rolling way. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_eYrv.png "在这里输入图片标题") + +## 3. Task details + +### Description of configuration item: + + - 执行器:the container where job executed in,it will be discovered automaticly if it has registered success when job was scheduled,and the job will be executed automaticly through this way.On the other side all tasks was grouped by this way.Tasks must be binded to a executor and it can be configured on "执行器管理" page; + - 描述:the decription of task + - 路由策略:when executors deployed as a cluster,it can configure multi route policys,include: + FIRST(第一个):default select the first executor; + LAST(最后一个):default select the last executor; + ROUND(轮询):round select the executor;; + RANDOM(随机):random select the executor; + CONSISTENT_HASH(一致性HASH):all jobs was evenly scheduled on different machines,make sure load balance of executors under the same group and the same job will be scheduled to the same machine. + LEAST_FREQUENTLY_USED(最不经常使用):default select the least often used executor. + LEAST_RECENTLY_USED(最近最久未使用):defalut select the longest not used executor. + FAILOVER(故障转移):beat with the executor in order and select the first beat success executor as target executor. + BUSYOVER(忙碌转移):check the executor busy or not in order,the first executor checked not busy is to be select as the target scheduled executor. + SHARDING_BROADCAST(分片广播):broadcast all executor nodes under the same executor group execute the job, slice number will be transferred at the same time,shard task will be executed accordate with the shard number. + + - Cron:Cron expression used to trigger job execution; + - 运行模式: + BEAN模式:job was maintained on the side of executor by as JobHandler instance,it will be executed accordate with "JobHandler" properties. + GLUE模式(Java):task source code is maintened in the schedule center,it must implement IJobHandler and explain by "groovy" in the executor instance,inject other bean instace by annotation @Resource/@Autowire. + GLUE模式(Shell):it’s source code is a shell script and maintained in the schedule center. + GLUE模式(Python):it’s source code is a python script and maintained in the schedule center. + - JobHandler:it’s used in "BEAN模式",it’s instance is defined by annotation @JobHandler on the JobHandler class name. + - 子任务Key:every task has a unique key (task Key can acquire from task list),when main task is done successfully it’s child task stand for by this key will be scheduled. + - 阻塞处理策略:the stategy handle the task when this task is scheduled too frequently and the task is block to wait for cpu time. + 单机串行(默认):task schedule request go into the FIFO queue and execute serially. + 丢弃后续调度:the schedule request will be discarded and marked as fail when the same task’s instance scheduled befor is running in the target executor. + 覆盖之前调度:the schedule request will be executed and clear before task queue when the same task’s instance scheduled befor is running in the target executor. + - 失败处理策略:handle policy for schedule fail + 失败告警(默认):it will trigger alarm such as send alarm mail when it’s scheduled fail. + 失败重试:it will try another time when it’s scheduled fai,if try fail it will trigger alarm for fail.every time it will trigger a new schedule request. + - 执行参数:the params needed in the run time of the task, multiple values are separated by commas,it will be passed to task instace as an array when task is scheduled. + - 报警邮件:the email used to receive the alarm mail when task is scheduled fail or execute fail, multiple values are separated by commas. + - 负责人:The person name response for the task. + +### 3.1 BEAN模式 +The task logic exist in the executor project as JobHandler,the develop steps as shown below: + +#### Step 1:develp obHandler in the executor project + - 1, create new java class implent com.xxl.job.core.handler.IJobHandler; + - 2, if you add @Component annotation on the top of the class name it’s will be managed as a bean instance by spring container; + - 3, add “@JobHandler(value=" customize jobhandler name")” annotation,the value stand for JobHandler name,it will be used as JobHandler property when create a new task in the schedule center. + + +#### Step 2:create task in schedule center +If you want learn more about configure item please go and sedd “Description of configuration item”,select "BEAN模式" as run mode,property JobHandler please fill in the value defined by @JobHande. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "在这里输入图片标题") + +### 3.2 GLUE模式(Java) +Task source code is maintained in the schedule center and can be updated by Web IDE online, it will be compiled and effective real-time,didn’t need to assign JobHandler,develop flow shown as below: + +#### Step 1:create task in schedule center +If you want learn more about configure item please go and sedd “Description of configuration item”,select "GLUE模式(Java)" as run mode. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tJOq.png "在这里输入图片标题") + +#### Step 2:develop task source code +Select the task record and click “GLUE” button on the righe of it,it will go to GLUE task’s WEB IDE page,on this page yo can edit you task code(also can edit in other IDE tools,copy and paste into this page). + +Version backtrack(support 30 versions while backtrack):on the WEB IDE page of GLUE task,on upper right corner drop down box please select “版本回溯”,it will display GLUE updated history,select the version you want it will display the source code of this version,it will backtrace the version while click save button. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题") + +### 3.3 GLUE模式(Shell) + +#### Step 1:create new task in schedule center +If you want learn more about configure item please go and sedd “Description of configuration item”,select "GLUE模式(Shell)"as run mode. + +#### Step 2:develop task source code +Select the task record and click “GLUE” button on the righe of it,it will go to GLUE task’s WEB IDE page,on this page yo can edit you task code(also can edit in other IDE tools,copy and paste into this page). + +Actually it is a shell script fragment. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_iUw0.png "在这里输入图片标题") + +### 3.4 GLUE模式(Python) + +#### Step 1:create new task in schedule center +If you want learn more about configure item please go and sedd “Description of configuration item”,select "GLUE模式(Python)"as run mode. + +#### Step 2:develop task source code +Select the task record and click “GLUE” button on the righe of it,it will go to GLUE task’s WEB IDE page,on this page yo can edit you task code(also can edit in other IDE tools,copy and paste into this page). + +Actually it is a python script fragment. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_BPLG.png "在这里输入图片标题") + + +## 4. Task Management +### 4.0 configure executor +click"执行器管理" on the left menu,it will go to the page as shown below: +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Hr2T.png "在这里输入图片标题") + +    1,"调度中心OnLine”:display schedule center machine list,when task is scheduled it will callback schedule center for notify the execution result in failover mode, so that it can avoid a single point scheduler; +    2,"执行器列表" :display all nodes under this executor group. + +If you want to create a new executor,please click "+新增执行器" button: +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_V3vF.png "在这里输入图片标题") + +### Description of executor attributes + + Appname: the unique identity of the executor cluster,executor will registe automatically and periodically by appname so that it can be scheduled. + 名称: the name of ther executor,it is used to describe the executor. + 排序: the order of executor,it will be used in the place where need to select executor. + 注册方式:which way the schedule center used to acquire executor address through; + 自动注册:executor will register automatically,through this schedule center can discover executor dynamically. + 手动录入:fill in executor address manually and it will be used by schedule center, multiple address separated by commas. + 机器地址:only effective when "注册方式" is "手动录入",support fill in executor address manually. + +### 4.1 create new task +Go to task management list page,click “新增任务” button on the upper right corner,on the pop-up window“新增任务”page configure task property and save.learn more info please go and see "3,task details". + +### 4.2 edit task +Go to task management list page and choose the task you want to edit ,click”编辑”button on the right side of the task,on the pop-up window “编辑任务”page edit task property and save. + +### 4.3 edit GLUE source code + +Only fit to GLUE task. + +choose the task you want to edit and click” GLUE”button on the right side of the task, it will go to the Web IDE page of GLUE task,then you can edit task source code on this page.you can read "3.2 GLUE模式(Java)" for more info. + +### 4.4 pause/recover task +You can pause or recover task but it just fit to follow up schedule trigger and won’t affect scheduled tasks,if you want to stop tasks which has been triggered,please go and see “4.8 stop the running task” + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题") + +### 4.5 manually trigger +You can trigger a task manually by Click “执行”button,it won’t affect original scheduling rules. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Z5wp.png "在这里输入图片标题") + +### 4.6 view schedule log +You can view task’s history schedule log by click “日志” button,on the history schedule log list page you can view every time of task’s schedule result,execution result and so on,click “执行日志” button can view the task’s full execute log. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_9235.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_inc8.png "在这里输入图片标题") + + 调度时间:schedule center trigger time when schedule and send execution signal to executor; + 调度结果:schedule center trigger task’s result, 200 represent success,500 or other number stands for fail; + 调度备注:schedule center trigger task’s remark info; + 执行器地址:the machine address where the task was executed; + 运行模式:run mode of triggered task,go and see "3,Task Details" for more info; + 任务参数:the input params of the executed task; + 执行时间:the callback time task was done in the executor; + 执行结果:task’s execute result in the executor, 200 represent success,500 or other number stands for fail; + 执行备注:task’s execute remark info in the executor; + 操作: + "执行日志"button:click this button you can view task’s execution detail log,go and see chapter 4.7 “view execution log” for more info; + "终止任务"button:click this button you can stop the task’s execution thread on this executor,include bloked task instance which didn’t has started; + +### 4.7 view execution log +Click the “执行日志” button on the right side of the record,you can go to the execution log page,you can view the full execution log of the logic business code, shown as below: + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tvGI.png "在这里输入图片标题") + +### 4.8 stop running tasks +Just fit to running tasks,on the task log list page,click “终止任务” button on the right side of the record, it will send stop command to the executor where the task was executed,finally the task was killed and the task instance execute queue of this task will be clear. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_hIci.png "在这里输入图片标题") + +It is implemented by interrupt execute thread, it will trigger InterruptedException.so if JobHandler catch this execuption and handle this exception this function is unavailable. + +So if you want stop the running task ,the JobHandler need to handle InterruptedException separately by throw this exception.the right logic is as shown below: +``` +try{ + // do something +} catch (Exception e) { + if (e instanceof InterruptedException) { + throw e; + } + logger.warn("{}", e); +} +``` + +If JobHandler start child thread,child thread also must not catch InterruptedException,and it should throw exception. + + +### 4.9 delete execution log +On the task log list page, after you select executor and task, you can click"删除" button on the right side and it will pop-up "日志清理" window,on the pop-up window you can choose different log delete policy,choose the policy you want to execute and click "确定" button it will delele relative logs: +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Ypik.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_EB65.png "在这里输入图片标题") + +### 4.10 delete task +Click the delete button on the right side of the task,the task will be deteted. + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Z9Qr.png "在这里输入图片标题") + +## 5. Overall design +### 5.1 Source directory introduction + - /doc :documentation and material + - /db :db scripts + - /xxl-job-admin :schedule and admin center + - /xxl-job-core :common core Jar +    - /xxl-job-executor-samples :executor,Demo project(you can develop on this demo project or adjust your own exist project to executor project) + +### 5.2 configure database +XXL-JOB schedule module is implemented based on Quartz cluster,it’s “database” is extended based on Quartz’s 11 mysql tables. + +XXL-JOB custom Quartz table structure prefix(XXL_JOB_QRTZ_). + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_bNwm.png "在这里输入图片标题") + +The added tables as shown below: + - XXL_JOB_QRTZ_TRIGGER_GROUP:executor basic table, maintain the info about the executor; + - XXL_JOB_QRTZ_TRIGGER_REGISTRY:executor register table, maintain addressed of online executors and schedule center machines. + - XXL_JOB_QRTZ_TRIGGER_INFO:schedule extend table,it is used to save XXL-JOB schedule extended info,such as task group,task name,machine address,executor,input params of task and alarm email and so on. + - XXL_JOB_QRTZ_TRIGGER_LOG:schedule log table,it is used to save XXL-JOB task’s histry schedule info,such as :schedule result,execution result,input param of scheduled task,scheduled machine and executor and so on. + - XXL_JOB_QRTZ_TRIGGER_LOGGLUE:schedule log table,it is used to save XXL-JOB task’s histry schedule info,such as :schedule result,execution result,input param of scheduled task,scheduled machine and executor and so on. + +So XXL-JOB database total has 16 tables. + +### 5.3 Architecture design +#### 5.3.1 Design target +All schedule behavior has been abstracted into “schedule center” common platform , it dosen’t include business logic and just responsible for starting schedule requests. + +All tasks was abstracted into separate JobHandler and was managed by executors, executor is responsible for receiving schedule request and execute the relative JobHandler business. + +So schedule and task can be decoupled from each other, by the way it can improve the overall stability and scalability of the system. + +#### 5.3.2 System composition +- **Schedule module(schedule center)**: + it is responsible for manage schedule info,send schedule request accord task configuration and it is not include an business code.schedule system decouple with the task, improve the overall stability and scalability of the system, at the same time schedule system performance is no longer limited to task modules. + Support visualization, simple and dynamic management schedule information, include create,update,delete, GLUE develop and task alarm and so on, All of the above operations will take effect in real time,support monitor schedule result and execution log and executor failover. +- **Executor module(Executor)**: + it is responsible for receive schedule request and execute task logic,task module focuses on the execution of the task, Development and maintenance is simpler and more efficient. + Receive execution request, end request and log request from schedule center. + +#### 5.3.3 Architecture diagram + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Qohm.png "在这里输入图片标题") + +### 5.4 Schedule module analysis +#### 5.4.1 Disadvantage of quartz +Quartz is a good open source project and was often as the first choice for job schedule.Tasks was managed by api in quartz cluster so it can avoid some disadvantages of single quartz instance,but it also has some disadvantage as shown below: + - problem 1:it is not humane while operate task by call apill. + - problem 2:it is need to store business QuartzJobBean into database, System Invasion is quite serious. + - problem 3:schedule logic and couple with QuartzJobBean in the same project,it will lead a problem in case that if schedule tasks gradually increased and task logic gradually increased,under this situation the performance of the schedule system will be greatly limited by business. +XXL-JOB solve above problems of quartz. + +#### 5.4.2 RemoteHttpJobBean +Under Quartz develop,task logic often was maintained by QuartzJobBean, couple is very serious.in XXL-JOB"Schedule module" and "task module" are completely decoupled,all scheduled tasks in schedule module use the same QuartzJobBean called RemoteHttpJobBean.the params of the tasks was maintained in the extended tables,when trigger RemoteHttpJobBean,it will parse different params and start remote cal l and it wil call relative remote executor. + +This call module is like RPC,RemoteHttpJobBean provide call proxy functionality,the executor is provided as remote service. + +#### 5.4.3 Schedule Center HA(Cluster) +It is based on Quartz cluster,databse use Mysql;while QUARTZ task schedule is used in Clustered Distributed Concurrent Environment,all nodes will report task info and store into database.it will fetch trigger from database while execute task,if trigger name and execute time is the same only one node will execute the task. + +``` +# for cluster +org.quartz.jobStore.tablePrefix = XXL_JOB_QRTZ_ +org.quartz.scheduler.instanceId: AUTO +org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX +org.quartz.jobStore.isClustered: true +org.quartz.jobStore.clusterCheckinInterval: 1000 +``` + +#### 5.4.4 Schedule threadpool +Default threads in the threadpool is 10 so it can avoid task schedule delay because of single thread block. + +``` +org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool +org.quartz.threadPool.threadCount: 10 +org.quartz.threadPool.threadPriority: 5 +org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true +``` + +business logic was executed on remote executor in XXL-JOB,schedule center just start one schedule request at every schedule time,executor will inqueue the request and response schedule center immediately. There is a huge difference from run business logic in quartz’s QuartzJobBean directly,just as Elephants and feathers; + +the logic of task in XXL-JOB schedule center is very light and single job average run time alaways under 100ms,(most is network time consume).so it can use limited threads to support a large mount of job run concurrently, 10 threads configured above can support at least 100 JOB normal execution. + +#### 5.4.5 @DisallowConcurrentExecution +This annotation is not used default by the schedule center of XXL-JOB schedule module, it use concurrent policy default,because RemoteHttpJobBean is common QuartzJobBean,so it greatly improve the capacity of schedule system and decrease the blocked chance of schedule module in the case of multi-threaded schedule. + +Every schedule module was scheduled and executed parallel in XXL-JOB,but tasks in executor is executed serially and support stop task. + +#### 5.4.6 misfire +The handle policy when miss the job’s trigger time. +he reason may be:restart service,schedule thread was blocked by QuartzJobBean, threads was exhausted,some task enable @DisallowConcurrentExecution,the last schedule was blocked and next schedule was missed. + +The default value of misfire in quartz.properties as shown below, unit in milliseconds: +``` +org.quartz.jobStore.misfireThreshold: 60000 +``` + +Misfire rule: + withMisfireHandlingInstructionDoNothing:does not trigger execute immediately and wait for next time schedule. + withMisfireHandlingInstructionIgnoreMisfires:execute immediately at the first frequency of the missed time. + withMisfireHandlingInstructionFireAndProceed:trigger task execution immediately at the frequency of the current time. + +XXL-JOB’s default misfire rule:withMisfireHandlingInstructionDoNothing + +``` +CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(jobInfo.getJobCron()).withMisfireHandlingInstructionDoNothing(); +CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build(); +``` + +#### 5.4.7 log callback service +When schedule center of the schedule module was deployed as web service, on one side it play as schedule center, on the other side it also provide api service for executor. + +The source code location of schedule center’s “log callback api service” as shown below: +``` +xxl-job-admin#com.xxl.job.admin.controller.JobApiController.callback +``` + +Executor will execute task when it receive task execute request.it will notify the task execute result to schedule center when the task is done. + +#### 5.4.8 task HA(Failover) +If executor project was deployed as cluster schedule center will known all online executor nodes,such as:“127.0.0.1:9997, 127.0.0.1:9998, 127.0.0.1:9999”. + +When "路由策略" select "故障转移(FAILOVER)",it will send heart beat check request in order while schedule center start schedule request. The first alive checked executor node will be selected and send schedule request to it. + +“调度备注” can be viewed on the monitor page when schedule success. As shown below: +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jrdI.png "在这里输入图片标题") + +“调度备注” will display local schedule route path、executor’s "注册方式"、"地址列表" and task’s "路由策略"。Under "故障转移(FAILOVER)" policy, schedule center take first address to do heartbeat detection, heat beat fail will automatically skip, the second address heart beat fail…… until the third address “127.0.0.1:9999” heart beat success, it was selected as target executor, then send schedule request to target executor, now the schedule process is end wait for the executor’s callback execution result. + +#### 5.4.9 schedule log +Every time when task was scheduled in the schedule center it will record a task log, the task log include three part as shown below: + +- 任务信息:include executor address、JobHandler and executor params,accord these parameters it can locate specific machine and task code that the task will be executed. +- 调度信息:include schedule time、schedule result and schedule log and so on,accord these parameters you can understand some task schedule info of schedule center. +- 执行信息:include execute time、execute result and execute log and so on, accord these parameters you can understand the task execution info in the executor. + +Schedule log stands fo single task schedule, attribute description is as follows: +- 执行器地址:machine addresses on which task will be executed. +- JobHandler:JobHandler name of task under Bean module. +- 任务参数:the input parameters of task +- 调度时间:the schedule time started by schedule center. +- 调度结果:schedule result of schedule center,SUCCESS or FAIL. +- 调度备注:remark info of task scheduled by schedule center, such as address heart beat log. +- 执行时间:the callback time when the task is done in the executor. +- 执行结果:task execute result in the executor,SUCCESS or FAIL. +- 执行备注:task execute remark info in the executor,such as exception log. +- 执行日志:full execution log of the business code during execution of the task,go and see “4.7 view execution log”. + +#### 5.4.10 Task dependency +principle:every task has a task key in XXL-JOB, every task can configure property “child task Key”,it can match task dependency relationship through task key. + +When parent task end execute and success, it will match child task dependency accord child task key, it will trigger child task execute once if it matched child task. + +On the task log page ,you can see matched child task and triggered child task’s log info when you “查看”button of “执行备注”,otherwise the child task didin’t execute, as shown beleow: + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Wb2o.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jOAU.png "在这里输入图片标题") + +### 5.5 Task "run mode" analysis +#### 5.5.1 "Bean模式" task +Development steps:go and see "chapter 3" . +principle: every Bean mode task is a Spring Bean instance and it is maintained in executor project’s Spring container. task class nedd to add “@JobHandler(value="name")” annotation, because executor identify task bean instance in spring container through annotation. Task class nedd to implements interface IJobHandler, task logic code in method execute(), the task logic in execute() method will be executed when executor received a schedule request from schedule center. + +#### 5.5.2 "GLUE模式(Java)" task +Development steps:go and see "chapter 3" . +Principle : every "GLUE模式(Java)" task code is a class implemets interface IJobHandler, when executor received schedule request from schedule center these code will be loaded by Groovy classloader and instantiate into a Java object and inject spring bean service declared in this code at the same time(please confirm service and class reference in Glue code exist in executor project), then call the object’s execute() method and execute task logic. + +#### 5.5.3 GLUE模式(Shell) + GLUE模式(Python) +Development steps:go and see "chapter 3" . +principle:the source code of script task is maintained in schedule center and script logic will be executed in executor. when script task was triggered, executor will load script source code and generate a script file on the machine where executor was deployed, the script will be called by java code, the script output log will be written to the task log file in real time so that we can monitor script execution in real time through schedule center, the return code 0 stands for success other for fail. + +All supported types of scripts as shown beloes: + + - shell script:shell script task will be enabled when select "GLUE模式(Shell)"as task run mode. + - python script: python script task will be enabled when select " GLUE模式(Python)"as task run mode. + + +#### 5.5.4 executor +Executor is actually an embedded Jetty server with default port 9999, as shown below(parameter:xxl.job.executor.port). + +Executor will identify Bean mode task in spring container through @JobHandler When project start, it will be managed use the value of annotation as key. + +When executor received schedule request from schedule center, if task type is “Bean模式” it will match bean mode task in Spring container and call it’s execute() method and execute task logic. if task type is “GLUE模式”, it will load Glue code, instantiate a Java object and inject other spring service(notice: the spring service injected in Glue code must exist in the same executor project), then call execute() method and execute task logic. + +#### 5.5.5 task log +XXL-JOB will generate a log file for every schedule request, the log info will be recorded by XxlJobLogger.log() method, the log file will be loaded when view log info through schedule center. + +(history version is implemented by overriding LOG4J’s Appender so it exists dependency restrictions, The way has been discraded in the new version) + +The location of log file can be specified in executor configuration file, default pattern is : /data/applogs/xxl-job/jobhandler/formatted date/primary key for database scheduling log records.log”. + +When start child thread in JobHandler, child thread will print log in parent JobHandler thread’s execute log in order to trace execute log. + +### 5.6 Communication module analysis + +#### 5.6.1 A complete task schedule communication process + - 1,schedule center send http request to executor, and the service in executor in fact is a jetty server with default port 9999. + - 2,executor execute task logic. + - 3,executor http callback with schedule center for schedule result, the service in schedule center used to receive callback request from executor is a set of api opended to executor. + +#### 5.6.2 Encrypt Communication data +When scheduler center send request to executor, it will use RequestModel and ResponseModel object to encapsulate schedule request parameters and response data, these two object will be serialized before communication, data protocol and time stamp will be checked so that achieve data encryption target. + +### 5.7 task register and task auto discover +Task executor machine property has been canceled from v1.5, instead of task register and auto discovery, get remote machine address dynamic. + + AppName: unique identify of executor cluster, executor is minimal unite of task register, every task recognize machine addresses under the executor on which it was binded. + Beat: heartbeat cycle of task register, default is 15s, and the time executor usedto register is twice the time, the time used to auto task discover is twice the beat time, the invalid time of register is twice the Beat time. + registry table: see XXL_JOB_QRTZ_TRIGGER_REGISTRY table, it will maintain a register record periodically while task register, such as the bind relationship between machine address and AppName, so that schedule center can recognize machine list by AppName dynamicly. + +To ensure system lightweight and reduce learning costs, it did not use Zookeeper as register center, Use DB as register center to do task registration. + +### 5.8 task execute result +Since v1.6.2, the task execute result is recognized through ReturnT of IJobHandler, it executes success when return value meets the condition "ReturnT.code == ReturnT.SUCCESS_CODE" , or it executes fail, and it can callback error message info to schedule center through ReturnT.msg, so it can control task execute results in the task logic. + +### 5.9 slice broadcat & dynamic slice +When “分片广播” is selected as route policy in executor cluster, one task schedule will broadcast all executor node in cluster to trigger task execute in every executor, pass slice parameter at the same time, so we can develop slice task by slice parameters. + +"分片广播" break the task by the dimensions of executor, support dynamic extend executor cluster so that it can add slice number dynamically to do business process, In case of large amount of data process can significantly improve task processing capacity and speed. + +The develop process of "分片广播" is the same as general task, The difference is that you can get slice parameters,code as shown below(go and see ShardingJobHandler in execuotr example ): + + int shardIndex = XxlJobContext.getXxlJobContext().getShardIndex(); + int shardTotal = XxlJobContext.getXxlJobContext().getShardTotal(); + +This slice parameter object has two properties: + + index:the current slice number(start with 0),stands for the number of current executor in the executor cluster. + total:total slice number,stands for total slices in the executor cluster. + +This feature applies to scenes as shown below: +- 1、slice task scene:when 10 executor to handle 10w records, 1w records need to be handled per machine, time-consuming 10 times lower; +- 2、Broadcast task scene:broadcast all cluster nodes to execute shell script、broadcast all cluster nodes to update cache. + +### 5.10 AccessToken +To improve system security it is need to check security between schedule center and executor, just allow communication between them when AccessToken of each other matched. + +The AccessToken of scheduler center and executor can be configured by xxl.job.accessToken. + +There are only two settings when communication between scheduler center and executor just: + +- one:do not configure AccessToken on both, close security check. +- two:configure the same AccessToken on both; + +### 5.11 Dispatching center API services +The scheduling center provides API services for executors and business parties to choose to use, and the currently available API services are available. + + 1. Job result callback service; + 2. Executor registration service; + 3. Executor registration remove services; + 4. Triggers a single execution service, and support the task to be triggered according to the business event; + +The scheduling center API service location: com.xxl.job.core.biz.AdminBiz.java + +The scheduling center API service requests reference code:com.xxl.job.adminbiz.AdminBizTest.java + + +## 6 Version update log +### 6.1 version V1.1.x,New features [2015-12-05] +**【since V1.1.x,XXL-JOB was used by company hiring me,alias Ferrari inner company,the latest version is recommended for new project】** +- 1、simple:support CRUD operation through Web page, simple and one minute to get started; +- 2、dynamic:support dynamic update task status,pause/recover task and effective in real time; +- 3、service HA:task info stored in mysql, Job service support cluster to make sure service HA; +- 4、task HA:when some Job services hangs up, tasks will be assigned to some other alive machines, if all nodes of the cluster hangs up, it will compensate for the execution of lost task when restart; +- 5、one task instance will only be executed on one executor; +- 6、task is executed serially; +- 7、support for custom parameters; +- 8、Support pause task execution remotely . + +### 6.2 version V1.2.x,New features [2016-01-17] +- 1、support task group; +- 2、suport local task, remote task; +- 3、support two types underlying communication ,Servlet or JETTY; +- 4、support task log; +- 5、support serially execution,parallel execution; + + Description:system architecture of V1.2 divided by function as shown below: + + - schedule module(schedule center):Responsible for managing schedule information,send schedule request according to the schedule configuration; + - execute module(executor):Responsible for receiving schedule request and execute task logic; + - communication module:Responsible for the communication between the schedule module and execute module; + advantage: + + - Decouple:execute module supply task api, schedule module maintains schedule information, The business is independent of each other; + - High scalability; + - stability; + +### 6.3 version V1.3.0,New features [2016-05-19] +- 1、discard local task module, remote task was recommended, easy to decouple system, the JobHandler of task was called executor. +- 2、dicard underlying communication type servlet, JETTY was recommended, schedule and callback bidirectional communication, rebuild the communication logic; +- 3、UI interactive optimization:optimize left menu expansion and menu item selected status , task list opens the table with compression optimization; +- 4、【important】executor is subdivided into two develop mode:BEAN、GLUE: + + Introduction to the executor mode: + - BEAN mode executor:every executor is a Spring Bean instance,it was recognized and scheduled by XXL-JOB through @JobHandler annotation; + -GLUE mode executor:every executor corresponds to a piece of code,edited and maintained online by Web, Dynamic compile and takes effect in real time, executor is responsible for loading GLUE code and executing; + +### 6.4 version V1.3.1,New features [2016-05-23] +- 1、Update project directory structure: + - /xxl-job-admin -------------------- 【schedule center】:Responsible for managing schedule information,send schedule request according to schedule configuration; + - /xxl-job-core ----------------------- Public core dependence + - /xxl-job-executor-example ------ 【executor】:Responsible for receiving scheduling request and execute task logic; + - /db ---------------------------------- create table script + - /doc --------------------------------- user manual +- 2、Upgrade the user manual under the new directory structure; +- 3、Optimize some interactions and UI; + +### 6.5 version V1.3.2,New features [2016-05-28] +- 1、Schedule logic for transactional handle; +- 2、executor asynchronous callback execution log; +- 3、【important】based on HA support of schedule center,extend executor’s Failover support,Support configure multiple execution addresses; + +### 6.6 version V1.4.0 New features [2016-07-24] +- 1、Task dependency: it is implemented by trigger event, it will automatically trigger a child task schedule after Task execute success and callback, multiple child tasks are separated by commas; +- 2、executor source code has been reconstructed, optimize underlying db script; +- 3、optimize task thread group logic of executor, before it is group by executor’s JobHandler so when multiple task reuse Jobhanlder will cause block with each other. Now it is grouped by task of schedule center so tasks are isolated from task execution. +- 4、optimize communication scheme between executor and schedule center, a simple RPC protocol was implemented through Hex + HC, optimize the maintenance and analysis process of communication parameters. +- 5、schedule center, create/edit task, page attribute adjustment: + - 5.1、the property JobName was removed from task add/edit page and it is changed to automatically generate by system: this field before is used to identify a task in schedule center and did not use in other scenes, so remove it to simplify the task creation; + - 5.2、adjust "GLUE模式" property in task add/edit page to near JobHandler input box; + - 5.3、"报警阈值" property was removed from task add/edit page; + - 5.4、"子任务Key" property was removed from task add/edit page, the key of task can be acquired from task list page, child task will be triggered by child task key when main task execute success. +- 6、bug fix: + - 6.1、optimize jetty executor shutdown, solve one problem may cause jetty could not shutdown. + - 6.2、optimize callback of executor task queue when task execute finish. Solve a problem which may cause task could not callback. + - 6.3、Optimize Page List Parameters of Schedule Center, solve one problem which may be caused by post length limit of server. + - 6.4、optmize executor Jobhandler annotation, solve a problem that container could not load the JobHandler caused by the transaction proxy. + - 6.5、optimize remote schedule, disable retry policy, solve a problem may caused repeat call; + +Tips: V1.3.x release has been published , enter the maintenance phase, branch address is [V1.3](https://github.com/xuxueli/xxl-job/tree/v1.3) .New features will be updated continuously in the master branch. + +### 6.7 version V1.4.1 New features [2016-09-06] +- 1、project successfully pushed to maven central warehouse, Central warehouse address and dependency as shown below: + ``` + + + com.xuxueli + xxl-job-core + ${最新稳定版} + + ``` +- 2、To adapt to the rules of central warehouse, groupId has been changed from com.xxl to com.xuxueli. +- 3、to resolve the problem that sub-modules can not be compiled separately, system version is not maintained in the project root pom, each sub-module is configured separately for version configuration; +- 4、optimize data byte length statistics rule of RPC communication it may reduce 50% of data traffic; +- 5、IJobHandler cancel task return value, before the execution status is judged by the return value, now it instead of task was executed successfully by default only when exception was caught the task execution was judged failed. +- 6、optimize system public pop-up box as a plugin; +- 7、optimize table structure and the table name now is upper case; +- 8、modify ContentType of JSON response from exception handler of schedule center to fix the bug that it is could not recognized by browser. + +### 6.8 version V1.4.2 New features [2016-09-29] +- 1、push V1.4.2 to maven central warehouse, main version V1.4 enter maintenance phase; +- 2、fix problem task list offset when add task; +- 3、fix a style disorder problem that caused by bootstrap does not support the modal frame overlap , the problem occurs when the task is edited; +- 4、optimize schedule status when schedule timeout and Handler could not matched; +- 5、the task could not stop problem caused by catch exception has given solution; + +### 6.9 version V1.5.0 New features [2016-11-13] +- 1、task register: executor registers the task automatically, schedule center will automatically discover the registered task and trigger execution. +- 2、add parameter AppName for executor: AppName is the unique identifier of each executor cluster, register periodically and automatically with AppName. +- 3、add column executor management in schedule center : manage online executors, automatically discover registered executors via the property AppName。Only managed executors are allowed to be used; +- 4、change Task group attribute to executor : each task needs to be bound to the specified exector, schedule address is obtained by binded executor; +- 5、discard property task machine: by the way of binding task with executor, automatically discovers registered remote executor address and triggers schedule request. +- 6、add DBGlueLoader in public dependency, it implement GLUE source code calssloader based on native jdbc, Reduce third party reliance (mybatis,spring-orm etc); simplify and optimize executor configuration (for GLUE task), Reduce the difficulty of getting started; +- 7、adjust table structure, reconstruct the project; +- 8、schedule center automatically registered and found, failover: schedule center periodically registered automatically, task callback can recognize all online schedule center addresses, task callback support failover so that it can avoid single point of risk. + +### 6.10 version V1.5.1 New features [2016-11-13] +- 1、Reconstruct the underlying code and optimize logic, clean POM and Clean Code; +- 2、Servlet/JSP Spec selected 3.0/2.2; +- 3、Spring updated to 3.2.17.RELEASE version; +- 4、Jetty updated to version 8.2.0.v20160908; +- 5、has push V1.5.0 and V1.5.1 to maven central warehouse; + +### 6.10 version V1.5.2 New features [2017-02-28] +- 1、optimize IP tools class which used to gets IP address,IP static cache; +- 2、both executor and schedule center support customize registered IP address;Solve problem when machine has multiple network card and get the wrong card; +- 3、solve the problem that it will generate multiple log files when executed across days; +- 4、the non-sensitive log level is adjusted to debug; +- 5、Upgrade the database connection pool to c3p0; +- 6、optimize log4j property of executor,remove invalid attribute; +- 7、reconstruct underlying code and optimize logic and Clean Code; +- 8、optimize Dependency Injection Logic of GLUE, support injected as alias; + +### 6.11 version V1.6.0 New features [2017-03-13] +- 1、upgrade communication scheme,the HEX communication model is adjusted to the B-RPC model based on HTTP; +- 2、executor supports set execution address list manually,provide switch to use automatically registered address or manually set address; +- 3、executor route rules:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移; +- 4、unified thread model and thread destruction scheme (by the way of listener or stop() method,Destroy the thread when container is destroyed;Daemon is sometimes not ideal); +- 5、unified system configuration data,Unified managed by configuration files; +- 6、CleanCode,Clean up invalid historical parameters; +- 7、extend data structure and adjust related table structure; +- 8、new created task defaults to a non-running state; +- 9、optimize update logic of GLUE mode task instance , The original update is based on the timeout value and now is updated according to the version number,version number plus one while source changed; + +### 6.12 version V1.6.1 New features [2017-03-25] +- 1、Rolling log; +- 2、reconstruct WebIDE interactive; +- 3、enhanced communication check,filter unnormal requests effectively; +- 4、enhanced permission check,Using dynamic login TOKEN(recommend instead of internal SSO); +- 5、optimize database configuration,solve garbled problem; + +### 6.13 version V1.6.2 New features [2017-04-25] +- 1、execution report:support view run time data in real time, such as task number, total schedule number, executor number etc., include schedule report , such as scheduled distribution graph on date, scheduled success distribution graph etc; +- 2、JobHandler support set return value for tasks, it is easy to control task execute result in task logic; +- 3、the problem could not view exception info when resource path include space or chinese word casused resource file could not be loaded; +- 4、optimize route policy:fix problems that Loop and LFU routing policy counters are no limit and first route is focused on the first machine; + +### 6.14 version V1.7.0 New features [2017-05-02] +- 1、script task:support develop and run script task by GLUE, include script type such as Shell、Python and Groovy; +- 2、add spring-boot type executor example project; +- 3、upgrade jetty to version 9.2; +- 4、task execute log remove log4j dependency, instead of self-realization,Thus eliminate the dependency on the log component; +- 5、executor remove GlueLoader dependency,instead of push mode,thus GLUE source code load no longer rely on JDBC; +- 6、get the project name when login and redirect, solve 404 problem when it is not deployed by the directory; + +### 6.15 version V1.7.1 New features [2017-05-08] +- 1、unified write and read code of execute log as UTF-8,solve log garbled problem under windows environment; +- 2、communication timeout period is limited to 10s,To avoid schedule thread is occupied under abnormal situation; +- 3、adjust executor , server stat, destroy and register logic; +- 4、optimize Jetty Server shutdown logic, repair port occupation caused by executor could not be closed normally and frequent printe c3p0 log probleam; +- 5、start child thread in JobHandler,support child thread print execute log and view by Rolling; +- 6、task log cleanup; +- 7、pop-up component is replaced by layer; +- 8、upgrade quartz to version 2.3.0; + +### 6.16 version V1.7.2 New features [2017-05-17] +- 1、block handle policy:the policy when schedule is too frequently and the executor it too late to handle, include multiple strategies:single machine serially execute(default)、discard subsequent schedule、override before schedule; +- 2、fail handle policy:handle policy when scheduled fail, include :failure alarm(default)、failed to retry; +- 3、The communication timeout is adjusted to 180s; +- 4、executor and database are completely decoupled,But the executor needs to configure schedule center cluster address。schedule center provides APIs for executor callbacks and heartbeat registration services,cancel jetty inner schedule center, heartbeat cycle is adjusted to 30s,heartbeat failure is triple heartbeat; +- 5、fix executor parameters lost bug when edit; +- 6、add task test Demo to make task logic test easier; + +### 6.17 version V1.8.0 New features [2017-07-17] +- 1、optimize update logic of task Cron,instead of rescheduleJob,at the same time preventing set cron repeatedly; +- 2、optimize API callback service failed status code,facilitate troubleshooting; +- 3、XxlJobLogger support multi-parameter; +- 4、route policy add "忙碌转移" mode:Perform idle detection in sequence,The first idle test successfully machine is selected as the target executor and trigger schedule; +- 5、reconstruct route policy code; +- 6、fix executor repeat registration problem; +- 7、Task thread will be destroyed after 30 times idle turn, reduce the inefficient thread consumption of low frequency tasks; +- 8、Executor task execution result batch callback so that reduce callback frequency to improve actuator performance; +- 9、cancle XML configuration of springboot executor project,instead of class configuration; +- 10、supports filter execute log based on running status; +- 11、optimize scheduling Center Task Registration Detection Logic; + +### 6.18 version V1.8.1 New features [2017-07-30] +- 1、slice broadcast task:When slice broadcast is selected as route policy in executor cluster, one task schedule will broadcast all executor node in cluster to trigger task execute in every executor, pass slice parameter at the same time, so we can develop slice task by slice parameters; +- 2、dynamic slice: break the task by the dimensions of executor, support dynamic extend executor cluster so that it can add slice number dynamically to do business process, In case of large amount of data process can significantly improve task processing capacity and speed; +- 3、executor JobHandler disables name conflicts; +- 4、executor cluster address list for natural sorting; +- 5、add test cases and optimize DAO layer code for Scheduling center; +- 6、schedule Center API service change to self-study RPC framework to u nify communication model; +- 7、add schedule center API service test Demo, convenient in dispatch center API extension and testing; +- 8、Task list page interaction optimization,The task list is automatically refreshed when the executor group is replaced,create new job defaults to locate current executor position; +- 9、access Token:To improve system security,it is used for safety check between schedule center and executor, communication allowed just when Both Access Token matched; +- 10、upgrade springboot version to 1.5.6.RELEASE of executor; +- 11、unify maven version dependency management; + +### 6.19 version V1.8.2 New features[Coding] +- 1,support configuring the HTTPS for executor callback URL; +- 2,Standardize project directory for extend multi executors; +- 3,add JFinal type executor sample project; + +### TODO LIST +- 1,Task privilege management:control privilege on executor, check privilege on core operations; +- 2,Task slice routing:using consistent Hash algorithm to calculate slice order as stable as possible, even if there is fluctuation in the registration machine will not cause large fluctuations in the order of slice. Currently using IP natural sorting can meet the demand,to be determined; +- 3,Failure retry optimization:The current failure to retry logic is execute the request logic once again after the scheduled request fails。The optimization point is retry for both scheduling and execution failures, retry a full schedule when retrying,This may lead schedule failure to an infinite loop,to be determined; +- 4,write file when callback failed,read the log when viewing the log,callback confirm after rebooting; +- 5,Task dependency,flow chart,child task + aggregation task,log of each node; +- 6,Scheduled task priority; +- 7,Remove quartz dependencies and rewrite scheduld module:insert the next execution record into delayqueue when add or resume task, schedule center cluster compete distributed lock,successful nodes bulk load expired delayqueue data and batch execution; +- 8,springboot and docker image,and push docker image to the central warehouse,further realize product out of the box; +- 9,globalization:schedule center interface and Official documents,add English version; +- 10,executor removal:notify schedule center and remove the corresponding execute node when executor is destroyed, improve the timeliness of executor state recognized; + +## 7. Other + +### 7.1 Contributing +Contributions are welcome! Open a pull request to fix a bug, or open an [Issue](https://github.com/xuxueli/xxl-job/issues/) to discuss a new feature or change. + +### 7.2 used records(record just for spread,Product is open source and free of charge) +Record for spread product and product is free and open source. +Welcome to [check in](https://github.com/xuxueli/xxl-job/issues/1 )on github. + +### 7.3 Copyright and License +This product is open source and free, and will continue to provide free community technical support. Individual or enterprise users are free to access and use. + +- Licensed under the GNU General Public License (GPL) v3. +- Copyright (c) 2015-present, xuxueli. + +--- +### Donate +No matter how much the amount is enough to express your thought, thank you very much :) [To donate](https://www.xuxueli.com/page/donate.html ) diff --git "a/project/xxl-job-2.4.0/doc/XXL-JOB\345\256\230\346\226\271\346\226\207\346\241\243.md" "b/project/xxl-job-2.4.0/doc/XXL-JOB\345\256\230\346\226\271\346\226\207\346\241\243.md" new file mode 100644 index 00000000..1d4c274e --- /dev/null +++ "b/project/xxl-job-2.4.0/doc/XXL-JOB\345\256\230\346\226\271\346\226\207\346\241\243.md" @@ -0,0 +1,2375 @@ +## 《分布式任务调度平台XXL-JOB》 + +[![Actions Status](https://github.com/xuxueli/xxl-job/workflows/Java%20CI/badge.svg)](https://github.com/xuxueli/xxl-job/actions) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.xuxueli/xxl-job/) +[![GitHub release](https://img.shields.io/github/release/xuxueli/xxl-job.svg)](https://github.com/xuxueli/xxl-job/releases) +[![GitHub stars](https://img.shields.io/github/stars/xuxueli/xxl-job)](https://github.com/xuxueli/xxl-job/) +[![Docker Status](https://img.shields.io/docker/pulls/xuxueli/xxl-job-admin)](https://hub.docker.com/r/xuxueli/xxl-job-admin/) +[![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](http://www.gnu.org/licenses/gpl-3.0.html) +[![donate](https://img.shields.io/badge/%24-donate-ff69b4.svg?style=flat)](https://www.xuxueli.com/page/donate.html) + +[TOCM] + +[TOC] + +## 一、简介 + +### 1.1 概述 +XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 + +### 1.2 社区交流 +- [社区交流](https://www.xuxueli.com/page/community.html) + +### 1.3 特性 +- 1、简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手; +- 2、动态:支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效; +- 3、调度中心HA(中心式):调度采用中心式设计,“调度中心”自研调度组件并支持集群部署,可保证调度中心HA; +- 4、执行器HA(分布式):任务分布式执行,任务"执行器"支持集群部署,可保证任务执行HA; +- 5、注册中心: 执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行。同时,也支持手动录入执行器地址; +- 6、弹性扩容缩容:一旦有新执行器机器上线或者下线,下次调度时将会重新分配任务; +- 7、触发策略:提供丰富的任务触发策略,包括:Cron触发、固定间隔触发、固定延时触发、API(事件)触发、人工触发、父子任务触发; +- 8、调度过期策略:调度中心错过调度时间的补偿处理策略,包括:忽略、立即补偿触发一次等; +- 9、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度; +- 10、任务超时控制:支持自定义任务超时时间,任务运行超时将会主动中断任务; +- 11、任务失败重试:支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;其中分片任务支持分片粒度的失败重试; +- 12、任务失败告警;默认提供邮件方式失败告警,同时预留扩展接口,可方便的扩展短信、钉钉等告警方式; +- 13、路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等; +- 14、分片广播任务:执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数开发分片任务; +- 15、动态分片:分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。 +- 16、故障转移:任务路由策略选择"故障转移"情况下,如果执行器集群中某一台机器故障,将会自动Failover切换到一台正常的执行器发送调度请求。 +- 17、任务进度监控:支持实时监控任务进度; +- 18、Rolling实时日志:支持在线查看调度结果,并且支持以Rolling方式实时查看执行器输出的完整的执行日志; +- 19、GLUE:提供Web IDE,支持在线开发任务逻辑代码,动态发布,实时编译生效,省略部署上线的过程。支持30个版本的历史版本回溯。 +- 20、脚本任务:支持以GLUE模式开发和运行脚本任务,包括Shell、Python、NodeJS、PHP、PowerShell等类型脚本; +- 21、命令行任务:原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可; +- 22、任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行, 多个子任务用逗号分隔; +- 23、一致性:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行; +- 24、自定义任务参数:支持在线配置调度任务入参,即时生效; +- 25、调度线程池:调度系统多线程触发调度运行,确保调度精确执行,不被堵塞; +- 26、数据加密:调度中心和执行器之间的通讯进行数据加密,提升调度信息安全性; +- 27、邮件报警:任务失败时支持邮件报警,支持配置多邮件地址群发报警邮件; +- 28、推送maven中央仓库: 将会把最新稳定版推送到maven中央仓库, 方便用户接入和使用; +- 29、运行报表:支持实时查看运行数据,如任务数量、调度次数、执行器数量等;以及调度报表,如调度日期分布图,调度成功分布图等; +- 30、全异步:任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰,理论上支持任意时长任务的运行; +- 31、跨语言:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。除此之外,还提供了 “多任务模式”和“httpJobHandler”等其他跨语言方案; +- 32、国际化:调度中心支持国际化设置,提供中文、英文两种可选语言,默认为中文; +- 33、容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现产品开箱即用; +- 34、线程池隔离:调度线程池进行隔离拆分,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性; +- 35、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色; +- 36、权限控制:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作; + +### 1.4 发展 +于2015年中,我在github上创建XXL-JOB项目仓库并提交第一个commit,随之进行系统结构设计,UI选型,交互设计…… + +于2015-11月,XXL-JOB终于RELEASE了第一个大版本V1.0, 随后我将之发布到OSCHINA,XXL-JOB在OSCHINA上获得了@红薯的热门推荐,同期分别达到了OSCHINA的“热门动弹”排行第一和git.oschina的开源软件月热度排行第一,在此特别感谢红薯,感谢大家的关注和支持。 + +于2015-12月,我将XXL-JOB发表到我司内部知识库,并且得到内部同事认可。 + +于2016-01月,我司展开XXL-JOB的内部接入和定制工作,在此感谢袁某和尹某两位同事的贡献,同时也感谢内部其他给与关注与支持的同事。 + +于2017-05-13,在上海举办的 "[第62期开源中国源创会](https://www.oschina.net/event/2236961)" 的 "放码过来" 环节,我登台对XXL-JOB做了演讲,台下五百位在场观众反响热烈([图文回顾](https://www.oschina.net/question/2686220_2242120) )。 + +于2017-10-22,又拍云 Open Talk 联合 Spring Cloud 中国社区举办的 "[进击的微服务实战派上海站](https://opentalk.upyun.com/303.html)",我登台对XXL-JOB做了演讲,现场观众反响热烈并在会后与XXL-JOB用户热烈讨论交流。 + +于2017-12-11,XXL-JOB有幸参会《[InfoQ ArchSummit全球架构师峰会](http://bj2017.archsummit.com/)》,并被拍拍贷架构总监"杨波老师"在专题 "[微服务原理、基础架构和开源实践](http://bj2017.archsummit.com/training/2)" 中现场介绍。 + +于2017-12-18,XXL-JOB参与"[2017年度最受欢迎中国开源软件](http://www.oschina.net/project/top_cn_2017?sort=1)"评比,在当时已录入的约九千个国产开源项目中角逐,最终进入了前30强。 + +于2018-01-15,XXL-JOB参与"[2017码云最火开源项目](https://www.oschina.net/news/92438/2017-mayun-top-50)"评比,在当时已录入的约六千五百个码云项目中角逐,最终进去了前20强。 + +于2018-04-14,iTechPlus在上海举办的 "[2018互联网开发者大会](http://www.itdks.com/eventlist/detail/2065)",我登台对XXL-JOB做了演讲,现场观众反响热烈并在会后与XXL-JOB用户热烈讨论交流。 + +于2018-05-27,在上海举办的 "[第75期开源中国源创会](https://www.oschina.net/event/2278742)" 的 "架构" 主题专场,我登台进行“基础架构与中间件图谱”主题演讲,台下上千位在场观众反响热烈([图文回顾](https://www.oschina.net/question/3802184_2280606) )。 + +于2018-12-05,XXL-JOB参与"[2018年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2018?sort=1)"评比,在当时已录入的一万多个开源项目中角逐,最终排名第19名。 + +于2019-12-10,XXL-JOB参与"[2019年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2019)"评比,在当时已录入的一万多个开源项目中角逐,最终排名"开发框架和基础组件类"第9名。 + +于2020-11-16,XXL-JOB参与"[2020年度最受欢迎中国开源软件](https://www.oschina.net/project/top_cn_2020)"评比,在当时已录入的一万多个开源项目中角逐,最终排名"开发框架和基础组件类"第8名。 + +于2021-12-06,XXL-JOB参与"[2021年度OSC中国开源项目评选](https://www.oschina.net/project/top_cn_2021) "评比,在当时已录入的一万多个开源项目中角逐,最终当选"最受欢迎项目"。 + +> 我司大众点评目前已接入XXL-JOB,内部别名《Ferrari》(Ferrari基于XXL-JOB的V1.1版本定制而成,新接入应用推荐升级最新版本)。 +据最新统计, 自2016-01-21接入至2017-12-01期间,该系统已调度约100万次,表现优异。新接入应用推荐使用最新版本,因为经过数十个版本的更新,系统的任务模型、UI交互模型以及底层调度通讯模型都有了较大的优化和提升,核心功能更加稳定高效。 + +至今,XXL-JOB已接入多家公司的线上产品线,接入场景如电商业务,O2O业务和大数据作业等,截止最新统计时间为止,XXL-JOB已接入的公司包括不限于: + + - 1、大众点评【美团点评】 + - 2、山东学而网络科技有限公司; + - 3、安徽慧通互联科技有限公司; + - 4、人人聚财金服; + - 5、上海棠棣信息科技股份有限公司 + - 6、运满满【运满满】 + - 7、米其林 (中国区)【米其林】 + - 8、妈妈联盟 + - 9、九樱天下(北京)信息技术有限公司 + - 10、万普拉斯科技有限公司【一加手机】 + - 11、上海亿保健康管理有限公司 + - 12、海尔馨厨【海尔】 + - 13、河南大红包电子商务有限公司 + - 14、成都顺点科技有限公司 + - 15、深圳市怡亚通 + - 16、深圳麦亚信科技股份有限公司 + - 17、上海博莹科技信息技术有限公司 + - 18、中国平安科技有限公司【中国平安】 + - 19、杭州知时信息科技有限公司 + - 20、博莹科技(上海)有限公司 + - 21、成都依能股份有限责任公司 + - 22、湖南高阳通联信息技术有限公司 + - 23、深圳市邦德文化发展有限公司 + - 24、福建阿思可网络教育有限公司 + - 25、优信二手车【优信】 + - 26、上海悠游堂投资发展股份有限公司【悠游堂】 + - 27、北京粉笔蓝天科技有限公司 + - 28、中秀科技(无锡)有限公司 + - 29、武汉空心科技有限公司 + - 30、北京蚂蚁风暴科技有限公司 + - 31、四川互宜达科技有限公司 + - 32、钱包行云(北京)科技有限公司 + - 33、重庆欣才集团 + - 34、咪咕互动娱乐有限公司【中国移动】 + - 35、北京诺亦腾科技有限公司 + - 36、增长引擎(北京)信息技术有限公司 + - 37、北京英贝思科技有限公司 + - 38、刚泰集团 + - 39、深圳泰久信息系统股份有限公司 + - 40、随行付支付有限公司 + - 41、广州瀚农网络科技有限公司 + - 42、享点科技有限公司 + - 43、杭州比智科技有限公司 + - 44、圳临界线网络科技有限公司 + - 45、广州知识圈网络科技有限公司 + - 46、国誉商业上海有限公司 + - 47、海尔消费金融有限公司,嗨付、够花【海尔】 + - 48、广州巴图鲁信息科技有限公司 + - 49、深圳市鹏海运电子数据交换有限公司 + - 50、深圳市亚飞电子商务有限公司 + - 51、上海趣医网络有限公司 + - 52、聚金资本 + - 53、北京父母邦网络科技有限公司 + - 54、中山元赫软件科技有限公司 + - 55、中商惠民(北京)电子商务有限公司 + - 56、凯京集团 + - 57、华夏票联(北京)科技有限公司 + - 58、拍拍贷【拍拍贷】 + - 59、北京尚德机构在线教育有限公司 + - 60、任子行股份有限公司 + - 61、北京时态电子商务有限公司 + - 62、深圳卷皮网络科技有限公司 + - 63、北京安博通科技股份有限公司 + - 64、未来无线网 + - 65、厦门瓷禧网络有限公司 + - 66、北京递蓝科软件股份有限公司 + - 67、郑州创海软件科技公司 + - 68、北京国槐信息科技有限公司 + - 69、浪潮软件集团 + - 70、多立恒(北京)信息技术有限公司 + - 71、广州极迅客信息科技有限公司 + - 72、赫基(中国)集团股份有限公司 + - 73、海投汇 + - 74、上海润益创业孵化器管理股份有限公司 + - 75、汉纳森(厦门)数据股份有限公司 + - 76、安信信托 + - 77、岚儒财富 + - 78、捷道软件 + - 79、湖北享七网络科技有限公司 + - 80、湖南创发科技责任有限公司 + - 81、深圳小安时代互联网金融服务有限公司 + - 82、湖北享七网络科技有限公司 + - 83、钱包行云(北京)科技有限公司 + - 84、360金融【360】 + - 85、易企秀 + - 86、摩贝(上海)生物科技有限公司 + - 87、广东芯智慧科技有限公司 + - 88、联想集团【联想】 + - 89、怪兽充电 + - 90、行圆汽车 + - 91、深圳店店通科技邮箱公司 + - 92、京东【京东】 + - 93、米庄理财 + - 94、咖啡易融 + - 95、梧桐诚选 + - 96、恒大地产【恒大】 + - 97、昆明龙慧 + - 98、上海涩瑶软件 + - 99、易信【网易】 + - 100、铜板街 + - 101、杭州云若网络科技有限公司 + - 102、特百惠(中国)有限公司 + - 103、常山众卡运力供应链管理有限公司 + - 104、深圳立创电子商务有限公司 + - 105、杭州智诺科技股份有限公司 + - 106、北京云漾信息科技有限公司 + - 107、深圳市多银科技有限公司 + - 108、亲宝宝 + - 109、上海博卡软件科技有限公司 + - 110、智慧树在线教育平台 + - 111、米族金融 + - 112、北京辰森世纪 + - 113、云南滇医通 + - 114、广州市分领网络科技有限责任公司 + - 115、浙江微能科技有限公司 + - 116、上海馨飞电子商务有限公司 + - 117、上海宝尊电子商务有限公司 + - 118、直客通科技技术有限公司 + - 119、科度科技有限公司 + - 120、上海数慧系统技术有限公司 + - 121、我的医药网 + - 122、多粉平台 + - 123、铁甲二手机 + - 124、上海海新得数据技术有限公司 + - 125、深圳市珍爱网信息技术有限公司【珍爱网】 + - 126、小蜜蜂 + - 127、吉荣数科技 + - 128、上海恺域信息科技有限公司 + - 129、广州荔支网络有限公司【荔枝FM】 + - 130、杭州闪宝科技有限公司 + - 131、北京互联新网科技发展有限公司 + - 132、誉道科技 + - 133、山西兆盛房地产开发有限公司 + - 134、北京蓝睿通达科技有限公司 + - 135、月亮小屋(中国)有限公司【蓝月亮】 + - 136、青岛国瑞信息技术有限公司 + - 137、博雅云计算(北京)有限公司 + - 138、华泰证券香港子公司 + - 139、杭州东方通信软件技术有限公司 + - 140、武汉博晟安全技术股份有限公司 + - 141、深圳市六度人和科技有限公司 + - 142、杭州趣维科技有限公司(小影) + - 143、宁波单车侠之家科技有限公司【单车侠】 + - 144、丁丁云康信息科技(北京)有限公司 + - 145、云钱袋 + - 146、南京中兴力维 + - 147、上海矽昌通信技术有限公司 + - 148、深圳萨科科技 + - 149、中通服创立科技有限责任公司 + - 150、深圳市对庄科技有限公司 + - 151、上证所信息网络有限公司 + - 152、杭州火烧云科技有限公司【婚礼纪】 + - 153、天津青芒果科技有限公司【芒果头条】 + - 154、长飞光纤光缆股份有限公司 + - 155、世纪凯歌(北京)医疗科技有限公司 + - 156、浙江霖梓控股有限公司 + - 157、江西腾飞网络技术有限公司 + - 158、安迅物流有限公司 + - 159、肉联网 + - 160、北京北广梯影广告传媒有限公司 + - 161、上海数慧系统技术有限公司 + - 162、大志天成 + - 163、上海云鹊医 + - 164、上海云鹊医 + - 165、墨迹天气【墨迹天气】 + - 166、上海逸橙信息科技有限公司 + - 167、沅朋物联 + - 168、杭州恒生云融网络科技有限公司 + - 169、绿米联创 + - 170、重庆易宠科技有限公司 + - 171、安徽引航科技有限公司(乐职网) + - 172、上海数联医信企业发展有限公司 + - 173、良彬建材 + - 174、杭州求是同创网络科技有限公司 + - 175、荷马国际 + - 176、点雇网 + - 177、深圳市华星光电技术有限公司 + - 178、厦门神州鹰软件科技有限公司 + - 179、深圳市招商信诺人寿保险有限公司 + - 180、上海好屋网信息技术有限公司 + - 181、海信集团【海信】 + - 182、信凌可信息科技(上海)有限公司 + - 183、长春天成科技发展有限公司 + - 184、用友金融信息技术股份有限公司【用友】 + - 185、北京咖啡易融有限公司 + - 186、国投瑞银基金管理有限公司 + - 187、晋松(上海)网络信息技术有限公司 + - 188、深圳市随手科技有限公司【随手记】 + - 189、深圳水务科技有限公司 + - 190、易企秀【易企秀】 + - 191、北京磁云科技 + - 192、南京蜂泰互联网科技有限公司 + - 193、章鱼直播 + - 194、奖多多科技 + - 195、天津市神州商龙科技股份有限公司 + - 196、岩心科技 + - 197、车码科技(北京)有限公司 + - 198、贵阳市投资控股集团 + - 199、康旗股份 + - 200、龙腾出行 + - 201、杭州华量软件 + - 202、合肥顶岭医疗科技有限公司 + - 203、重庆表达式科技有限公司 + - 204、上海米道信息科技有限公司 + - 205、北京益友会科技有限公司 + - 206、北京融贯电子商务有限公司 + - 207、中国外汇交易中心 + - 208、中国外运股份有限公司 + - 209、中国上海晓圈教育科技有限公司 + - 210、普联软件股份有限公司 + - 211、北京科蓝软件股份有限公司 + - 212、江苏斯诺物联科技有限公司 + - 213、北京搜狐-狐友【搜狐】 + - 214、新大陆网商金融 + - 215、山东神码中税信息科技有限公司 + - 216、河南汇顺网络科技有限公司 + - 217、北京华夏思源科技发展有限公司 + - 218、上海东普信息科技有限公司 + - 219、上海鸣勃网络科技有限公司 + - 220、广东学苑教育发展有限公司 + - 221、深圳强时科技有限公司 + - 222、上海云砺信息科技有限公司 + - 223、重庆愉客行网络有限公司 + - 224、数云 + - 225、国家电网运检部 + - 226、杭州找趣 + - 227、浩鲸云计算科技股份有限公司 + - 228、科大讯飞【科大讯飞】 + - 229、杭州行装网络科技有限公司 + - 230、即有分期金融 + - 231、深圳法司德信息科技有限公司 + - 232、上海博复信息科技有限公司 + - 233、杭州云嘉云计算有限公司 + - 234、有家民宿(有家美宿) + - 235、北京赢销通软件技术有限公司 + - 236、浙江聚有财金融服务外包有限公司 + - 237、易族智汇(北京)科技有限公司 + - 238、合肥顶岭医疗科技开发有限公司 + - 239、车船宝(深圳)旭珩科技有限公司) + - 240、广州富力地产有限公司 + - 241、氢课(上海)教育科技有限公司 + - 242、武汉氪细胞网络技术有限公司 + - 243、杭州有云科技有限公司 + - 244、上海仙豆智能机器人有限公司 + - 245、拉卡拉支付股份有限公司【拉卡拉】 + - 246、虎彩印艺股份有限公司 + - 247、北京数微科技有限公司 + - 248、广东智瑞科技有限公司 + - 249、找钢网 + - 250、九机网 + - 251、杭州跑跑网络科技有限公司 + - 252、深圳未来云集 + - 253、杭州每日给力科技有限公司 + - 254、上海齐犇信息科技有限公司 + - 255、滴滴出行【滴滴】 + - 256、合肥云诊信息科技有限公司 + - 257、云知声智能科技股份有限公司 + - 258、南京坦道科技有限公司 + - 259、爱乐优(二手平台) + - 260、猫眼电影(私有化部署)【猫眼电影】 + - 261、美团大象(私有化部署)【美团大象】 + - 262、作业帮教育科技(北京)有限公司【作业帮】 + - 263、北京小年糕互联网技术有限公司 + - 264、山东矩阵软件工程股份有限公司 + - 265、陕西国驿软件科技有限公司 + - 266、君开信息科技 + - 267、村鸟网络科技有限责任公司 + - 268、云南国际信托有限公司 + - 269、金智教育 + - 270、珠海市筑巢科技有限公司 + - 271、上海百胜软件股份有限公司 + - 272、深圳市科盾科技有限公司 + - 273、哈啰出行【哈啰】 + - 274、途虎养车【途虎】 + - 275、卡思优派人力资源集团 + - 276、南京观为智慧软件科技有限公司 + - 277、杭州城市大脑科技有限公司 + - 278、猿辅导【猿辅导】 + - 279、洛阳健创网络科技有限公司 + - 280、魔力耳朵 + - 281、亿阳信通 + - 282、上海招鲤科技有限公司 + - 283、四川商旅无忧科技服务有限公司 + - 284、UU跑腿 + - 285、北京老虎证券【老虎证券】 + - 286、悠活省吧(北京)网络科技有限公司 + - 287、F5未来商店 + - 288、深圳环阳通信息技术有限公司 + - 289、遠傳電信 + - 290、作业帮(北京)教育科技有限公司【作业帮】 + - 291、成都科鸿智信科技有限公司 + - 292、北京木屋时代科技有限公司 + - 293、大学通(哈尔滨)科技有限责任公司 + - 294、浙江华坤道威数据科技有限公司 + - 295、吉祥航空【吉祥航空】 + - 296、南京圆周网络科技有限公司 + - 297、广州市洋葱omall电子商务 + - 298、天津联物科技有限公司 + - 299、跑哪儿科技(北京)有限公司 + - 300、深圳市美西西餐饮有限公司(喜茶) + - 301、平安不动产有限公司【平安】 + - 302、江苏中海昇物联科技有限公司 + - 303、湖南牙医帮科技有限公司 + - 304、重庆民航凯亚信息技术有限公司(易通航) + - 305、递易(上海)智能科技有限公司 + - 306、亚朵 + - 307、浙江新课堂教育股份有限公司 + - 308、北京蜂创科技有限公司 + - 309、德一智慧城市信息系统有限公司 + - 310、北京翼点科技有限公司 + - 311、湖南智数新维度信息科技有限公司 + - 312、北京玖扬博文文化发展有限公司 + - 313、上海宇珩信息科技有限公司 + - 314、全景智联(武汉)科技有限公司 + - 315、天津易客满国际物流有限公司 + - 316、南京爱福路汽车科技有限公司 + - 317、我房旅居集团 + - 318、湛江亲邻科技有限公司 + - 319、深圳市姜科网络有限公司 + - 320、青岛日日顺物流有限公司 + - 321、南京太川信息技术有限公司 + - 322、美图之家科技优先公司【美图】 + - 323、南京太川信息技术有限公司 + - 324、众薪科技(北京)有限公司 + - 325、武汉安安物联科技有限公司 + - 326、北京智客朗道网络科技有限公司 + - 327、深圳市超级猩猩健身管理管理有限公司 + - 328、重庆达志科技有限公司 + - 329、上海享评信息科技有限公司 + - 330、薪得付信息科技 + - 331、跟谁学 + - 332、中道(苏州)旅游网络科技有限公司 + - 333、广州小卫科技有限公司 + - 334、上海非码网络科技有限公司 + - 335、途家网网络技术(北京)有限公司【途家】 + - 336、广州辉凡信息科技有限公司 + - 337、天维尔信息科技股份有限公司 + - 338、上海极豆科技有限公司 + - 339、苏州触达信息技术有限公司 + - 340、北京热云科技有限公司 + - 341、中智企服(北京)科技有限公司 + - 342、易联云计算(杭州)有限责任公司 + - 343、青岛航空股份有限公司【青岛航空】 + - 344、山西博睿通科技有限公司 + - 345、网易杭州网络有限公司【网易】 + - 346、北京果果乐学科技有限公司 + - 347、百望股份有限公司 + - 348、中保金服(深圳)科技有限公司 + - 349、天津运友物流科技股份有限公司 + - 350、广东创能科技股份有限公司 + - 351、上海倚博信息科技有限公司 + - 352、深圳百果园实业(集团)股份有限公司 + - 353、广州细刻网络科技有限公司 + - 354、武汉鸿业众创科技有限公司 + - 355、金锡科技(广州)有限公司 + - 356、易瑞国际电子商务有限公司 + - 357、奇点云 + - 358、中视信息科技有限公司 + - 359、开源项目:datax-web + - 360、云知声智能科技股份有限公司 + - 361、开源项目:bboss + - 362、成都深驾科技有限公司 + - 363、FunPlus【趣加】 + - 364、杭州创匠信科技有限公司 + - 365、龙匠(北京)科技发展有限公司 + - 366、广州一链通互联网科技有限公司 + - 367、上海星艾网络科技有限公司 + - 368、虎博网络技术(上海)有限公司 + - 369、青岛优米信息技术有限公司 + - 370、八维通科技有限公司 + - 371、烟台合享智星数据科技有限公司 + - 372、东吴证券股份有限公司 + - 373、中通云仓股份有限公司【中通】 + - 374、北京加菲猫科技有限公司 + - 375、北京匠心演绎科技有限公司 + - 376、宝贝走天下 + - 377、厦门众库科技有限公司 + - 378、海通证券数据中心 + - 389、湖南快乐通宝小额贷款有限公司 + - 380、浙江大华技术股份有限公司 + - 381、杭州魔筷科技有限公司 + - 382、青岛掌讯通区块链科技有限公司 + - 383、新大陆金融科技 + - 384、常州玺拓软件科技有限公司 + - 385、北京正保网格教育科技有限公司 + - 386、统一企业(中国)投资有限公司【统一】 + - 387、微革网络科技有限公司 + - 388、杭州融易算科技有限公司 + - 399、青岛上啥班网络科技有限公司 + - 390、京东酒世界 + - 391、杭州爱博仕科技有限公司 + - 392、五星金服控股有限公司 + - 393、福建乐摩物联科技有限公司 + - 394、百炼智能科技有限公司 + - 395、山东能源数智云科技有限公司 + - 396、招商局能源运输股份有限公司 + - 397、三一集团【三一】 + - 398、东巴文(深圳)健康管理有限公司 + - 399、索易软件 + - 400、深圳市宁远科技有限公司 + - 401、熙牛医疗 + - 402、南京智鹤电子科技有限公司 + - 403、嘀嗒出行【嘀嗒出行】 + - 404、广州虎牙信息科技有限公司【虎牙】 + - 405、广州欧莱雅百库网络科技有限公司【欧莱雅】 + - 406、微微科技有限公司 + - 407、我爱我家房地产经纪有限公司【我爱我家】 + - 408、九号发现 + - 409、薪人薪事 + - 410、武汉氪细胞网络技术有限公司 + - 411、广州市斯凯奇商业有限公司 + - 412、微淼商学院 + - 413、杭州车盛科技有限公司 + - 414、深兰科技(上海)有限公司 + - 415、安徽中科美络信息技术有限公司 + - 416、比亚迪汽车工业有限公司【比亚迪】 + - 417、湖南小桔信息技术有限公司 + - 418、安徽科大国创软件科技有限公司 + - 419、克而瑞 + - 420、陕西云基华海信息技术有限公司 + - 421、安徽深宁科技有限公司 + - 422、广东康爱多数字健康有限公司 + - 423、嘉里电子商务 + - 424、上海时代光华教育发展有限公司 + - 425、CityDo + - 426、上海禹知信息科技有限公司 + - 427、广东智瑞科技有限公司 + - 428、西安爱铭网络科技有限公司 + - 429、心医国际数字医疗系统(大连)有限公司 + - 430、乐其电商 + - 431、锐达科技 + - 432、天津长城滨银汽车金融有限公司 + - 433、代码网 + - 434、东莞市东城乔伦软件开发工作室 + - 435、浙江百应科技有限公司 + - 436、上海力爱帝信息技术有限公司(Red E) + - 437、云徙科技有限公司 + - 438、北京康智乐思网络科技有限公司【大姨吗APP】 + - 439、安徽开元瞬视科技有限公司 + - 440、立方 + - 441、厦门纵行科技 + - 442、乐山-菲尼克斯半导体有限公司 + - 443、武汉光谷联合集团有限公司 + - 444、上海金仕达软件科技有限公司 + - 445、深圳易世通达科技有限公司 + - 446、爱动超越人工智能科技(北京)有限责任公司 + - 447、迪普信(北京)科技有限公司 + - 448、掌站科技(北京)有限公司 + - 449、深圳市华云中盛股份有限公司 + - 450、上海原圈科技有限公司 + - 451、广州赞赏信息科技有限公司 + - 452、Amber Group + - 453、德威国际货运代理(上海)公司 + - 454、浙江杰夫兄弟智慧科技有限公司 + - 455、信也科技 + - 456、开思时代科技(深圳)有限公司 + - 457、大连槐德科技有限公司 + - 458、同程生活 + - 459、松果出行 + - 460、企鹅杏仁集团 + - 461、宁波科云信息科技有限公司 + - 462、上海格蓝威驰信息科技有限公司 + - 463、杭州趣淘鲸科技有限公司 + - 464、湖州市数字惠民科技有限公司 + - 465、乐普(北京)医疗器械股份有限公司 + - 466、广州市晴川高新技术开发有限公司 + - 467、山西缇客科技有限公司 + - 468、徐州卡西穆电子商务有限公司 + - 469、格创东智科技有限公司 + - 470、世纪龙信息网络有限责任公司 + - 471、邦道科技有限公司 + - 472、河南中盟新云科技股份有限公司 + - 473、横琴人寿保险有限公司 + - 474、上海海隆华钟信息技术有限公司 + - 475、上海久湛 + - 476、上海仙豆智能机器人有限公司 + - 477、广州汇尚网络科技有限公司 + - 478、深圳市阿卡索资讯股份有限公司 + - 479、青岛佳家康健康管理有限责任公司 + - 480、蓝城兄弟 + - 481、成都天府通金融服务股份有限公司 + - 482、深圳云镖网络科技有限公司 + - 483、上海影创科技 + - 484、成都艾拉物联 + - 485、北京客邻尚品网络技术有限公司 + - 486、IT实战联盟 + - 487、杭州尤拉夫科技有限公司 + - 488、中大检测(湖南)股份有限公司 + - 489、江苏电老虎工业互联网股份有限公司 + - 490、上海助通信息科技有限公司 + - 491、北京符节科技有限公司 + - 492、杭州英祐科技有限公司 + - 493、江苏电老虎工业互联网股份有限公司 + - 494、深圳市点猫科技有限公司 + - 495、杭州天音 + - 496、深圳市二十一科技互联网有限公司 + - 497、海南海口翎度科技 + - 498、北京小趣智品科技有限公司 + - 499、广州石竹计算机软件有限公司 + - 500、深圳市惟客数据科技有限公司 + - 501、中国医疗器械有限公司 + - 502、上海云谦科技有限公司 + - 503、上海磐农信息科技有限公司 + - 504、广州领航食品有限公司 + - 505、青岛掌讯通区块链科技有限公司 + - 506、北京新网数码信息技术有限公司 + - 507、超体信息科技(深圳)有限公司 + - 508、长沙店帮手信息科技有限公司 + - 509、上海助弓装饰工程有限公司 + - 510、杭州寻联网络科技有限公司 + - 511、成都大淘客科技有限公司 + - 512、松果出行 + - 513、深圳市唤梦科技有限公司 + - 514、上汽集团商用车技术中心 + - 515、北京中航讯科技股份有限公司 + - 516、北龙中网(北京)科技有限责任公司 + - 517、前海超级前台(深圳)信息技术有限公司 + - 518、上海中商网络股份有限公司 + - 519、上海助通信息科技有限公司 + - 520、宁波聚臻智能科技有限公司 + - 521、上海零动数码科技股份有限公司 + - 522、浙江学海教育科技有限公司 + - 523、聚学云(山东)信息技术有限公司 + - 524、多氟多新材料股份有限公司 + - 525、智慧眼科技股份有限公司 + - 526、广东智通人才连锁股份有限公司 + - 527、世纪开元智印互联科技集团股份有限公司 + - 528、北京理想汽车【理想汽车】 + - 529、巽逸科技(重庆)有限公司 + - 530、义乌购电子商务有限公司 + - 531、深圳市珂莱蒂尔服饰有限公司 + - 532、江西国泰利民信息科技有限公司 + - 533、广西广电大数据科技有限公司 + - 534、杭州艾麦科技有限公司 + - 535、广州小滴科技有限公司 + - 536、佳缘科技股份有限公司 + - 537、上海深擎信息科技有限公司 + - 538、武商网 + - 539、福建民本信息科技有限公司 + - 540、杭州惠合信息科技有限公司 + - 541、厦门爱立得科技有限公司 + - 542、成都拟合未来科技有限公司 + - 543、宁波聚臻智能科技有限公司 + - 544、广东百慧科技有限公司 + - 545、笨马网络 + - 546、深圳市信安数字科技有限公司 + - 547、深圳市思乐数据技术有限公司 + - 548、四川绿源集科技有限公司 + - 549、湖南云医链生物科技有限公司 + - 550、杭州源诚科技有限公司 + - 551、北京开课吧科技有限公司 + - 552、北京多来点信息技术有限公司 + - 553、JEECG BOOT低代码开发平台 + - 554、苏州同元软控信息技术有限公司 + - 555、江苏大泰信息技术有限公司 + - 556、北京大禹汇智 + - 557、北京盛哲科技有限公司 + - 558、广州钛动科技有限公司 + - 559、北京大禹汇智科技有限公司 + - 560、湖南鼎翰文化股份有限公司 + - 561、苏州安软信息科技有限公司 + - 562、芒果tv + - 563、上海艺赛旗软件股份有限公司 + - 564、中盈优创资讯科技有限公司 + - 565、乐乎公寓 + - 566、启明信息 + - 567、苏州安软 + - 568、南京富金的软件科技有限公司 + - 569、深圳市新科聚合网络技术有限公司 + - 570、你好现在(北京)科技股份有限公司 + - 571、360考试宝典 + - 572、北京一零科技有限公司 + - 573、厦门星纵信息 + - 574、Dalligent Solusi Indonesia + - 575、深圳华普物联科技有限公司 + - 576、深圳行健自动化股份有限公司 + - 577、深圳市富融信息科技服务有限公司 + - 578、蓝鸟云 + - 579、上海澎博财经资讯有限公司 + - 580、北京小鸦科技有限公司 + - 581、杭州盈泉云科技有限公司 + - 582、惟客数据 + - 583、GOSO香蜜闺秀 + - 584、普乐师(上海)数字科技有限公司 + - 585、西安市雁塔区咖北堂网络科技部 + - 586、宁波聚臻智能科技有限公司 + - 587、普乐师数字科技有限公司 + - 588、江苏蟹联网科技有限公司 + - 589、杭州未智科技有限公司 + - 590、安吉智行物流有限公司 + - 591、华生大家居集团有限公司 + - 592、美心食品(广州)有限公司 + - 593、货拉拉【货拉拉APP】 + - 594、杭州思韬瑞科技有限公司 + - 595、杭州玖融科技有限公司 + - 596、北京优海网络科技有限公司 + - 597、浙江大维高新技术股份有限公司 + - 598、粤港澳大湾区数字经济研究院 + - 599、普康(杭州)健康科技有限公司 + - 600、华西证券股份有限公司【华西证券】 + - 601、杭州海康机器人股份有限公司【海康】 + - 602、河南宸邦信息技术有限公司 + - 603、成都次元节点网络科技有限公司 + - …… + +> 更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。 + +欢迎大家的关注和使用,XXL-JOB也将拥抱变化,持续发展。 + + +### 1.5 下载 + +#### 文档地址 + +- [中文文档](https://www.xuxueli.com/xxl-job/) +- [English Documentation](https://www.xuxueli.com/xxl-job/en/) + +#### 源码仓库地址 + +源码仓库地址 | Release Download +--- | --- +[https://github.com/xuxueli/xxl-job](https://github.com/xuxueli/xxl-job) | [Download](https://github.com/xuxueli/xxl-job/releases) +[http://gitee.com/xuxueli0323/xxl-job](http://gitee.com/xuxueli0323/xxl-job) | [Download](http://gitee.com/xuxueli0323/xxl-job/releases) + + +#### 中央仓库地址 + +``` + + + com.xuxueli + xxl-job-core + ${最新稳定版本} + +``` + + +### 1.6 环境 +- Maven3+ +- Jdk1.8+ +- Mysql5.7+ + + +## 二、快速入门 + +### 2.1 初始化“调度数据库” +请下载项目源码并解压,获取 "调度数据库初始化SQL脚本" 并执行即可。 + +"调度数据库初始化SQL脚本" 位置为: + + /xxl-job/doc/db/tables_xxl_job.sql + +调度中心支持集群部署,集群情况下各节点务必连接同一个mysql实例; + +如果mysql做主从,调度中心集群节点务必强制走主库; + +### 2.2 编译源码 +解压源码,按照maven格式将源码导入IDE, 使用maven进行编译即可,源码结构如下: + + xxl-job-admin:调度中心 + xxl-job-core:公共依赖 + xxl-job-executor-samples:执行器Sample示例(选择合适的版本执行器,可直接使用,也可以参考其并将现有项目改造成执行器) + :xxl-job-executor-sample-springboot:Springboot版本,通过Springboot管理执行器,推荐这种方式; + :xxl-job-executor-sample-frameless:无框架版本; + + +### 2.3 配置部署“调度中心” + + 调度中心项目:xxl-job-admin + 作用:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。 + +#### 步骤一:调度中心配置: +调度中心配置文件地址: + + /xxl-job/xxl-job-admin/src/main/resources/application.properties + + +调度中心配置内容说明: + + ### 调度中心JDBC链接:链接地址请保持和 2.1章节 所创建的调度数据库的地址一致 + spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai + spring.datasource.username=root + spring.datasource.password=root_pwd + spring.datasource.driver-class-name=com.mysql.jdbc.Driver + + ### 报警邮箱 + spring.mail.host=smtp.qq.com + spring.mail.port=25 + spring.mail.username=xxx@qq.com + spring.mail.password=xxx + spring.mail.properties.mail.smtp.auth=true + spring.mail.properties.mail.smtp.starttls.enable=true + spring.mail.properties.mail.smtp.starttls.required=true + spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory + + ### 调度中心通讯TOKEN [选填]:非空时启用; + xxl.job.accessToken= + + ### 调度中心国际化配置 [必填]: 默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文; + xxl.job.i18n=zh_CN + + ## 调度线程池最大线程配置【必填】 + xxl.job.triggerpool.fast.max=200 + xxl.job.triggerpool.slow.max=100 + + ### 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能; + xxl.job.logretentiondays=30 + + + +#### 步骤二:部署项目: +如果已经正确进行上述配置,可将项目编译打包部署。 + +调度中心访问地址:http://localhost:8080/xxl-job-admin (该地址执行器将会使用到,作为回调地址) + +默认登录账号 "admin/123456", 登录后运行界面如下图所示。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_6yC0.png "在这里输入图片标题") + +至此“调度中心”项目已经部署成功。 + +#### 步骤三:调度中心集群(可选): +调度中心支持集群部署,提升调度系统容灾和可用性。 + +调度中心集群部署时,几点要求和建议: +- DB配置保持一致; +- 集群机器时钟保持一致(单机集群忽视); +- 建议:推荐通过nginx为调度中心集群做负载均衡,分配域名。调度中心访问、执行器回调配置、调用API服务等操作均通过该域名进行。 + + +#### 其他:Docker 镜像方式搭建调度中心: + +- 下载镜像 + +``` +// Docker地址:https://hub.docker.com/r/xuxueli/xxl-job-admin/ (建议指定版本号) +docker pull xuxueli/xxl-job-admin +``` + +- 创建容器并运行 + +``` +docker run -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin -d xuxueli/xxl-job-admin:{指定版本} + +/** +* 如需自定义 mysql 等配置,可通过 "-e PARAMS" 指定,参数格式 PARAMS="--key=value --key2=value2" ; +* 配置项参考文件:/xxl-job/xxl-job-admin/src/main/resources/application.properties +* 如需自定义 JVM内存参数 等配置,可通过 "-e JAVA_OPTS" 指定,参数格式 JAVA_OPTS="-Xmx512m" ; +*/ +docker run -e PARAMS="--spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai" -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin -d xuxueli/xxl-job-admin:{指定版本} +``` + + +### 2.4 配置部署“执行器项目” + + “执行器”项目:xxl-job-executor-sample-springboot (提供多种版本执行器供选择,现以 springboot 版本为例,可直接使用,也可以参考其并将现有项目改造成执行器) + 作用:负责接收“调度中心”的调度并执行;可直接部署执行器,也可以将执行器集成到现有业务项目中。 + +#### 步骤一:maven依赖 +确认pom文件中引入了 "xxl-job-core" 的maven依赖; + +#### 步骤二:执行器配置 +执行器配置,配置文件地址: + + /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties + +执行器配置,配置内容说明: + + ### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; + xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin + + ### 执行器通讯TOKEN [选填]:非空时启用; + xxl.job.accessToken= + + ### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 + xxl.job.executor.appname=xxl-job-executor-sample + ### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。 + xxl.job.executor.address= + ### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"; + xxl.job.executor.ip= + ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口; + xxl.job.executor.port=9999 + ### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径; + xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler + ### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能; + xxl.job.executor.logretentiondays=30 + + +#### 步骤三:执行器组件配置 + +执行器组件,配置文件地址: + + /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java + +执行器组件,配置内容说明: + +``` +@Bean +public XxlJobSpringExecutor xxlJobExecutor() { + logger.info(">>>>>>>>>>> xxl-job config init."); + XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); + xxlJobSpringExecutor.setAdminAddresses(adminAddresses); + xxlJobSpringExecutor.setAppname(appname); + xxlJobSpringExecutor.setIp(ip); + xxlJobSpringExecutor.setPort(port); + xxlJobSpringExecutor.setAccessToken(accessToken); + xxlJobSpringExecutor.setLogPath(logPath); + xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); + + return xxlJobSpringExecutor; +} +``` + +#### 步骤四:部署执行器项目: +如果已经正确进行上述配置,可将执行器项目编译打部署,系统提供多种执行器Sample示例项目,选择其中一个即可,各自的部署方式如下。 + + xxl-job-executor-sample-springboot:项目编译打包成springboot类型的可执行JAR包,命令启动即可; + xxl-job-executor-sample-frameless:项目编译打包成JAR包,命令启动即可; + + +至此“执行器”项目已经部署结束。 + +#### 步骤五:执行器集群(可选): +执行器支持集群部署,提升调度系统可用性,同时提升任务处理能力。 + +执行器集群部署时,几点要求和建议: +- 执行器回调地址(xxl.job.admin.addresses)需要保持一致;执行器根据该配置进行执行器自动注册等操作。 +- 同一个执行器集群内AppName(xxl.job.executor.appname)需要保持一致;调度中心根据该配置动态发现不同集群的在线执行器列表。 + + +### 2.5 开发第一个任务“Hello World” +本示例以新建一个 “GLUE模式(Java)” 运行模式的任务为例。更多有关任务的详细配置,请查看“章节三:任务详解”。 +( “GLUE模式(Java)”的执行代码托管到调度中心在线维护,相比“Bean模式任务”需要在执行器项目开发部署上线,更加简便轻量) + +> 前提:请确认“调度中心”和“执行器”项目已经成功部署并启动; + +#### 步骤一:新建任务: +登录调度中心,点击下图所示“新建任务”按钮,新建示例任务。然后,参考下面截图中任务的参数配置,点击保存。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_o8HQ.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "在这里输入图片标题") + + +#### 步骤二:“GLUE模式(Java)” 任务开发: +请点击任务右侧 “GLUE” 按钮,进入 “GLUE编辑器开发界面” ,见下图。“GLUE模式(Java)” 运行模式的任务默认已经初始化了示例任务代码,即打印Hello World。 +( “GLUE模式(Java)” 运行模式的任务实际上是一段继承自IJobHandler的Java类代码,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务,详细介绍请查看第三章节) + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Fgql.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题") + +#### 步骤三:触发执行: +请点击任务右侧 “执行” 按钮,可手动触发一次任务执行(通常情况下,通过配置Cron表达式进行任务调度触发)。 + +#### 步骤四:查看日志: +请点击任务右侧 “日志” 按钮,可前往任务日志界面查看任务日志。 +在任务日志界面中,可查看该任务的历史调度记录以及每一次调度的任务调度信息、执行参数和执行信息。运行中的任务点击右侧的“执行日志”按钮,可进入日志控制台查看实时执行日志。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_inc8.png "在这里输入图片标题") + +在日志控制台,可以Rolling方式实时查看任务在执行器一侧运行输出的日志信息,实时监控任务进度; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_eYrv.png "在这里输入图片标题") + +## 三、任务详解 + +### 配置属性详细说明: + + 基础配置: + - 执行器:任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器, 实现任务自动发现功能; 另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器, 可在 "执行器管理" 进行设置; + - 任务描述:任务的描述信息,便于任务管理; + - 负责人:任务的负责人; + - 报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔; + + 触发配置: + - 调度类型: + 无:该类型不会主动触发调度; + CRON:该类型将会通过CRON,触发任务调度; + 固定速度:该类型将会以固定速度,触发任务调度;按照固定的间隔时间,周期性触发; + 固定延迟:该类型将会以固定延迟,触发任务调度;按照固定的延迟时间,从上次调度结束后开始计算延迟时间,到达延迟时间后触发下次调度; + - CRON:触发任务执行的Cron表达式; + - 固定速度:固定速度的时间间隔,单位为秒; + - 固定延迟:固定延迟的时间间隔,单位为秒; + + 任务配置: + - 运行模式: + BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务; + GLUE模式(Java):任务以源码方式维护在调度中心;该模式的任务实际上是一段继承自IJobHandler的Java类代码并 "groovy" 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务; + GLUE模式(Shell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "shell" 脚本; + GLUE模式(Python):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "python" 脚本; + GLUE模式(PHP):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "php" 脚本; + GLUE模式(NodeJS):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "nodejs" 脚本; + GLUE模式(PowerShell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "PowerShell" 脚本; + - JobHandler:运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@JobHandler”注解自定义的value值; + - 执行参数:任务执行所需的参数; + + 高级配置: + - 路由策略:当执行器集群部署时,提供丰富的路由策略,包括; + FIRST(第一个):固定选择第一个机器; + LAST(最后一个):固定选择最后一个机器; + ROUND(轮询):; + RANDOM(随机):随机选择在线的机器; + CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。 + LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举; + LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举; + FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度; + BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度; + SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务; + - 子任务:每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),当本任务执行结束并且执行成功时,将会触发子任务ID所对应的任务的一次主动调度。 + - 调度过期策略: + - 忽略:调度过期后,忽略过期的任务,从当前时间开始重新计算下次触发时间; + - 立即执行一次:调度过期后,立即执行一次,并从当前时间开始重新计算下次触发时间; + - 阻塞处理策略:调度过于密集执行器来不及处理时的处理策略; + 单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行; + 丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败; + 覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务; + - 任务超时时间:支持自定义任务超时时间,任务运行超时将会主动中断任务; + - 失败重试次数;支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试; + + + + + +### 3.1 BEAN模式(类形式) + +Bean模式任务,支持基于类的开发方式,每个任务对应一个Java类。 + +- 优点:不限制项目环境,兼容性好。即使是无框架项目,如main方法直接启动的项目也可以提供支持,可以参考示例项目 "xxl-job-executor-sample-frameless"; +- 缺点: + - 每个任务需要占用一个Java类,造成类的浪费; + - 不支持自动扫描任务并注入到执行器容器,需要手动注入。 + +#### 步骤一:执行器项目中,开发Job类: + + 1、开发一个继承自"com.xxl.job.core.handler.IJobHandler"的JobHandler类,实现其中任务方法。 + 2、手动通过如下方式注入到执行器容器。 + ``` + XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler()); + ``` + +#### 步骤二:调度中心,新建调度任务 +后续步骤和 "3.2 BEAN模式(方法形式)"一致,可以前往参考。 + + +### 3.2 BEAN模式(方法形式) + +Bean模式任务,支持基于方法的开发方式,每个任务对应一个方法。 + +- 优点: + - 每个任务只需要开发一个方法,并添加"@XxlJob"注解即可,更加方便、快速。 + - 支持自动扫描任务并注入到执行器容器。 +- 缺点:要求Spring容器环境; + +>基于方法开发的任务,底层会生成JobHandler代理,和基于类的方式一样,任务也会以JobHandler的形式存在于执行器任务容器中。 + +#### 步骤一:执行器项目中,开发Job方法: + + 1、任务开发:在Spring Bean实例中,开发Job方法; + 2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。 + 3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志; + 4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果; + +``` +// 可参考Sample示例执行器中的 "com.xxl.job.executor.service.jobhandler.SampleXxlJob" ,如下: +@XxlJob("demoJobHandler") +public void demoJobHandler() throws Exception { + XxlJobHelper.log("XXL-JOB, Hello World."); +} +``` + +#### 步骤二:调度中心,新建调度任务 +参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "BEAN模式",JobHandler属性填写任务注解“@XxlJob”中定义的值; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAsz.png "在这里输入图片标题") + +#### 原生内置Bean模式任务 +为方便用户参考与快速实用,示例执行器内原生提供多个Bean模式任务Handler,可以直接配置实用,如下: + +- demoJobHandler:简单示例任务,任务内部模拟耗时任务逻辑,用户可在线体验Rolling Log等功能; +- shardingJobHandler:分片示例任务,任务内部模拟处理分片参数,可参考熟悉分片任务; +- httpJobHandler:通用HTTP任务Handler;业务方只需要提供HTTP链接等信息即可,不限制语言、平台。示例任务入参如下: + ``` + url: http://www.xxx.com + method: get 或 post + data: post-data + ``` +- commandJobHandler:通用命令行任务Handler;业务方只需要提供命令行即可;如 “pwd”命令; + + +### 3.3 GLUE模式(Java) +任务以源码方式维护在调度中心,支持通过Web IDE在线更新,实时编译和生效,因此不需要指定JobHandler。开发流程如下: + +#### 步骤一:调度中心,新建调度任务: +参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(Java)"; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tJOq.png "在这里输入图片标题") + +#### 步骤二:开发任务代码: +选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。 + +版本回溯功能(支持30个版本的版本回溯):在GLUE任务的Web IDE界面,选择右上角下拉框“版本回溯”,会列出该GLUE的更新历史,选择相应版本即可显示该版本代码,保存后GLUE代码即回退到对应的历史版本; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_dNUJ.png "在这里输入图片标题") + +### 3.4 GLUE模式(Shell) + +#### 步骤一:调度中心,新建调度任务 +参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(Shell)"; + +#### 步骤二:开发任务代码: +选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。 + +该模式的任务实际上是一段 "shell" 脚本; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_iUw0.png "在这里输入图片标题") + +### 3.4 GLUE模式(Python) + +#### 步骤一:调度中心,新建调度任务 +参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(Python)"; + +#### 步骤二:开发任务代码: +选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。 + +该模式的任务实际上是一段 "python" 脚本; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_BPLG.png "在这里输入图片标题") + +### 3.5 GLUE模式(NodeJS) + +#### 步骤一:调度中心,新建调度任务 +参考上文“配置属性详细说明”对新建的任务进行参数配置,运行模式选中 "GLUE模式(NodeJS)"; + +#### 步骤二:开发任务代码: +选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。 + +该模式的任务实际上是一段 "nodeJS" 脚本; + +### 3.6 GLUE模式(PHP) +同上 + +### 3.7 GLUE模式(PowerShell) +同上 + + + +## 四、操作指南 + +### 4.1 配置执行器 +点击进入"执行器管理"界面, 如下图: +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Hr2T.png "在这里输入图片标题") + + 1、"调度中心OnLine:"右侧显示在线的"调度中心"列表, 任务执行结束后, 将会以failover的模式进行回调调度中心通知执行结果, 避免回调的单点风险; + 2、"执行器列表" 中显示在线的执行器列表, 可通过"OnLine 机器"查看对应执行器的集群机器。 + +点击按钮 "+新增执行器" 弹框如下图, 可新增执行器配置: + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_V3vF.png "在这里输入图片标题") + +执行器属性说明 + + AppName: 是每个执行器集群的唯一标示AppName, 执行器会周期性以AppName为对象进行自动注册。可通过该配置自动发现注册成功的执行器, 供任务调度时使用; + 名称: 执行器的名称, 因为AppName限制字母数字等组成,可读性不强, 名称为了提高执行器的可读性; + 排序: 执行器的排序, 系统中需要执行器的地方,如任务新增, 将会按照该排序读取可用的执行器列表; + 注册方式:调度中心获取执行器地址的方式; + 自动注册:执行器自动进行执行器注册,调度中心通过底层注册表可以动态发现执行器机器地址; + 手动录入:人工手动录入执行器的地址信息,多地址逗号分隔,供调度中心使用; + 机器地址:"注册方式"为"手动录入"时有效,支持人工维护执行器的地址信息; + +### 4.2 新建任务 +进入任务管理界面,点击“新增任务”按钮,在弹出的“新增任务”界面配置任务属性后保存即可。详情页参考章节 "三、任务详解"。 + +### 4.3 编辑任务 +进入任务管理界面,选中指定任务。点击该任务右侧“编辑”按钮,在弹出的“编辑任务”界面更新任务属性后保存即可,可以修改设置的任务属性信息: + +### 4.4 编辑GLUE代码 + +该操作仅针对GLUE任务。 + +选中指定任务,点击该任务右侧“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发。可参考章节 "3.3 GLUE模式(Java)"。 + +### 4.5 启动/停止任务 +可对任务进行“启动”和“停止”操作。 +需要注意的是,此处的启动/停止仅针对任务的后续调度触发行为,不会影响到已经触发的调度任务,如需终止已经触发的调度任务,可查看“4.9 终止运行中的任务” + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题") + +### 4.6 手动触发一次调度 +点击“执行”按钮,可手动触发一次任务调度,不影响原有调度规则。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题") + +### 4.7 查看调度日志 +点击“日志”按钮,可以查看任务历史调度日志。在历史调入日志界面可查看每次任务调度的调度结果、执行结果等,点击“执行日志”按钮可查看执行器完整日志。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_ZAhX.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_UDSo.png "在这里输入图片标题") + + 调度时间:"调度中心"触发本次调度并向"执行器"发送任务执行信号的时间; + 调度结果:"调度中心"触发本次调度的结果,200表示成功,500或其他表示失败; + 调度备注:"调度中心"触发本次调度的日志信息; + 执行器地址:本次任务执行的机器地址 + 运行模式:触发调度时任务的运行模式,运行模式可参考章节 "三、任务详解"; + 任务参数:本地任务执行的入参 + 执行时间:"执行器"中本次任务执行结束后回调的时间; + 执行结果:"执行器"中本次任务执行的结果,200表示成功,500或其他表示失败; + 执行备注:"执行器"中本次任务执行的日志信息; + 操作: + "执行日志"按钮:点击可查看本地任务执行的详细日志信息;详见“4.8 查看执行日志”; + "终止任务"按钮:点击可终止本地调度对应执行器上本任务的执行线程,包括未执行的阻塞任务一并被终止; + +### 4.8 查看执行日志 +点击执行日志右侧的 “执行日志” 按钮,可跳转至执行日志界面,可以查看业务代码中打印的完整日志,如下图; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_tvGI.png "在这里输入图片标题") + +### 4.9 终止运行中的任务 +仅针对执行中的任务。 +在任务日志界面,点击右侧的“终止任务”按钮,将会向本次任务对应的执行器发送任务终止请求,将会终止掉本次任务,同时会清空掉整个任务执行队列。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_hIci.png "在这里输入图片标题") + +任务终止时通过 "interrupt" 执行线程的方式实现, 将会触发 "InterruptedException" 异常。因此如果JobHandler内部catch到了该异常并消化掉的话, 任务终止功能将不可用。 + +因此, 如果遇到上述任务终止不可用的情况, 需要在JobHandler中应该针对 "InterruptedException" 异常进行特殊处理 (向上抛出) , 正确逻辑如下: +``` +try{ + // do something +} catch (Exception e) { + if (e instanceof InterruptedException) { + throw e; + } + logger.warn("{}", e); +} +``` + +而且,在JobHandler中开启子线程时,子线程也不可catch处理"InterruptedException",应该主动向上抛出。 + +任务终止时会执行对应JobHandler的"destroy()"方法,可以借助该方法处理一些资源回收的逻辑。 + + +### 4.10 删除执行日志 +在任务日志界面,选中执行器和任务之后,点击右侧的"删除"按钮将会出现"日志清理"弹框,弹框中支持选择不同类型的日志清理策略,选中后点击"确定"按钮即可进行日志清理操作; +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Ypik.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_EB65.png "在这里输入图片标题") + +### 4.11 删除任务 +点击删除按钮,可以删除对应任务。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Z9Qr.png "在这里输入图片标题") + +### 4.12 用户管理 +进入 "用户管理" 界面,可查看和管理用户信息; + +目前用户分为两种角色: +- 管理员:拥有全量权限,支持在线管理用户信息,为用户分配权限,权限分配粒度为执行器; +- 普通用户:仅拥有被分配权限的执行器,及相关任务的操作权限; + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_1001.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_1002.png "在这里输入图片标题") + + +## 五、总体设计 +### 5.1 源码目录介绍 + - /doc :文档资料 + - /db :“调度数据库”建表脚本 + - /xxl-job-admin :调度中心,项目源码 + - /xxl-job-core :公共Jar依赖 + - /xxl-job-executor-samples :执行器,Sample示例项目(大家可以在该项目上进行开发,也可以将现有项目改造生成执行器项目) + +### 5.2 “调度数据库”配置 +XXL-JOB调度模块基于自研调度组件并支持集群部署,调度数据库表说明如下: + + - xxl_job_lock:任务调度锁表; + - xxl_job_group:执行器信息表,维护任务执行器信息; + - xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等; + - xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等; + - xxl_job_log_report:调度日志报表:用户存储XXL-JOB任务调度日志的报表,调度中心报表功能页面会用到; + - xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能; + - xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息; + - xxl_job_user:系统用户表; + + +### 5.3 架构设计 +#### 5.3.1 设计思想 +将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。 + +将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。 + +因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性; + +#### 5.3.2 系统组成 +- **调度模块(调度中心)**: + 负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块; + 支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。 +- **执行模块(执行器)**: + 负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效; + 接收“调度中心”的执行请求、终止请求和日志请求等。 + +#### 5.3.3 架构图 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Qohm.png "在这里输入图片标题") + +### 5.4 调度模块剖析 +#### 5.4.1 quartz的不足 +Quartz作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中Quartz采用API的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题: + +- 问题一:调用API的的方式操作任务,不人性化; +- 问题二:需要持久化业务QuartzJobBean到底层数据表中,系统侵入性相当严重。 +- 问题三:调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务; +- 问题四:quartz底层以“抢占式”获取DB锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而XXL-JOB通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。 + +XXL-JOB弥补了quartz的上述不足之处。 + +#### 5.4.2 自研调度模块 +XXL-JOB最终选择自研调度组件(早期调度组件基于Quartz);一方面是为了精简系统降低冗余依赖,另一方面是为了提供系统的可控度与稳定性; + +XXL-JOB中“调度模块”和“任务模块”完全解耦,调度模块进行任务调度时,将会解析不同的任务参数发起远程调用,调用各自的远程执行器服务。这种调用模型类似RPC调用,调度中心提供调用代理的功能,而执行器提供远程服务的功能。 + +#### 5.4.3 调度中心HA(集群) +基于数据库的集群方案,数据库选用Mysql;集群分布式并发环境中进行定时任务调度时,会在各个节点会上报任务,存到数据库中,执行时会从数据库中取出触发器来执行,如果触发器的名称和执行时间相同,则只有一个节点去执行此任务。 + +#### 5.4.4 调度线程池 +调度采用线程池方式实现,避免单线程因阻塞而引起任务调度延迟。 + +#### 5.4.5 并行调度 +XXL-JOB调度模块默认采用并行机制,在多线程调度的情况下,调度模块被阻塞的几率很低,大大提高了调度系统的承载量。 + +XXL-JOB的不同任务之间并行调度、并行执行。 +XXL-JOB的单个任务,针对多个执行器是并行运行的,针对单个执行器是串行执行的。同时支持任务终止。 + +#### 5.4.6 过期处理策略 +任务调度错过触发时间时的处理策略: +- 可能原因:服务重启;调度线程被阻塞,线程被耗尽;上次调度持续阻塞,下次调度被错过; +- 处理策略: + - 过期超5s:本次忽略,当前时间开始计算下次触发时间 + - 过期5s内:立即触发一次,当前时间开始计算下次触发时间 + + +#### 5.4.7 日志回调服务 +调度模块的“调度中心”作为Web服务部署时,一方面承担调度中心功能,另一方面也为执行器提供API服务。 + +调度中心提供的"日志回调服务API服务"代码位置如下: +``` +xxl-job-admin#com.xxl.job.admin.controller.JobApiController.callback +``` + +“执行器”在接收到任务执行请求后,执行任务,在执行结束之后会将执行结果回调通知“调度中心”: + +#### 5.4.8 任务HA(Failover) +执行器如若集群部署,调度中心将会感知到在线的所有执行器,如“127.0.0.1:9997, 127.0.0.1:9998, 127.0.0.1:9999”。 + +当任务"路由策略"选择"故障转移(FAILOVER)"时,当调度中心每次发起调度请求时,会按照顺序对执行器发出心跳检测请求,第一个检测为存活状态的执行器将会被选定并发送调度请求。 + +调度成功后,可在日志监控界面查看“调度备注”,如下; +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jrdI.png "在这里输入图片标题") + +“调度备注”可以看出本地调度运行轨迹,执行器的"注册方式"、"地址列表"和任务的"路由策略"。"故障转移(FAILOVER)"路由策略下,调度中心首先对第一个地址进行心跳检测,心跳失败因此自动跳过,第二个依然心跳检测失败…… +直至心跳检测第三个地址“127.0.0.1:9999”成功,选定为“目标执行器”;然后对“目标执行器”发送调度请求,调度流程结束,等待执行器回调执行结果。 + +#### 5.4.9 调度日志 +调度中心每次进行任务调度,都会记录一条任务日志,任务日志主要包括以下三部分内容: + +- 任务信息:包括“执行器地址”、“JobHandler”和“执行参数”等属性,点击任务ID按钮可查看,根据这些参数,可以精确的定位任务执行的具体机器和任务代码; +- 调度信息:包括“调度时间”、“调度结果”和“调度日志”等,根据这些参数,可以了解“调度中心”发起调度请求时具体情况。 +- 执行信息:包括“执行时间”、“执行结果”和“执行日志”等,根据这些参数,可以了解在“执行器”端任务执行的具体情况; + +调度日志,针对单次调度,属性说明如下: +- 执行器地址:任务执行的机器地址; +- JobHandler:Bean模式表示任务执行的JobHandler名称; +- 任务参数:任务执行的入参; +- 调度时间:调度中心,发起调度的时间; +- 调度结果:调度中心,发起调度的结果,SUCCESS或FAIL; +- 调度备注:调度中心,发起调度的备注信息,如地址心跳检测日志等; +- 执行时间:执行器,任务执行结束后回调的时间; +- 执行结果:执行器,任务执行的结果,SUCCESS或FAIL; +- 执行备注:执行器,任务执行的备注信息,如异常日志等; +- 执行日志:任务执行过程中,业务代码中打印的完整执行日志,见“4.8 查看执行日志”; + +#### 5.4.10 任务依赖 +原理:XXL-JOB中每个任务都对应有一个任务ID,同时,每个任务支持设置属性“子任务ID”,因此,通过“任务ID”可以匹配任务依赖关系。 + +当父任务执行结束并且执行成功时,将会根据“子任务ID”匹配子任务依赖,如果匹配到子任务,将会主动触发一次子任务的执行。 + +在任务日志界面,点击任务的“执行备注”的“查看”按钮,可以看到匹配子任务以及触发子任务执行的日志信息,如无信息则表示未触发子任务执行,可参考下图。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Wb2o.png "在这里输入图片标题") + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_jOAU.png "在这里输入图片标题") + +#### 5.4.11 全异步化 & 轻量级 + +- 全异步化设计:XXL-JOB系统中业务逻辑在远程执行器执行,触发流程全异步化设计。相比直接在调度中心内部执行业务逻辑,极大的降低了调度线程占用时间; + - 异步调度:调度中心每次任务触发时仅发送一次调度请求,该调度请求首先推送“异步调度队列”,然后异步推送给远程执行器 + - 异步执行:执行器会将请求存入“异步执行队列”并且立即响应调度中心,异步运行。 +- 轻量级设计:XXL-JOB调度中心中每个JOB逻辑非常 “轻”,在全异步化的基础上,单个JOB一次运行平均耗时基本在 "10ms" 之内(基本为一次请求的网络开销);因此,可以保证使用有限的线程支撑大量的JOB并发运行; + +得益于上述两点优化,理论上默认配置下的调度中心,单机能够支撑 5000 任务并发运行稳定运行; + +实际场景中,由于调度中心与执行器网络ping延迟不同、DB读写耗时不同、任务调度密集程度不同,会导致任务量上限会上下波动。 + +如若需要支撑更多的任务量,可以通过 "调大调度线程数" 、"降低调度中心与执行器ping延迟" 和 "提升机器配置" 几种方式优化。 + +#### 5.4.12 均衡调度 +调度中心在集群部署时会自动进行任务平均分配,触发组件每次获取与线程池数量(调度中心支持自定义调度线程池大小)相关数量的任务,避免大量任务集中在单个调度中心集群节点; + +### 5.5 任务 "运行模式" 剖析 +#### 5.5.1 "Bean模式" 任务 +开发步骤:可参考 "章节三" ; +原理:每个Bean模式任务都是一个Spring的Bean类实例,它被维护在“执行器”项目的Spring容器中。任务类需要加“@JobHandler(value="名称")”注解,因为“执行器”会根据该注解识别Spring容器中的任务。任务类需要继承统一接口“IJobHandler”,任务逻辑在execute方法中开发,因为“执行器”在接收到调度中心的调度请求时,将会调用“IJobHandler”的execute方法,执行任务逻辑。 + +#### 5.5.2 "GLUE模式(Java)" 任务 +开发步骤:可参考 "章节三" ; +原理:每个 "GLUE模式(Java)" 任务的代码,实际上是“一个继承自“IJobHandler”的实现类的类代码”,“执行器”接收到“调度中心”的调度请求时,会通过Groovy类加载器加载此代码,实例化成Java对象,同时注入此代码中声明的Spring服务(请确保Glue代码中的服务和类引用在“执行器”项目中存在),然后调用该对象的execute方法,执行任务逻辑。 + +#### 5.5.3 GLUE模式(Shell) + GLUE模式(Python) + GLUE模式(PHP) + GLUE模式(NodeJS) + GLUE模式(Powershell) +开发步骤:可参考 "章节三" ; +原理:脚本任务的源码托管在调度中心,脚本逻辑在执行器运行。当触发脚本任务时,执行器会加载脚本源码在执行器机器上生成一份脚本文件,然后通过Java代码调用该脚本;并且实时将脚本输出日志写到任务日志文件中,从而在调度中心可以实时监控脚本运行情况; + +目前支持的脚本类型如下: + + - shell脚本:任务运行模式选择为 "GLUE模式(Shell)"时支持 "Shell" 脚本任务; + - python脚本:任务运行模式选择为 "GLUE模式(Python)"时支持 "Python" 脚本任务; + - php脚本:任务运行模式选择为 "GLUE模式(PHP)"时支持 "PHP" 脚本任务; + - nodejs脚本:任务运行模式选择为 "GLUE模式(NodeJS)"时支持 "NodeJS" 脚本任务; + - powershell:任务运行模式选择为 "GLUE模式(PowerShell)"时支持 "PowerShell" 脚本任务; + +脚本任务通过 Exit Code 判断任务执行结果,状态码可参考章节 "5.15 任务执行结果说明"; + +#### 5.5.4 执行器 +执行器实际上是一个内嵌的Server,默认端口9999(配置项:xxl.job.executor.port)。 + +在项目启动时,执行器会通过“@JobHandler”识别Spring容器中“Bean模式任务”,以注解的value属性为key管理起来。 + +“执行器”接收到“调度中心”的调度请求时,如果任务类型为“Bean模式”,将会匹配Spring容器中的“Bean模式任务”,然后调用其execute方法,执行任务逻辑。如果任务类型为“GLUE模式”,将会加载GLue代码,实例化Java对象,注入依赖的Spring服务(注意:Glue代码中注入的Spring服务,必须存在与该“执行器”项目的Spring容器中),然后调用execute方法,执行任务逻辑。 + +#### 5.5.5 任务日志 +XXL-JOB会为每次调度请求生成一个单独的日志文件,需要通过 "XxlJobHelper.log" 打印执行日志,“调度中心”查看执行日志时将会加载对应的日志文件。 + +(历史版本通过重写LOG4J的Appender实现,存在依赖限制,该方式在新版本已经被抛弃) + +日志文件存放的位置可在“执行器”配置文件进行自定义,默认目录格式为:/data/applogs/xxl-job/jobhandler/“格式化日期”/“数据库调度日志记录的主键ID.log”。 + +在JobHandler中开启子线程时,子线程将会把日志打印在父线程即JobHandler的执行日志中,方便日志追踪。 + +### 5.6 通讯模块剖析 + +#### 5.6.1 一次完整的任务调度通讯流程 + - 1、“调度中心”向“执行器”发送http调度请求: “执行器”中接收请求的服务,实际上是一台内嵌Server,默认端口9999; + - 2、“执行器”执行任务逻辑; + - 3、“执行器”http回调“调度中心”调度结果: “调度中心”中接收回调的服务,是针对执行器开放一套API服务; + +#### 5.6.2 通讯数据加密 +调度中心向执行器发送的调度请求时使用RequestModel和ResponseModel两个对象封装调度请求参数和响应数据, 在进行通讯之前底层会将上述两个对象对象序列化,并进行数据协议以及时间戳检验,从而达到数据加密的功能; + +### 5.7 任务注册, 任务自动发现 +自v1.5版本之后, 任务取消了"任务执行机器"属性, 改为通过任务注册和自动发现的方式, 动态获取远程执行器地址并执行。 + + AppName: 每个执行器机器集群的唯一标示, 任务注册以 "执行器" 为最小粒度进行注册; 每个任务通过其绑定的执行器可感知对应的执行器机器列表; + 注册表: 见"xxl_job_registry"表, "执行器" 在进行任务注册时将会周期性维护一条注册记录,即机器地址和AppName的绑定关系; "调度中心" 从而可以动态感知每个AppName在线的机器列表; + 执行器注册: 任务注册Beat周期默认30s; 执行器以一倍Beat进行执行器注册, 调度中心以一倍Beat进行动态任务发现; 注册信息的失效时间为三倍Beat; + 执行器注册摘除:执行器销毁时,将会主动上报调度中心并摘除对应的执行器机器信息,提高心跳注册的实时性; + + +为保证系统"轻量级"并且降低学习部署成本,没有采用Zookeeper作为注册中心,采用DB方式进行任务注册发现; + +### 5.8 任务执行结果 +自v1.6.2之后,任务执行结果通过 "IJobHandler" 的返回值 "ReturnT" 进行判断; +当返回值符合 "ReturnT.code == ReturnT.SUCCESS_CODE" 时表示任务执行成功,否则表示任务执行失败,而且可以通过 "ReturnT.msg" 回调错误信息给调度中心; +从而,在任务逻辑中可以方便的控制任务执行结果; + +### 5.9 分片广播 & 动态分片 +执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务; + +"分片广播" 以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。 + +"分片广播" 和普通任务开发流程一致,不同之处在于可以获取分片参数,获取分片参数进行分片业务处理。 + +- Java语言任务获取分片参数方式:BEAN、GLUE模式(Java) +``` +// 可参考Sample示例执行器中的示例任务"ShardingJobHandler"了解试用 +int shardIndex = XxlJobHelper.getShardIndex(); +int shardTotal = XxlJobHelper.getShardTotal(); +``` +- 脚本语言任务获取分片参数方式:GLUE模式(Shell)、GLUE模式(Python)、GLUE模式(Nodejs) +``` +// 脚本任务入参固定为三个,依次为:任务传参、分片序号、分片总数。以Shell模式任务为例,获取分片参数代码如下 +echo "分片序号 index = $2" +echo "分片总数 total = $3" +``` + +分片参数属性说明: + + index:当前分片序号(从0开始),执行器集群列表中当前执行器的序号; + total:总分片数,执行器集群的总机器数量; + +该特性适用场景如: +- 1、分片任务场景:10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍; +- 2、广播任务场景:广播执行器机器运行shell脚本、广播集群节点进行缓存更新等 + +### 5.10 访问令牌(AccessToken) +为提升系统安全性,调度中心和执行器进行安全性校验,双方AccessToken匹配才允许通讯; + +调度中心和执行器,可通过配置项 "xxl.job.accessToken" 进行AccessToken的设置。 + +调度中心和执行器,如果需要正常通讯,只有两种设置; + +- 设置一:调度中心和执行器,均不设置AccessToken;关闭安全性校验; +- 设置二:调度中心和执行器,设置了相同的AccessToken; + +### 5.11 故障转移 & 失败重试 +一次完整任务流程包括"调度(调度中心) + 执行(执行器)"两个阶段。 + +- "故障转移"发生在调度阶段,在执行器集群部署时,如果某一台执行器发生故障,该策略支持自动进行Failover切换到一台正常的执行器机器并且完成调度请求流程。 +- "失败重试"发生在"调度 + 执行"两个阶段,支持通过自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试; + +### 5.12 执行器灰度上线 +调度中心与业务解耦,只需部署一次后常年不需要维护。但是,执行器中托管运行着业务作业,作业上线和变更需要重启执行器,尤其是Bean模式任务。 +执行器重启可能会中断运行中的任务。但是,XXL-JOB得益于自建执行器与自建注册中心,可以通过灰度上线的方式,避免因重启导致的任务中断的问题。 + +步骤如下: +- 1、执行器改为手动注册,下线一半机器列表(A组),线上运行另一半机器列表(B组); +- 2、等待A组机器任务运行结束并编译上线;执行器注册地址替换为A组; +- 3、等待B组机器任务运行结束并编译上线;执行器注册地址替换为A组+B组; +操作结束; + +### 5.13 任务执行结果说明 +系统根据以下标准判断任务执行结果,可参考之。 + +-- | Bean/Glue(Java) | Glue(Shell) 等脚本任务 +--- | --- | --- +成功 | IJobHandler.SUCCESS | 0 +失败 | IJobHandler.FAIL | -1(非0状态码) + +### 5.14 任务超时控制 +支持设置任务超时时间,任务运行超时的情况下,将会主动中断任务; + +需要注意的是,任务超时中断时与任务终止机制(可查看“4.9 终止运行中的任务”)类似,也是通过 "interrupt" 中断任务,因此业务代码需要将 "InterruptedException" 外抛,否则功能不可用。 + +### 5.15 跨语言 +XXL-JOB是一个跨语言的任务调度平台,主要体现在如下几个方面: +- 1、RESTful API:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。(可参考章节 “调度中心/执行器 RESTful API” ) +- 2、多任务模式:提供Java、Python、PHP……等十来种任务模式,可参考章节 “5.5 任务 "运行模式" ”;理论上可扩展任意语言任务模式; +- 2、提供基于HTTP的任务Handler(Bean任务,JobHandler="httpJobHandler");业务方只需要提供HTTP链接等相关信息即可,不限制语言、平台;(可参考章节 “原生内置Bean模式任务” ) + +### 5.16 任务失败告警 +默认提供邮件失败告警,可扩展短信、钉钉等方式。如果需要新增一种告警方式,只需要新增一个实现 "com.xxl.job.admin.core.alarm.JobAlarm" 接口的告警实现即可。可以参考默认提供邮箱告警实现 "EmailJobAlarm"。 + +### 5.17 调度中心Docker镜像构建 +可以通过以下命令快速构建调度中心,并启动运行; +``` +mvn clean package +docker build -t xuxueli/xxl-job-admin ./xxl-job-admin +docker run --name xxl-job-admin -p 8080:8080 -d xuxueli/xxl-job-admin +``` + +### 5.20 避免任务重复执行 +调度密集或者耗时任务可能会导致任务阻塞,集群情况下调度组件小概率情况下会重复触发; +针对上述情况,可以通过结合 "单机路由策略(如:第一台、一致性哈希)" + "阻塞策略(如:单机串行、丢弃后续调度)" 来规避,最终避免任务重复执行。 + +### 5.21 命令行任务 +原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可; +如任务参数 "pwd" 将会执行命令并输出数据; + +### 5.22 日志自动清理 +XXL-JOB日志主要包含如下两部分,均支持日志自动清理,说明如下: +- 调度中心日志表数据:可借助配置项 "xxl.job.logretentiondays" 设置日志表数据保存天数,过期日志自动清理;详情可查看上文配置说明; +- 执行器日志文件数据:可借助配置项 "xxl.job.executor.logretentiondays" 设置日志文件数据保存天数,过期日志自动清理;详情可查看上文配置说明; + +### 5.23 调度结果丢失处理 +执行器因网络抖动回调失败或宕机等异常情况,会导致任务调度结果丢失。由于调度中心依赖执行器回调来感知调度结果,因此会导致调度日志永远处于 "运行中" 状态。 + +针对该问题,调度中心提供内置组件进行处理,逻辑为:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败; + + +## 六、调度中心/执行器 RESTful API +XXL-JOB 目标是一种跨平台、跨语言的任务调度规范和协议。 + +针对Java应用,可以直接通过官方提供的调度中心与执行器,方便快速的接入和使用调度中心,可以参考上文 “快速入门” 章节。 + +针对非Java应用,可借助 XXL-JOB 的标准 RESTful API 方便的实现多语言支持。 + +- 调度中心 RESTful API: + - 说明:调度中心提供给执行器使用的API;不局限于官方执行器使用,第三方可使用该API来实现执行器; + - API列表:执行器注册、任务结果回调等; +- 执行器 RESTful API : + - 说明:执行器提供给调度中心使用的API;官方执行器默认已实现,第三方执行器需要实现并对接提供给调度中心; + - API列表:任务触发、任务终止、任务日志查询……等; + +此处 RESTful API 主要用于非Java语言定制个性化执行器使用,实现跨语言。除此之外,如果有需要通过API操作调度中心,可以个性化扩展 “调度中心 RESTful API” 并使用。 + +### 6.1 调度中心 RESTful API + +API服务位置:com.xxl.job.core.biz.AdminBiz ( com.xxl.job.admin.controller.JobApiController ) +API服务请求参考代码:com.xxl.job.adminbiz.AdminBizTest + +#### a、任务回调 +``` +说明:执行器执行完任务后,回调任务结果时使用 + +------ + +地址格式:{调度中心根地址}/api/callback + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + [{ + "logId":1, // 本次调度日志ID + "logDateTim":0, // 本次调度日志时间 + "handleCode":200, // 200 表示任务执行正常,500表示失败 + "handleMsg": null + } + }] + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### b、执行器注册 +``` +说明:执行器注册时使用,调度中心会实时感知注册成功的执行器并发起任务调度 + +------ + +地址格式:{调度中心根地址}/api/registry + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "registryGroup":"EXECUTOR", // 固定值 + "registryKey":"xxl-job-executor-example", // 执行器AppName + "registryValue":"http://127.0.0.1:9999/" // 执行器地址,内置服务跟地址 + } + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### c、执行器注册摘除 +``` +说明:执行器注册摘除时使用,注册摘除后的执行器不参与任务调度与执行 + +------ + +地址格式:{调度中心根地址}/api/registryRemove + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "registryGroup":"EXECUTOR", // 固定值 + "registryKey":"xxl-job-executor-example", // 执行器AppName + "registryValue":"http://127.0.0.1:9999/" // 执行器地址,内置服务跟地址 + } + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +### 6.2 执行器 RESTful API + +API服务位置:com.xxl.job.core.biz.ExecutorBiz +API服务请求参考代码:com.xxl.job.executorbiz.ExecutorBizTest + +#### a、心跳检测 +``` +说明:调度中心检测执行器是否在线时使用 + +------ + +地址格式:{执行器内嵌服务根地址}/beat + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### b、忙碌检测 +``` +说明:调度中心检测指定执行器上指定任务是否忙碌(运行中)时使用 + +------ + +地址格式:{执行器内嵌服务根地址}/idleBeat + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "jobId":1 // 任务ID + } + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### c、触发任务 +``` +说明:触发任务执行 + +------ + +地址格式:{执行器内嵌服务根地址}/run + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "jobId":1, // 任务ID + "executorHandler":"demoJobHandler", // 任务标识 + "executorParams":"demoJobHandler", // 任务参数 + "executorBlockStrategy":"COVER_EARLY", // 任务阻塞策略,可选值参考 com.xxl.job.core.enums.ExecutorBlockStrategyEnum + "executorTimeout":0, // 任务超时时间,单位秒,大于零时生效 + "logId":1, // 本次调度日志ID + "logDateTime":1586629003729, // 本次调度日志时间 + "glueType":"BEAN", // 任务模式,可选值参考 com.xxl.job.core.glue.GlueTypeEnum + "glueSource":"xxx", // GLUE脚本代码 + "glueUpdatetime":1586629003727, // GLUE脚本更新时间,用于判定脚本是否变更以及是否需要刷新 + "broadcastIndex":0, // 分片参数:当前分片 + "broadcastTotal":0 // 分片参数:总分片 + } + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### f、终止任务 +``` +说明:终止任务 + +------ + +地址格式:{执行器内嵌服务根地址}/kill + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "jobId":1 // 任务ID + } + + +响应数据格式: + { + "code": 200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + } +``` + +#### d、查看执行日志 +``` +说明:终止任务,滚动方式加载 + +------ + +地址格式:{执行器内嵌服务根地址}/log + +Header: + XXL-JOB-ACCESS-TOKEN : {请求令牌} + +请求数据格式如下,放置在 RequestBody 中,JSON格式: + { + "logDateTim":0, // 本次调度日志时间 + "logId":0, // 本次调度日志ID + "fromLineNum":0 // 日志开始行号,滚动加载日志 + } + +响应数据格式: + { + "code":200, // 200 表示正常、其他失败 + "msg": null // 错误提示消息 + "content":{ + "fromLineNum":0, // 本次请求,日志开始行数 + "toLineNum":100, // 本次请求,日志结束行号 + "logContent":"xxx", // 本次请求日志内容 + "isEnd":true // 日志是否全部加载完 + } + } +``` + + + +## 七、版本更新日志 +### 7.1 版本 V1.1.x,新特性[2015-12-05] +**【于V1.1.x版本,XXL-JOB正式应用于我司,内部定制别名为 “Ferrari”,新接入应用推荐使用最新版本】** +- 1、简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手; +- 2、动态:支持动态修改任务状态,动态暂停/恢复任务,即时生效; +- 3、服务HA:任务信息持久化到mysql中,Job服务天然支持集群,保证服务HA; +- 4、任务HA:某台Job服务挂掉,任务会平滑分配给其他的某一台存活服务,即使所有服务挂掉,重启时或补偿执行丢失任务; +- 5、一个任务只会在其中一台服务器上执行; +- 6、任务串行执行; +- 7、支持自定义参数; +- 8、支持远程任务执行终止; + +### 7.2 版本 V1.2.x,新特性[2016-01-17] +- 1、支持任务分组; +- 2、支持“本地任务”、“远程任务”; +- 3、底层通讯支持两种方式,Servlet方式 + JETTY方式; +- 4、支持“任务日志”; +- 5、支持“串行执行”,并行执行; + + 说明:V1.2版本将系统架构按功能拆分为: + + - 调度模块(调度中心):负责管理调度信息,按照调度配置发出调度请求; + - 执行模块(执行器):负责接收调度请求并执行任务逻辑; + - 通讯模块:负责调度模块和任务模块之间的信息通讯; + 优点: + + - 解耦:任务模块提供任务接口,调度模块维护调度信息,业务相互独立; + - 高扩展性; + - 稳定性; + +### 7.3 版本 V1.3.0,新特性[2016-05-19] +- 1、遗弃“本地任务”模式,推荐使用“远程任务”,易于系统解耦,任务对应的JobHandler统称为“执行器”; +- 2、遗弃“servlet”方式底层系统通讯,推荐使用JETTY方式,调度+回调双向通讯,重构通讯逻辑; +- 3、UI交互优化:左侧菜单展开状态优化,菜单项选中状态优化,任务列表打开表格有压缩优化; +- 4、【重要】“执行器”细分为:BEAN、GLUE两种开发模式,简介见下文: + + “执行器” 模式简介: + - BEAN模式执行器:每个执行器都是Spring的一个Bean实例,XXL-JOB通过注解@JobHandler识别和调度执行器; + -GLUE模式执行器:每个执行器对应一段代码,在线Web编辑和维护,动态编译生效,执行器负责加载GLUE代码和执行; + +### 7.4 版本 V1.3.1,新特性[2016-05-23] +- 1、更新项目目录结构: + - /xxl-job-admin -------------------- 【调度中心】:负责管理调度信息,按照调度配置发出调度请求; + - /xxl-job-core ----------------------- 公共依赖 + - /xxl-job-executor-example ------ 【执行器】:负责接收调度请求并执行任务逻辑; + - /db ---------------------------------- 建表脚本 + - /doc --------------------------------- 用户手册 +- 2、在新的目录结构上,升级了用户手册; +- 3、优化了一些交互和UI; + +### 7.5 版本 V1.3.2,新特性[2016-05-28] +- 1、调度逻辑进行事务包裹; +- 2、执行器异步回调执行日志; +- 3、【重要】在 “调度中心” 支持HA的基础上,扩展执行器的Failover支持,支持配置多执行期地址; + +### 7.6 版本 V1.4.0 新特性[2016-07-24] +- 1、任务依赖: 通过事件触发方式实现, 任务执行成功并回调时会主动触发一次子任务的调度, 多个子任务用逗号分隔; +- 2、执行器底层实现代码进行重度重构, 优化底层建表脚本; +- 3、执行器中任务线程分组逻辑优化: 之前根据执行器JobHandler进行线程分组,当多个任务复用Jobhanlder会导致相互阻塞。现改为根据调度中心任务进行任务线程分组,任务与任务执行相互隔离; +- 4、执行器调度通讯方案优化, 通过Hex + HC实现建议RPC通讯协议, 优化了通讯参数的维护和解析流程; +- 5、调度中心, 新建/编辑任务, 界面属性调整: + - 5.1、任务新增/编辑界面中去除 "任务名JobName"属性 ,该属性改为系统自动生成: 该字段之前主要用于在 "调度中心" 唯一标示一个任务, 现实意义不大, 因此计划淡化掉该字段,改为系统生成UUID,从而简化任务新建的操作; + - 5.2、任务新增/编辑界面中去除 "GLUE模式" 复选框位置调整, 改为贴近"JobHandler"输入框右侧; + - 5.3、任务新增/编辑界面中去除 "报警阈值" 属性; + - 5.4、任务新增/编辑界面中去除 "子任务Key" 属性, 每个任务全局任务Key可以从任务列表获取, 当本任务执行结束且成功后, 将会根据子任务Key匹配子任务并主动触发一次子任务执行; +- 6、问题修复: + - 6.1、执行器jetty关闭优化,解决一处可能导致jetty无法关闭的问题; + - 6.2、执行器任务终止时,执行队列回调优化,解决一处导致任务无法回调的问题; + - 6.3、调度中心中列表分页参数优化,解决一处因服务器限制post长度而引起的问题; + - 6.4、执行器Jobhandler注解优化,解决一处因事务代理导致的容器无法加载JobHandler的问题; + - 6.5、远程调度优化,禁用retry策略,解决一处可能导致重复调用的问题; + +Tips: 历史版本(V1.3.x)目前已经Release至稳定版本, 进入维护阶段, 地址见分支 [V1.3](https://github.com/xuxueli/xxl-job/tree/v1.3) 。新特性将会在master分支持续更新。 + +### 7.7 版本 V1.4.1 新特性[2016-09-06] +- 1、项目成功推送maven中央仓库, 中央仓库地址以及依赖如下: + ``` + + + com.xuxueli + xxl-job-core + ${最新稳定版} + + ``` +- 2、为适配中央仓库规则, 项目groupId从com.xxl改为com.xuxueli。 +- 3、系统版本不在维护在项目跟pom中,各个子模块单独配置版本配置,解决子模块无法单独编译的问题; +- 4、底层RPC通讯,传输数据的字节长度统计规则优化,可节省50%数据传输量; +- 5、IJobHandler取消任务返回值,原通过返回值判断执行状态,逻辑改为:默认任务执行成功,仅在捕获异常时认定任务执行失败。 +- 6、系统公共弹框功能,插件化; +- 7、底层表结构,表明统一大写; +- 8、调度中心,异常处理器JSON响应的ContentType修改,修复浏览器不识别的问题; + +### 7.8 版本 V1.4.2 新特性[2016-09-29] +- 1、推送新版本 V1.4.2 至中央仓库, 大版本 V1.4 进入维护阶段; +- 2、任务新增时,任务列表偏移问题修复; +- 3、修复一处因bootstrap不支持模态框重叠而导致的样式错乱的问题, 在任务编辑时会出现该问题; +- 4、调度超时和Handler匹配不到时,调度状态优化; +- 5、因catch异常,导致任务不可终止的问题,给出解决方案, 见文档; + +### 7.9 版本 V1.5.0 特性[2016-11-13] +- 1、任务注册: 执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行。 +- 2、"执行器" 新增参数 "AppName" : 是每个执行器集群的唯一标示AppName, 并周期性以AppName为对象进行自动注册。 +- 3、调度中心新增栏目 "执行器管理" : 管理在线的执行器, 通过属性AppName自动发现注册的执行器。只有被管理的执行器才允许被使用; +- 4、"任务组"属性改为"执行器": 每个任务需要绑定指定的执行器, 调度地址通过绑定的执行器获取; +- 5、抛弃"任务机器"属性: 通过任务绑定的执行器, 自动发现注册的远程执行器地址并触发调度请求。 +- 6、"公共依赖"中新增DBGlueLoader,基于原生jdbc实现GLUE源码的加载器,减少第三方依赖(mybatis,spring-orm等);精简和优化执行器测配置(针对GLUE任务),降低上手难度; +- 7、表结构调整,底层重构优化; +- 8、"调度中心"自动注册和发现,failover: 调度中心周期性自动注册, 任务回调时可以感知在线的所有调度中心地址, 通过failover的方式进行任务回调,避免回调单点风险。 + +### 7.10 版本 V1.5.1 特性[2016-11-13] +- 1、底层代码重构和逻辑优化,POM清理以及CleanCode; +- 2、Servlet/JSP Spec设定为3.0/2.2 +- 3、Spring升级至3.2.17.RELEASE版本; +- 4、Jetty升级版本至8.2.0.v20160908; +- 5、已推送V1.5.0和V1.5.1至Maven中央仓库; + +### 7.11 版本 V1.5.2 特性[2017-02-28] +- 1、IP工具类获取IP逻辑优化,IP静态缓存; +- 2、执行器、调度中心,均支持自定义注册IP地址;解决机器多网卡时错误网卡注册的情况; +- 3、任务跨天执行时生成多份日志文件的问题修复; +- 4、底层日志底层日志调整,非敏感日志level调整为debug; +- 5、升级数据库连接池c3p0版本; +- 6、执行器log4j配置优化,去除无效属性; +- 7、底层代码重构和逻辑优化以及CleanCode; +- 8、GLUE依赖注入逻辑优化,支持别名注入; + +### 7.12 版本 V1.6.0 特性[2017-03-13] +- 1、通讯方案升级,原基于HEX的通讯模型调整为基于HTTP的B-RPC的通讯模型; +- 2、执行器支持手动设置执行地址列表,提供开关切换使用注册地址还是手动设置的地址; +- 3、执行器路由规则:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移; +- 4、规范线程模型统一,统一线程销毁方案(通过listener或stop方法,容器销毁时销毁线程;Daemon方式有时不太理想); +- 5、规范系统配置数据,通过配置文件统一管理; +- 6、CleanCode,清理无效的历史参数; +- 7、底层扩展数据结构以及相关表结构调整; +- 8、新建任务默认为非运行状态; +- 9、GLUE模式任务实例更新逻辑优化,原根据超时时间更新改为根据版本号更新,源码变动版本号加一; + +### 7.13 版本 V1.6.1 特性[2017-03-25] +- 1、Rolling日志; +- 2、WebIDE交互重构; +- 3、通讯增强校验,有效过滤非正常请求; +- 4、权限增强校验,采用动态登录TOKEN(推荐接入内部SSO); +- 5、数据库配置优化,解决乱码问题; + +### 7.14 版本 V1.6.2 特性[2017-04-25] +- 1、运行报表:支持实时查看运行数据,如任务数量、调度次数、执行器数量等;以及调度报表,如调度日期分布图,调度成功分布图等; +- 2、JobHandler支持设置任务返回值,在任务逻辑中可以方便的控制任务执行结果; +- 3、资源路径包含空格或中文时资源文件无法加载时,无法准确查看异常信息的问题处理。 +- 4、路由策越优化:循环和LFU路由策略计数器自增无上限问题和首次路由压力集中在首台机器的问题修复; + +### 7.15 版本 V1.7.0 特性[2017-05-02] +- 1、脚本任务:支持以GLUE模式开发和运行脚本任务,包括Shell、Python和Groovy等类型脚本; +- 2、新增spring-boot类型执行器example项目; +- 3、升级jetty版本至9.2; +- 4、任务运行日志移除log4j组件依赖,改为底层自主实现,从而取消了对日志组件的依赖限制; +- 5、执行器移除GlueLoader依赖,改为推送方式实现,从而GLUE源码加载不再依赖JDBC; +- 6、登录拦截Redirect时获取项目名,解决非根据目录发布时跳转404问题; + +### 7.16 版本 V1.7.1 特性[2017-05-08] +- 1、运行日志读写编码统一为UTF-8,解决windows环境下日志乱码问题; +- 2、通讯超时时间限定为10s,避免异常情况下调度线程占用; +- 3、执行器,server启动、销毁和注册逻辑调整; +- 4、JettyServer关闭逻辑优化,修复执行器无法正常关闭导致端口占用和频繁打印c3p0日志的问题; +- 5、JobHandler中开启子线程时,支持子线程输出执行日志并通过Rolling查看。 +- 6、任务日志清理功能; +- 7、弹框组件统一替换为layer; +- 8、升级quartz版本至2.3.0; + +### 7.17 版本 V1.7.2 特性[2017-05-17] +- 1、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度; +- 2、失败处理策略;调度失败时的处理策略,策略包括:失败告警(默认)、失败重试; +- 3、通讯时间戳超时时间调整为180s; +- 4、执行器与数据库彻底解耦,但是执行器需要配置调度中心集群地址。调度中心提供API供执行器回调和心跳注册服务,取消调度中心内部jetty,心跳周期调整为30s,心跳失效为三倍心跳; +- 5、执行参数编辑时丢失问题修复; +- 6、新增任务测试Demo,方便在开发时进行任务逻辑测试; + +### 7.18 版本 V1.8.0 特性[2017-07-17] +- 1、任务Cron更新逻辑优化,改为rescheduleJob,同时防止cron重复设置; +- 2、API回调服务失败状态码优化,方便问题排查; +- 3、XxlJobLogger的日志多参数支持; +- 4、路由策略新增 "忙碌转移" 模式:按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度; +- 5、路由策略代码重构; +- 6、执行器重复注册问题修复; +- 7、任务线程轮空30次后自动销毁,降低低频任务的无效线程消耗。 +- 8、执行器任务执行结果批量回调,降低回调频率提升执行器性能; +- 9、springboot版本执行器,取消XML配置,改为类配置方式; +- 10、执行日志,支持根据运行 "状态" 筛选日志; +- 11、调度中心任务注册检测逻辑优化; + +### 7.19 版本 V1.8.1 特性[2017-07-30] +- 1、分片广播任务:执行器集群部署时,任务路由策略选择"分片广播"情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数处理分片任务; +- 2、动态分片:分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。 +- 3、执行器JobHandler禁止命名冲突; +- 4、执行器集群地址列表进行自然排序; +- 5、调度中心,DAO层代码精简优化并且新增测试用例覆盖; +- 6、调度中心API服务改为自研RPC形式,统一底层通讯模型; +- 7、新增调度中心API服务测试Demo,方便在调度中心API扩展和测试; +- 8、任务列表页交互优化,更换执行器分组时自动刷新任务列表,新建任务时默认定位在当前执行器位置; +- 9、访问令牌(accessToken):为提升系统安全性,调度中心和执行器进行安全性校验,双方AccessToken匹配才允许通讯; +- 10、springboot版本执行器,升级至1.5.6.RELEASE版本; +- 11、统一maven依赖版本管理; + +### 7.20 版本 V1.8.2 特性[2017-09-04] +- 1、项目主页搭建:提供中英文文档:https://www.xuxueli.com/xxl-job +- 2、JFinal执行器Sample示例项目; +- 3、事件触发:除了"Cron方式"和"任务依赖方式"触发任务执行之外,支持基于事件的触发任务方式。调度中心提供触发任务单次执行的API服务,可根据业务事件灵活触发。 +- 4、执行器摘除:执行器销毁时,主动通知调度中心并摘除对应执行器节点,提高执行器状态感知的时效性。 +- 5、执行器手动设置IP时将会绑定Host; +- 6、规范项目目录,方便扩展多执行器; +- 7、解决执行器回调URL不支持配置HTTPS时问题; +- 8、执行器回调线程销毁前, 批量回调队列中数据,防止任务结果丢失; +- 9、调度中心任务监控线程销毁时,批量对失败任务告警,防止告警信息丢失; +- 10、任务日志文件路径时间戳格式化时SimpleDateFormat并发问题解决; + +### 7.21 版本 V1.9.0 特性[2017-12-29] +- 1、新增Nutz执行器Sample示例项目; +- 2、新增任务运行模式 "GLUE模式(NodeJS) ",支持NodeJS脚本任务; +- 3、脚本任务Shell、Python和Nodejs等支持获取分片参数; +- 4、失败重试,完整支持:调度中心调度失败且启用"失败重试"策略时,将会自动重试一次;执行器执行失败且回调失败重试状态(新增失败重试状态返回值)时,也将会自动重试一次; +- 5、失败告警策略扩展:默认提供邮件失败告警,可扩展短信等,扩展代码位置为 "JobFailMonitorHelper.failAlarm"; +- 6、执行器端口支持自动生成(小于等于0时),避免端口定义冲突; +- 7、调度报表优化,支持时间区间筛选; +- 8、Log组件支持输出异常栈信息,底层实现优化; +- 9、告警邮件样式优化,调整为表格形式,邮件组件调整为commons-email简化邮件操作; +- 10、项目依赖全量升级至较新稳定版本,如spring、jackson等等; +- 11、任务日志,记录发起调度的机器信息; +- 12、交互优化,如登录注销; +- 13、任务Cron长度扩展支持至128位,支持负责类型Cron设置; +- 14、执行器地址录入交互优化,地址长度扩展支持至512位,支持大规模执行器集群配置; +- 15、任务参数“IJobHandler.execute”入参改为“String params”,增强入参通用性。 +- 16、IJobHandler提供init/destroy方法,支持在相应任务线程初始化和销毁时进行附加操作; +- 17、任务注解调整为 “@JobHandler”,与任务抽象接口统一; +- 18、修复任务监控线程被耗时任务阻塞的问题; +- 19、修复任务监控线程无法监控任务触发和执行状态均未0的问题; +- 20、执行器动态代理对象,拦截非业务方法的执行; +- 21、修复JobThread捕获Error错误不更新JobLog的问题; +- 22、修复任务列表界面左侧菜单合并时样式错乱问题; +- 23、调度中心项目日志配置改为xml文件格式; +- 24、Log地址格式兼容,支持非"/"结尾路径配置; +- 25、底层系统日志级别规范调整,清理遗留代码; +- 26、建表SQL优化,支持同步创建制定编码的库和表; +- 27、系统安全性优化,登录Token写Cookie时进行MD5加密,同时Cookie启用HttpOnly; +- 28、新增"任务ID"属性,移除"JobKey"属性,前者承担所有功能,方便后续增强任务依赖功能。 +- 29、任务循环依赖问题修复,避免子任务与父任务重复导致的调度死循环; +- 30、任务列表新增筛选条件 "任务描述",快速检索任务; +- 31、执行器Log文件定期清理功能:执行器新增配置项("xxl.job.executor.logretentiondays")日志保存天数,日志文件过期自动删除。 + +### 7.22 版本 V1.9.1 特性[2018-02-22] +- 1、国际化:调度中心实现国际化,支持中文、英文两种语言,默认为中文。 +- 2、调度报表新增"运行中"中状态项; +- 3、调度报表优化,报表SQL调优并且新增LocalCache缓存(缓存时间60s),提高大数据量下报表加载速度; +- 4、修复打包部署时资源文件乱码问题; +- 5、修复新版本chrome滚动到顶部失效问题; +- 6、调度中心配置加载优化,取消对配置文件名的强依赖,支持加载磁盘配置; +- 7、修复脚本任务Log文件未正常close的问题; +- 8、项目依赖全量升级至较新稳定版本,如spring、jackson等等; + +### 7.23 版本 V1.9.2 特性[2018-10-05] +- 1、任务超时控制:新增任务属性 "任务超时时间",并支持自定义,任务运行超时将会主动中断任务; +- 2、任务失败重试次数:新增任务属性 "失败重试次数",并支持自定义,当任务失败时将会按照预设的失败重试次数主动进行重试;同时收敛废弃其他失败重试策略,如调度失败、执行失败、状态码失败等; +- 3、新增任务运行模式 "GLUE模式(PHP) ",支持php脚本任务; +- 4、新增任务运行模式 "GLUE模式(PowerShell) ",支持PowerShell脚本任务; +- 5、调度全异步处理:任务触发之后,推送到调度队列,多线程并发处理调度请求,提高任务调度速率的同时,避免因网络问题导致quartz调度线程阻塞的问题; +- 6、执行器任务结果落盘优化:执行器回调失败时将任务结果写磁盘,待重启或网络恢复时重试回调任务结果,防止任务执行结果丢失; +- 7、任务日志查询速度大幅提升:百万级别数据量搜索速度提升1000倍; +- 8、调度中心提供API服务,支持通过API服务对任务进行查询、新增、更新、启停等操作; +- 9、底层自研Log组件参数占位符改为"{}",并修复打印有参日志时参数不匹配导致报错的问题; +- 10、任务回调结果优化,支持展示在Rolling log中,方便问题排查; +- 11、底层LocalCache组件兼容性优化,支持jdk9、jdk10及以上版本编译部署; +- 12、告警邮件固定使用 UTF-8 编码格式,修复由机器编码导致的邮件乱码问题; +- 13、告警邮件中展示失败告警信息; +- 14、告警邮箱支持SSL配置; +- 15、Window机器下File.separator不兼容问题修复; +- 16、脚本任务异常Log输出优化; +- 17、任务线程停止变量修饰符优化; +- 18、脚本任务Log文件流关闭优化; +- 19、任务报表成功、失败和进行中统计问题修复; +- 20、核心依赖Core内部国际化处理; +- 21、默认Quartz线程数调整为50; +- 22、新增左侧菜单"运行报表"; +- 23、执行器手动设置IP时取消绑定Host的操作,该IP仅供执行器注册使用;修复指定外网IP时无法绑定执行器Host的问题; +- 24、取消父子任务不可重复的限制,支持循环任务触发等特殊场景; +- 25、任务调度备注中标注任务触发类型,如Cron触发、父任务触发、API触发等等,方便排查调度日志; +- 26、底层日志组件SimpleDateFormat线程安全问题修复; +- 27、执行器通讯线程优化,corePoolSize从256降低至32; +- 28、任务日志表状态字段类型优化; +- 29、GLUE脚本文件自动清理功能,及时清理过期脚本文件; +- 30、执行器注册方式切换优化,切换自动注册时主动同步在线机器,避免执行器为空的问题; +- 31、跨平台:除了提供Java、Python、PHP等十来种任务模式之外,新增提供基于HTTP的任务模式; +- 32、底层RPC序列化协议调整为hessian2; +- 33、修复表字段 “t.order”与数据库关键字冲突查询失败的问题, +- 34、任务属性枚举 "任务模式、阻塞策略" 国际化优化; +- 35、分片任务失败重试优化,仅重试当前失败的分片; +- 36、任务触发时支持动态传参,调度中心与API服务均提供提供动态参数功能; +- 37、任务执行日志、调度日志字段类型调整,改为text类型并取消字数限制; +- 38、GLUE任务脚本字段类型调整,改为mediumtext类型,提高GLUE长度上限; +- 39、任务监控线程Log输出优化,运行中任务的监控Log改为debug级别,减少非核心日志量; +- 40、项目依赖全量升级至较新稳定版本,如spring、Jackson、groovy等等; +- 41、docker支持:调度中心提供 Dockerfile 方便快速构建docker镜像; + +### 7.24 版本 V2.0.0 Release Notes[2018-11-04] +- 1、调度中心迁移到 springboot; +- 2、底层通讯组件迁移至 xxl-rpc; +- 3、容器化:提供官方docker镜像,并实时更新推送dockerhub(docker pull xuxueli/xxl-job-admin),进一步实现产品开箱即用; +- 4、新增无框架执行器Sample示例项目 "xxl-job-executor-sample-frameless"。不依赖第三方框架,只需main方法即可启动运行执行器; +- 5、命令行任务:原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可; +- 6、任务状态优化,仅运行状态"NORMAL"任务关联至quartz,降低quartz底层数据存储与调度压力; +- 7、任务状态规范:新增任务默认停止状态,任务更新时保持任务状态不变; +- 8、IP获取逻辑优化,优先遍历网卡来获取可用IP; +- 9、任务新增的API服务接口返回任务ID,方便调用方实用; +- 10、组件化优化,移除对 spring 的依赖:非spring应用选用 "XxlJobExecutor" 、spring应用选用 "XxlJobSpringExecutor" 作为执行器组件; +- 11、任务RollingLog展示逻辑优化,修复超时任务无法查看的问题; +- 12、多项UI组件升级到最新版本,如:CodeMirror、Echarts、Jquery 等; +- 13、项目依赖升级 groovy 至较新稳定版本;pom清理; +- 14、子任务失败重试重试逻辑优化,子任务失败时将会按照其预设的失败重试次数主动进行重试 + +### 7.25 版本 v2.0.1 Release Notes[2018-11-09] +- 1、左侧菜单折叠动画问题修复; +- 2、调度报表日期分布图默认值统一; +- 3、freemarker对数字默认加千分位问题修复,解决日志ID被分隔导致查看日志失败问题; +- 4、底层通讯组件升级,修复通讯异常时无效等待的问题; +- 5、执行器启动之后jetty停止的问题修复; + +### 7.26 版本 v2.0.2 Release Notes[2019-04-20] +- 1、底层通讯方案优化:升级较新版本xxl-rpc,由"JETTY"方案调整为"NETTY_HTTP"方案,执行器内嵌netty-http-server提供服务,调度中心复用容器端口提供服务; +- 2、任务告警逻辑调整,改为通过扫描失败日志方式触发。一方面精确扫描失败任务,降低扫描范围;另一方面取消内存队列,降低线程内存消耗; +- 3、Quartz触发线程池废弃并替换为 "XxlJobThreadPool",降低线程切换、内存占用带来的消耗,提高调度性能; +- 4、调度线程池隔离,拆分为"Fast"和"Slow"两个线程池,1分钟窗口期内任务耗时达500ms超过10次,该窗口期内判定为慢任务,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性; +- 5、执行器热部署时JobHandler重新初始化,修复由此导致的 "jobhandler naming conflicts." 问题; +- 6、新增Class的加载缓存,解决频繁加载Class会使jvm的方法区空间不足导致OOM的问题; +- 7、任务支持更换绑定执行器,方便任务分组转移和管理; +- 8、调度中心告警邮件发送组件改为 “spring-boot-starter-mail”; +- 9、记住密码功能优化,选中时永久记住;非选中时关闭浏览器即登出; +- 10、项目依赖升级至较新稳定版本,如quartz、spring、jackson、groovy、xxl-rpc等等; +- 11、精简项目,取消第三方依赖,如 commons-collections4、commons-lang3 ; +- 12、执行器回调日志落盘方案复用RPC序列化方案,并移除Jackson依赖; +- 13、底层Log调优,应用正常终止取消异常栈信息打印; +- 14、交互优化,尽量避免新开页面窗口;仅WebIDE支持新开页,并提供窗口快速关闭按钮;任务启、停、删除、触发等轻操作提示改为toast方式, +- 15、任务暂停、删除优化,避免quartz delete不完整导致任务脏数据; +- 16、任务回调、心跳注册成功日志优化,非核心常规日志调整为debug级别,降低冗余日志输出; +- 17、调整首页报表默认区间为本周,避免日志量太大查询缓慢; +- 18、LRU路由更新不及时问题修复; +- 19、任务失败告警邮件发送逻辑优化; +- 20、调度日志排序逻辑调整为按照调度时间倒序,兼容TIDB等主键不连续日志存储组件; +- 21、执行器优雅停机优化; +- 22、连接池配置优化,增强连接有效性验证; +- 23、JobHandler#msg长度限制,修复异常情况下日志超长导致内存溢出的问题; +- 24、升级xxl-rpc至较新版本,修复springboot 2.x版本兼容性问题; + +### 7.27 版本 v2.1.0 Release Notes[2019-07-07] +- 1、自研调度组件,移除quartz依赖:一方面是为了精简系统降低冗余依赖,另一方面是为了提供系统的可控度与稳定性; + - 触发:单节点周期性触发,运行事件如delayqueue; + - 调度:集群竞争,负载方式协同处理,锁竞争-更新触发信息-推送时间轮-锁释放-锁竞争; +- 2、底层表结构重构:移除11张quartz相关表,并对现有表结构优化梳理; +- 3、任务日志主键调整为long数据类型,防止海量日志情况下数据溢出; +- 4、底层线程模型重构:移除Quartz线程池,降低系统线程与内存开销; +- 5、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色; +- 6、权限管理:执行器维度进行权限控制,管理员拥有全量权限,普通用户需要分配执行器权限后才允许相关操作; +- 7、调度线程池参数调优; +- 8、注册表索引优化,缓解锁表问题; +- 9、新增Jboot执行器Sample示例项目; +- 10、任务列表优化,支持根据 "任务状态"、"负责人" 属性筛选任务; +- 11、任务日志列表交互优化,操作按钮合并为分割按钮; +- 12、项目依赖升级至较新稳定版本,如spring、springboot、groovy、xxl-rpc等等;并清理冗余POM; +- 13、升级xxl-rpc至较新版本,修复代理服务初始化时远程服务不可用导致长连冗余创建的问题; +- 14、首页调度报表的日期排序在TIDB下乱序问题修复; +- 15、调度中心与执行器双向通讯超时时间调整为3s; +- 16、调度组件销毁流程优化,先停止调度线程,然后等待时间轮内存量任务处理完成,最终销毁时间轮线程; +- 17、执行器回调线程优化,回调地址为空时销毁问题修复; +- 18、HttpJobHandler优化,响应数据指定UTF-8格式,避免中文乱码; +- 19、代码优化,ConcurrentHashMap变量类型改为ConcurrentMap,避免因不同版本实现不同导致的兼容性问题; + +### 7.28 版本 v2.1.1 Release Notes[2019-11-24] +- 1、 调度中心日志自动清理功能(至此,调度中心/执行器均支持日志自动清理,过期天数均默认设置为30天):调度中心新增配置项("xxl.job.logretentiondays")日志保存天数,过期日志自动清理;解决海量日志情况下日志表慢SQL问题;限制大于等于7时生效,否则关闭清理功能,默认为30; +- 2、 调度报表优化:新增日志报表的存储表,三天内的任务日志会以每分钟一次的频率异步同步至报表中;任务报表仅读取报表数据,极大提升加载速度; +- 3、 Cron在线生成工具:任务新增、编辑框通过组件在线生成Cron表达式; +- 4、 Cron下次执行时间查询:支持通过界面在线查看后续连续5次执行时间; +- 5、 调度中心新增应用健康检查功能,借助“spring-boot-starter-actuator”,相对地址 “/actuator/health”; +- 6、 DB脚本默认编码改为utf8mb4,修复字符乱码问题(建议Mysql版本5.7+); +- 7、 调度中心任务平均分配,触发组件每次获取与线程池数量相关数量的任务,避免大量任务集中在单个调度中心集群节点; +- 8、 任务触发组件优化,预加载频率正常1s一次,当预加载轮空时主动休眠一个加载周期,动态降低加载频率从而降低DB压力; +- 9、 调度组件优化:针对永远不会触发的Cron禁止配置和启动;任务Cron最后一次触发后再也不会触发时,比如一次性任务,主动停止相关任务; +- 10、DB重连优化,修复DB宕机重连后任务调度停止的问题,重连后自动加入调度集群触发任务调度; +- 11、注册监控线程优化,降低死锁几率; +- 12、调度中心日志删除优化,改为分页获取ID并根据ID删除的方式,避免批量删除海量日志导致死锁问题; +- 13、任务重试时参数丢失的问题修复; +- 14、调度中心移除SQL中的 "now()" 函数;集群部署时不再依赖DB时钟,仅需要保证调度中心应用节点时钟一致即可; +- 15、任务触发组件加载顺序调整,避免小概率情况下组件随机加载顺序导致的I18N的NPE问题; +- 16、JobThread自销毁优化,避免并发触发导致triggerQueue中任务丢失问题; +- 17、调度中心密码限制18位,修复修改密码超过18位无法登录的问题; +- 18、任务告警组件分页参数无效问题修复; +- 19、升级xxl-rpc版本:服务端线程优化,降低线程内存开销;IpUtil优化:增加连通性校,过滤明确非法的网卡; +- 20、调度中心回调API服务改为restful方式; +- 21、UI优化,任务列表和日志列表数据表格宽度比例调整,避免数据换行提升体验; +- 22、登录界面取消默认填写的登录账号密码; +- 23、执行器表属性调整,"顺序" 属性调整为整型,解决执行器数据较多时无法正确排序的问题; +- 24、任务列表交互优化,支持查看任务所属执行器的注册节点; +- 25、项目依赖升级至较新稳定版本,如spring、spring-boot、mybatis、slf4j、groovy等等; +- 26、日志组件优化:调度中心支持控制每次请求最大加载行数,日志量太大时分批请求,避免单次加载日志量太大阻塞页面; + +### 7.29 版本 v2.1.2 Release Notes[2019-12-12] +- 1、方法任务支持:由原来基于JobHandler类任务开发方式,优化为支持基于方法的任务开发方式;因此,可以支持单个类中开发多个任务方法,进行类复用 +``` +@XxlJob("demoJobHandler") +public ReturnT execute(String param) { + XxlJobLogger.log("hello world"); + return ReturnT.SUCCESS; +} +``` +- 2、移除commons-exec,采用原生方式实现,降低第三方依赖; +- 3、执行器回调乱码问题修复; +- 4、调度中心dispatcher servlet加载顺序优化; +- 5、执行器回调地址https兼容支持; +- 6、多个项目依赖升级至较新稳定版本; +- 注意:最新版本 "XxlJobSpringExecutor" 逻辑有调整,历史项目中该组件的配置方式请参考Sample示例项目进行调整,尤其注意需要移除组件的init和destroy方法; + +### 7.30 版本 v2.2.0 Release Notes[2020-04-14] +- 1、RESTful API:调度中心与执行器提供语言无关的 RESTful API 服务,第三方任意语言可据此对接调度中心或者实现执行器。 +- 2、任务复制功能:点击复制是弹出新建任务弹框,并初始化被复制任务信息; +- 3、任务手动执行一次的时候,支持指定本次执行的机器地址,为空则从执行器获取; +- 4、任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败; +- 5、调度中心升级springboot2.x;因此,系统要求JDK8+; +- 6、XxlJob注解扫描方式优化,支持查找父类以及接口和基于类代理等常见情况;修复任务为空时小概率NPE问题; +- 7、移除旧类注解JobHandler,推荐使用基于方法注解 "@XxlJob" 的方式进行任务开发;(如需保留类注解JobHandler使用方式,可以参考旧版逻辑定制开发); +- 8、任务告警组件模块化:如果需要新增一种告警方式,只需要新增一个实现 "com.xxl.job.admin.core.alarm.JobAlarm" 接口的告警实现即可,更加灵活、方便定制; +- 9、调度中心国际化完善:新增 "中文繁体" 支持。默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文; +- 10、执行器注册逻辑优化:新增配置项 ”注册地址 / xxl.job.executor.address“,优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。 +- 11、默认数据库连接池调整为hikari,移除tomcat-jdbc依赖; +- 12、多个项目依赖升级至较新稳定版本,如mybatis、groovy和mysql驱动等; +- 13、执行器优雅停机优化,修复任务线程中断未join导致回调丢失的问题; +- 14、一致性哈希路由策略优化:默认虚拟节点数量调整为100,提高路由的均衡性; +- 15、通用HTTP任务Handler(httpJobHandler)优化,扩展自定义参数信息,示例参数如下; +``` +url: http://www.xxx.com +method: get 或 post +data: post-data +``` +- 16、SQL脚本编码默认utf8mb4执行,避免小概率下容器环境中乱码问题; +- 17、Web IDE交互问题修复:输入源码备注之后按回车跳转error问题处理; +- 18、执行器初始化逻辑优化:修复懒加载的Bean被提前初始化问题; +- 19、执行器注册默认值优化; +- 20、修复bootstrap.min.css.map 404问题; +- 21、执行器UI交互优化,移除冗余order属性; +- 22、执行备注消息长度限制,修复数据超长无法存储导致导致回调失败的问题; +注意:XxlJobSpringExecutor组件个别字段调整:“appName” 调整为 “appname” ,升级时该组件时需要注意; + +### 7.31 版本 v2.3.0 Release Notes[2021-02-09] +- 1、【新增】调度过期策略:调度中心错过调度时间的补偿处理策略,包括:忽略、立即补偿触发一次等; +- 2、【新增】触发策略:除了常规Cron、API、父子任务触发方式外,新增提供 "固定间隔触发、(固定延时触发,实验中)" 新触发方式; +- 3、【新增】新增任务辅助工具 "XxlJobHelper":提供统一任务辅助能力,包括:任务上下文信息维护获取(任务参数、任务ID、分片参数)、日志输出、任务结果设置……等; + - 3.1、"ShardingUtil" 组件废弃:改用 "XxlJobHelper.getShardIndex()/getShardTotal();" 获取分片参数; + - 3.2、"XxlJobLogger" 组件废弃:改用 "XxlJobHelper.log" 进行日志输出; +- 4、【优化】任务核心类 "IJobHandler" 的 "execute" 方法取消出入参设计。改为通过 "XxlJobHelper.getJobParam" 获取任务参数并替代方法入参,通过 "XxlJobHelper.handleSuccess/handleFail" 设置任务结果并替代方法出参,示例代码如下; +``` +@XxlJob("demoJobHandler") +public void execute() { + String param = XxlJobHelper.getJobParam(); // 获取参数 + XxlJobHelper.handleSuccess(); // 设置任务结果 +} +``` +- 5、【优化】Cron编辑器增强:Cron编辑器修改cron时可实时查看最近运行时间; +- 6、【优化】执行器示例项目规范整理; +- 7、【优化】任务调度生命周期重构:调度(schedule)、触发(trigger)、执行(handle)、回调(callback)、结束(complete); +- 8、【优化】执行器注册组件优化:注册逻辑调整为异步方式,提高注册性能; +- 9、【优化】执行器鉴权校验:执行器启动时主动校验accessToken,为空则主动Warn告警;(已规划安全强化:AccessToken动态生成、动态启停等) +- 10、【优化】邮箱告警配置优化:将"spring.mail.from"与"spring.mail.username"属性拆分开,更加灵活的支持一些无密码邮箱服务; +- 11、【优化】多个项目依赖升级至较新稳定版本,如netty、groovy、spring、springboot、mybatis等; +- 12、【优化】UI组件常规升级,提升组件稳定性; +- 13、【优化】调度中心页面交互优化:用户管理模块密码列取消;多处表达autocomplete取消;执行器管理模块XSS拦截校验等; +- 14、【优化】调度中心任务状态探测慢SQL问题优化; +- 15、【修复】GLUE-Java模式任务,init/destroy无法执行问题修复; +- 16、【修复】Cron编辑器问题修复:修复小概率情况下cron单个字段修改时导致其他字段被重置问题; +- 17、【修复】通用HTTP任务Handler(httpJobHandler)优化:修复 "setDoOutput(true)" 导致任务请求GetMethod失效问题; +- 18、【修复】执行器Commandhandler示例任务优化,修复极端情况下脚本进程挂起问题; +- 19、【修复】调度通讯组件优化,修复RestFul方式调用 DotNet 版本执行器时心跳检测失败问题; +- 20、【修复】调度中心远程执行日志查询乱码问题修复; +- 21、【修复】调度中心组件加载顺序优化,修复极端情况下调度组件初始慢导致的调度失败问题; +- 22、【修复】执行器注册线程优化,修复极端情况下初始化失败时导致NPE问题; +- 23、【修复】调度线程连接池优化,修复连接有效性校验超时问题; +- 24、【修复】执行器注册表字段优化,解决执行器注册节点过多导致注册信息存储和更新失败的问题; +- 25、【修复】轮训路由策略优化,修复小概率下并发问题; +- 26、【修复】页面redirect跳转后https变为http问题修复; +- 27、【修复】执行器日志清理优化,修复小概率下日志文件为空导致清理异常问题; + +### 7.32 版本 v2.3.1 Release Notes[2022-05-21] +- 1、【修复】修复风险漏洞,升级问题低版本项目依赖:CVE-2021-2471、CVE-2022-22965等。 +- 2、【修复】修复故障告警逻辑,邮箱校验逻辑下放至EmailJobAlarm中,避免对其他告警方式的干扰。 +- 3、【优化】调度通讯默认启用accessToken,提升系统安全性(建议生产环境自定义accessToken)。 +- 4、【优化】合并多项PR,项目代码结构、健壮性优化:PR-2833、PR-2812、PR-2541、PR-2537、PR-2514、PR-2509、PR-2591。 +- 5、【优化】任务线程名优化,提升可读性与问题定位效率(ISSUE-2527)。 + +### 7.33 版本 v2.4.0 Release Notes[2022-03-23] +- 1、【优化】执行器任务Bean扫描逻辑优化:解决懒加载注解失效问题。 +- 2、【优化】多个项目依赖升级至较新稳定版本,涉及netty、groovy、spring、springboot、mybatis等; +- 3、【修复】"CVE-2022-36157" 授权漏洞修复。 +- 4、【修复】"CVE-2022-43183" SSRF漏洞修复。 + + +### 7.34 新版本规划 [规划中] +- 1、[规划中]DAG流程任务 + - DAG任务:支持参数传递,共享数据:DAG任务创建、管理,DAG任务日志查看、操作; + - 子任务:废弃 +- 2、[规划中]多数据库支持,DAO层通过JPA实现,不限制数据库类型; +- 3、[规划中]告警增强:邮件告警 + webhook告警; +- 4、[规划中]安全强化:AccessToken动态生成、动态启停;控制调度、回调; +- 5、[规划中]任务导入导出工具,灵活支持版本升级、迁移等场景。 +- 6、【优化】[规划中]任务日志重构:一次调度只记录一条主任务,维护起止时间和状态。 + - 普通任务:只记录一条主任务; + - 广播任务:记录一条主任务,每个分片任务记录一条次任务,关联在主任务上; + - 重试任务:失败时,新增主任务。所有调度记录,包括入口调度和重试调度,均挂载主任务上。 +- 7、【优化】[规划中]分片任务:全部完成后才会出发后置节点; + +### TODO LIST +- 1、任务分片路由:分片采用一致性Hash算法计算出尽量稳定的分片顺序,即使注册机器存在波动也不会引起分批分片顺序大的波动;目前采用IP自然排序,可以满足需求,待定; +- 2、调度隔离:调度中心针对不同执行器,各自维护不同的调度和远程触发组件。 +- 3、调度任务优先级; +- 4、多数据库支持,DAO层通过JPA实现,不限制数据库类型; +- 5、执行器Log清理功能:调度中心Log删除时同步删除执行器中的Log文件; +- 6、延时任务:API触发,支持"动态传参、延时消费";该功能与 XXL-MQ 冲突,该场景建议用后者; +- 7、调度线程池改为协程方式实现,大幅降低系统内存消耗; +- 8、任务、执行器数据全量本地缓存;新增消息表广播通知; +- 9、忙碌转移优化,全部机器忙碌时不再直接失败; +- 10、任务触发参数优化:支持选择 "Cron触发"、"固定间隔时间触发"、"指定时间点触发"、"不选择" 等; +- 11、调度日志列表加上执行时长列,并支持排序; +- 12、DAG流程任务: + - 替换子任务,支持参数传递,共享数据: + - 配置并列的"a-b、b-c"路径列表,构成串行、并行、dag任务流程,"dagre-d3"绘图;任务依赖,流程图,子任务+会签任务,各节点日志;支持根据成功、失败选择分支; + - 分片任务:全部完成后才会出发后置节点; +- 13、日期过滤:支持多个时间段排除; +- 14、告警增强: + - 邮件告警:支持自定义标题、模板格式; + - webhook告警:支持自定义告警URL、请求体格式; +- 15、新增任务运行模式 "GLUE模式(GO) ",支持GO任务; +- 16、GLUE 模式 Web Ide 版本对比功能; +- 17、注册中心优化,实时性注册发现:心跳注册间隔10s,refresh失败则首次注册并立即更新注册信息,心跳类似;30s过期销毁; +- 18、提供执行器Docker镜像; +- 19、脚本任务,支持数据参数,新版本仅支持单参数不支持需要兼容; +- 20、批量调度:调度请求入queue,调度线程批量获取调度请求并发起远程调度;提高线程效率; +- 21、执行器端口复用,复用容器端口提供通讯服务; +- 22、分片任务全部成功后触发子任务; +- 23、新增执行器描述属性;任务名称属性; +- 24、自定义失败重试时间间隔; +- 25、任务标签:方便搜索; +- 26、执行器:dag执行器,不需要注册机器; + + +## 八、其他 + +### 8.1 项目贡献 +欢迎参与项目贡献!比如提交PR修复一个bug,或者新建 [Issue](https://github.com/xuxueli/xxl-job/issues/) 讨论新特性或者变更。 + +### 8.2 用户接入登记 +更多接入的公司,欢迎在 [登记地址](https://github.com/xuxueli/xxl-job/issues/1 ) 登记,登记仅仅为了产品推广。 + +### 8.3 开源协议和版权 +产品开源免费,并且将持续提供免费的社区技术支持。个人或企业内部可自由的接入和使用。如有需要可邮件联系作者免费获取项目授权。 + +- Licensed under the GNU General Public License (GPL) v3. +- Copyright (c) 2015-present, xuxueli. + +--- +### 捐赠 +无论捐赠金额多少都足够表达您这份心意,非常感谢 :) [前往捐赠](https://www.xuxueli.com/page/donate.html ) diff --git "a/project/xxl-job-2.4.0/doc/XXL-JOB\346\236\266\346\236\204\345\233\276.pptx" "b/project/xxl-job-2.4.0/doc/XXL-JOB\346\236\266\346\236\204\345\233\276.pptx" new file mode 100644 index 00000000..5445216c Binary files /dev/null and "b/project/xxl-job-2.4.0/doc/XXL-JOB\346\236\266\346\236\204\345\233\276.pptx" differ diff --git a/project/xxl-job-2.4.0/doc/db/tables_xxl_job.sql b/project/xxl-job-2.4.0/doc/db/tables_xxl_job.sql new file mode 100644 index 00000000..eef97830 --- /dev/null +++ b/project/xxl-job-2.4.0/doc/db/tables_xxl_job.sql @@ -0,0 +1,122 @@ +# +# XXL-JOB v2.4.0 +# Copyright (c) 2015-present, xuxueli. + +CREATE database if NOT EXISTS `cpq01` default character set utf8mb4 collate utf8mb4_unicode_ci; +use `cpq01`; + +SET NAMES utf8mb4; + +CREATE TABLE `xxl_job_info` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `job_group` int(11) NOT NULL COMMENT '执行器主键ID', + `job_desc` varchar(255) NOT NULL, + `add_time` datetime DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `author` varchar(64) DEFAULT NULL COMMENT '作者', + `alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件', + `schedule_type` varchar(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型', + `schedule_conf` varchar(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型', + `misfire_strategy` varchar(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略', + `executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略', + `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler', + `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数', + `executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略', + `executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒', + `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数', + `glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型', + `glue_source` mediumtext COMMENT 'GLUE源代码', + `glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注', + `glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间', + `child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔', + `trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行', + `trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间', + `trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_log` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `job_group` int(11) NOT NULL COMMENT '执行器主键ID', + `job_id` int(11) NOT NULL COMMENT '任务,主键ID', + `executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址', + `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler', + `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数', + `executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2', + `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数', + `trigger_time` datetime DEFAULT NULL COMMENT '调度-时间', + `trigger_code` int(11) NOT NULL COMMENT '调度-结果', + `trigger_msg` text COMMENT '调度-日志', + `handle_time` datetime DEFAULT NULL COMMENT '执行-时间', + `handle_code` int(11) NOT NULL COMMENT '执行-状态', + `handle_msg` text COMMENT '执行-日志', + `alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败', + PRIMARY KEY (`id`), + KEY `I_trigger_time` (`trigger_time`), + KEY `I_handle_code` (`handle_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_log_report` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `trigger_day` datetime DEFAULT NULL COMMENT '调度-时间', + `running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量', + `suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量', + `fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量', + `update_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_logglue` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `job_id` int(11) NOT NULL COMMENT '任务,主键ID', + `glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型', + `glue_source` mediumtext COMMENT 'GLUE源代码', + `glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注', + `add_time` datetime DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_registry` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `registry_group` varchar(50) NOT NULL, + `registry_key` varchar(255) NOT NULL, + `registry_value` varchar(255) NOT NULL, + `update_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_group` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `app_name` varchar(64) NOT NULL COMMENT '执行器AppName', + `title` varchar(12) NOT NULL COMMENT '执行器名称', + `address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入', + `address_list` text COMMENT '执行器地址列表,多地址逗号分隔', + `update_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(50) NOT NULL COMMENT '账号', + `password` varchar(50) NOT NULL COMMENT '密码', + `role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员', + `permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割', + PRIMARY KEY (`id`), + UNIQUE KEY `i_username` (`username`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `xxl_job_lock` ( + `lock_name` varchar(50) NOT NULL COMMENT '锁名称', + PRIMARY KEY (`lock_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2018-11-03 22:21:31' ); +INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', ''); +INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL); +INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock'); + +commit; + diff --git "a/project/xxl-job-2.4.0/doc/images/cnblog-\351\246\226\351\241\265-\346\257\217\346\227\245\344\270\200\345\215\232-\347\254\254\344\270\200.png" "b/project/xxl-job-2.4.0/doc/images/cnblog-\351\246\226\351\241\265-\346\257\217\346\227\245\344\270\200\345\215\232-\347\254\254\344\270\200.png" new file mode 100644 index 00000000..f292d3e4 Binary files /dev/null and "b/project/xxl-job-2.4.0/doc/images/cnblog-\351\246\226\351\241\265-\346\257\217\346\227\245\344\270\200\345\215\232-\347\254\254\344\270\200.png" differ diff --git "a/project/xxl-job-2.4.0/doc/images/cnblog-\351\246\226\351\241\265-\347\203\255\351\227\250\345\212\250\345\274\271-\347\254\254\344\270\200.png" "b/project/xxl-job-2.4.0/doc/images/cnblog-\351\246\226\351\241\265-\347\203\255\351\227\250\345\212\250\345\274\271-\347\254\254\344\270\200.png" new file mode 100644 index 00000000..a594c061 Binary files /dev/null and "b/project/xxl-job-2.4.0/doc/images/cnblog-\351\246\226\351\241\265-\347\203\255\351\227\250\345\212\250\345\274\271-\347\254\254\344\270\200.png" differ diff --git a/project/xxl-job-2.4.0/doc/images/donate-alipay.jpg b/project/xxl-job-2.4.0/doc/images/donate-alipay.jpg new file mode 100644 index 00000000..78472ce6 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/donate-alipay.jpg differ diff --git a/project/xxl-job-2.4.0/doc/images/donate-paypal.png b/project/xxl-job-2.4.0/doc/images/donate-paypal.png new file mode 100644 index 00000000..24e78a40 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/donate-paypal.png differ diff --git a/project/xxl-job-2.4.0/doc/images/donate-wechat.png b/project/xxl-job-2.4.0/doc/images/donate-wechat.png new file mode 100644 index 00000000..4d16dab9 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/donate-wechat.png differ diff --git a/project/xxl-job-2.4.0/doc/images/gitee-gvp.jpg b/project/xxl-job-2.4.0/doc/images/gitee-gvp.jpg new file mode 100644 index 00000000..dcc195b0 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/gitee-gvp.jpg differ diff --git a/project/xxl-job-2.4.0/doc/images/img_1001.png b/project/xxl-job-2.4.0/doc/images/img_1001.png new file mode 100644 index 00000000..549880b8 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_1001.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_1002.png b/project/xxl-job-2.4.0/doc/images/img_1002.png new file mode 100644 index 00000000..411f8ec1 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_1002.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_6yC0.png b/project/xxl-job-2.4.0/doc/images/img_6yC0.png new file mode 100644 index 00000000..01bf573f Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_6yC0.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_BPLG.png b/project/xxl-job-2.4.0/doc/images/img_BPLG.png new file mode 100644 index 00000000..c928205f Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_BPLG.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_EB65.png b/project/xxl-job-2.4.0/doc/images/img_EB65.png new file mode 100644 index 00000000..ccf222d4 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_EB65.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_Fgql.png b/project/xxl-job-2.4.0/doc/images/img_Fgql.png new file mode 100644 index 00000000..f8840516 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_Fgql.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_Hr2T.png b/project/xxl-job-2.4.0/doc/images/img_Hr2T.png new file mode 100644 index 00000000..4b5a73c2 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_Hr2T.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_Qohm.png b/project/xxl-job-2.4.0/doc/images/img_Qohm.png new file mode 100644 index 00000000..854b6c82 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_Qohm.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_UDSo.png b/project/xxl-job-2.4.0/doc/images/img_UDSo.png new file mode 100644 index 00000000..a0b81a43 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_UDSo.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_V3vF.png b/project/xxl-job-2.4.0/doc/images/img_V3vF.png new file mode 100644 index 00000000..4607c84f Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_V3vF.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_Wb2o.png b/project/xxl-job-2.4.0/doc/images/img_Wb2o.png new file mode 100644 index 00000000..fec8dcae Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_Wb2o.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_Ypik.png b/project/xxl-job-2.4.0/doc/images/img_Ypik.png new file mode 100644 index 00000000..6b4a2dd0 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_Ypik.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_Z9Qr.png b/project/xxl-job-2.4.0/doc/images/img_Z9Qr.png new file mode 100644 index 00000000..2bfb0440 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_Z9Qr.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_ZAhX.png b/project/xxl-job-2.4.0/doc/images/img_ZAhX.png new file mode 100644 index 00000000..4a6039ad Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_ZAhX.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_ZAsz.png b/project/xxl-job-2.4.0/doc/images/img_ZAsz.png new file mode 100644 index 00000000..bbb83eca Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_ZAsz.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_dNUJ.png b/project/xxl-job-2.4.0/doc/images/img_dNUJ.png new file mode 100644 index 00000000..a1a57a06 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_dNUJ.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_eYrv.png b/project/xxl-job-2.4.0/doc/images/img_eYrv.png new file mode 100644 index 00000000..3f9c5e9e Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_eYrv.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_hIci.png b/project/xxl-job-2.4.0/doc/images/img_hIci.png new file mode 100644 index 00000000..05292095 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_hIci.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_iUw0.png b/project/xxl-job-2.4.0/doc/images/img_iUw0.png new file mode 100644 index 00000000..b746000b Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_iUw0.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_inc8.png b/project/xxl-job-2.4.0/doc/images/img_inc8.png new file mode 100644 index 00000000..e1d38d6f Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_inc8.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_jOAU.png b/project/xxl-job-2.4.0/doc/images/img_jOAU.png new file mode 100644 index 00000000..beddc97d Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_jOAU.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_jrdI.png b/project/xxl-job-2.4.0/doc/images/img_jrdI.png new file mode 100644 index 00000000..69b38c06 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_jrdI.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_o8HQ.png b/project/xxl-job-2.4.0/doc/images/img_o8HQ.png new file mode 100644 index 00000000..46b5bd0a Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_o8HQ.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_tJOq.png b/project/xxl-job-2.4.0/doc/images/img_tJOq.png new file mode 100644 index 00000000..f3d00625 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_tJOq.png differ diff --git a/project/xxl-job-2.4.0/doc/images/img_tvGI.png b/project/xxl-job-2.4.0/doc/images/img_tvGI.png new file mode 100644 index 00000000..4b2265a7 Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/img_tvGI.png differ diff --git "a/project/xxl-job-2.4.0/doc/images/qq\347\276\244-\344\270\200\344\270\252xxl\345\220\214\345\255\246\350\277\233\344\272\20658.png" "b/project/xxl-job-2.4.0/doc/images/qq\347\276\244-\344\270\200\344\270\252xxl\345\220\214\345\255\246\350\277\233\344\272\20658.png" new file mode 100644 index 00000000..a525e0b7 Binary files /dev/null and "b/project/xxl-job-2.4.0/doc/images/qq\347\276\244-\344\270\200\344\270\252xxl\345\220\214\345\255\246\350\277\233\344\272\20658.png" differ diff --git a/project/xxl-job-2.4.0/doc/images/xxl-logo.jpg b/project/xxl-job-2.4.0/doc/images/xxl-logo.jpg new file mode 100644 index 00000000..0169ccee Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/xxl-logo.jpg differ diff --git a/project/xxl-job-2.4.0/doc/images/xxl-logo.png b/project/xxl-job-2.4.0/doc/images/xxl-logo.png new file mode 100644 index 00000000..045c006c Binary files /dev/null and b/project/xxl-job-2.4.0/doc/images/xxl-logo.png differ diff --git a/project/xxl-job-2.4.0/pom.xml b/project/xxl-job-2.4.0/pom.xml new file mode 100644 index 00000000..66223a90 --- /dev/null +++ b/project/xxl-job-2.4.0/pom.xml @@ -0,0 +1,144 @@ + + 4.0.0 + com.xuxueli + xxl-job + 2.4.0 + pom + + ${project.artifactId} + A distributed task scheduling framework. + https://www.xuxueli.com/ + + + xxl-job-core + xxl-job-admin + xxl-job-executor-samples + + + + UTF-8 + UTF-8 + UTF-8 + 1.8 + 1.8 + true + + 4.1.90.Final + 2.10.1 + + 5.3.26 + 2.7.9 + + 2.3.0 + 8.0.32 + + 1.7.36 + 5.9.2 + 1.3.2 + + 4.0.10 + + 3.2.1 + 3.5.0 + 3.0.1 + + + + + + + + + + + GNU General Public License version 3 + https://opensource.org/licenses/GPL-3.0 + + + + + master + https://github.com/xuxueli/xxl-job.git + scm:git:https://github.com/xuxueli/xxl-job.git + scm:git:git@github.com:xuxueli/xxl-job.git + + + + XXL + xuxueli + 931591021@qq.com + https://github.com/xuxueli + + + + + + + release + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + package + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + package + + jar + + + none + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + false + + + + verify + + sign + + + + + + + + + oss + https://oss.sonatype.org/content/repositories/snapshots/ + + + oss + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/Dockerfile b/project/xxl-job-2.4.0/xxl-job-admin/Dockerfile new file mode 100644 index 00000000..dc195371 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/Dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:8-jre-slim +MAINTAINER xuxueli + +ENV PARAMS="" + +ENV TZ=PRC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +ADD target/xxl-job-admin-*.jar /app.jar + +ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS /app.jar $PARAMS"] \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/pom.xml b/project/xxl-job-2.4.0/xxl-job-admin/pom.xml new file mode 100644 index 00000000..0d9c4da4 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/pom.xml @@ -0,0 +1,113 @@ + + 4.0.0 + + com.xuxueli + xxl-job + 2.4.0 + + xxl-job-admin + jar + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring-boot.version} + pom + import + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-freemarker + + + + + org.springframework.boot + spring-boot-starter-mail + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis-spring-boot-starter.version} + + + + com.mysql + mysql-connector-j + ${mysql-connector-j.version} + + + + + com.xuxueli + xxl-job-core + ${project.parent.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + + + + com.spotify + docker-maven-plugin + 0.4.13 + + + ${project.artifactId}:${project.version} + ${project.basedir} + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + + + + + + diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java new file mode 100644 index 00000000..fce10a81 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/XxlJobAdminApplication.java @@ -0,0 +1,16 @@ +package com.xxl.job.admin; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author xuxueli 2018-10-28 00:38:13 + */ +@SpringBootApplication +public class XxlJobAdminApplication { + + public static void main(String[] args) { + SpringApplication.run(XxlJobAdminApplication.class, args); + } + +} \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/CustomApiController.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/CustomApiController.java new file mode 100644 index 00000000..e258f917 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/CustomApiController.java @@ -0,0 +1,34 @@ +package com.xxl.job.admin.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.core.biz.model.ReturnT; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * 自定义API接口 + * @author chenpq05 + * @since 2023/3/28 16:33 + */ +@RestController +@RequestMapping("/custom-api") +public class CustomApiController { + + private static Logger logger = LoggerFactory.getLogger(CustomApiController.class); + + @RequestMapping("/mytest") + @PermissionLimit(limit = false) + public ReturnT mytest(@RequestBody XxlJobInfo jobInfo) throws Exception{ + logger.info("##################"); + String s = new ObjectMapper().writeValueAsString(jobInfo); + logger.info(s); + return new ReturnT(); + } +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java new file mode 100644 index 00000000..eb63f0bd --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/IndexController.java @@ -0,0 +1,96 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +public class IndexController { + + @Resource + private XxlJobService xxlJobService; + @Resource + private LoginService loginService; + + + @RequestMapping("/") + public String index(Model model) { + + Map dashboardMap = xxlJobService.dashboardInfo(); + model.addAllAttributes(dashboardMap); + + return "index"; + } + + @RequestMapping("/chartInfo") + @ResponseBody + public ReturnT> chartInfo(Date startDate, Date endDate) { + ReturnT> chartInfo = xxlJobService.chartInfo(startDate, endDate); + return chartInfo; + } + + @RequestMapping("/toLogin") + @PermissionLimit(limit=false) + public ModelAndView toLogin(HttpServletRequest request, HttpServletResponse response,ModelAndView modelAndView) { + if (loginService.ifLogin(request, response) != null) { + modelAndView.setView(new RedirectView("/",true,false)); + return modelAndView; + } + return new ModelAndView("login"); + } + + @RequestMapping(value="login", method=RequestMethod.POST) + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT loginDo(HttpServletRequest request, HttpServletResponse response, String userName, String password, String ifRemember){ + boolean ifRem = (ifRemember!=null && ifRemember.trim().length()>0 && "on".equals(ifRemember))?true:false; + return loginService.login(request, response, userName, password, ifRem); + } + + @RequestMapping(value="logout", method=RequestMethod.POST) + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT logout(HttpServletRequest request, HttpServletResponse response){ + return loginService.logout(request, response); + } + + @RequestMapping("/help") + public String help() { + + /*if (!PermissionInterceptor.ifLogin(request)) { + return "redirect:/toLogin"; + }*/ + + return "help"; + } + + @InitBinder + public void initBinder(WebDataBinder binder) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + dateFormat.setLenient(false); + binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java new file mode 100644 index 00000000..aa51e739 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobApiController.java @@ -0,0 +1,72 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.GsonTool; +import com.xxl.job.core.util.XxlJobRemotingUtil; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * Created by xuxueli on 17/5/10. + */ +@Controller +@RequestMapping("/api") +public class JobApiController { + + @Resource + private AdminBiz adminBiz; + + /** + * api + * + * @param uri + * @param data + * @return + */ + @RequestMapping("/{uri}") + @ResponseBody + @PermissionLimit(limit=false) + public ReturnT api(HttpServletRequest request, @PathVariable("uri") String uri, @RequestBody(required = false) String data) { + + // valid + if (!"POST".equalsIgnoreCase(request.getMethod())) { + return new ReturnT(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support."); + } + if (uri==null || uri.trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty."); + } + if (XxlJobAdminConfig.getAdminConfig().getAccessToken()!=null + && XxlJobAdminConfig.getAdminConfig().getAccessToken().trim().length()>0 + && !XxlJobAdminConfig.getAdminConfig().getAccessToken().equals(request.getHeader(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN))) { + return new ReturnT(ReturnT.FAIL_CODE, "The access token is wrong."); + } + + // services mapping + if ("callback".equals(uri)) { + List callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class); + return adminBiz.callback(callbackParamList); + } else if ("registry".equals(uri)) { + RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class); + return adminBiz.registry(registryParam); + } else if ("registryRemove".equals(uri)) { + RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class); + return adminBiz.registryRemove(registryParam); + } else { + return new ReturnT(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found."); + } + + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java new file mode 100644 index 00000000..fe4a0e84 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobCodeController.java @@ -0,0 +1,96 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLogGlue; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobLogGlueDao; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.glue.GlueTypeEnum; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.List; + +/** + * job code controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/jobcode") +public class JobCodeController { + + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + private XxlJobLogGlueDao xxlJobLogGlueDao; + + @RequestMapping + public String index(HttpServletRequest request, Model model, int jobId) { + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(jobId); + List jobLogGlues = xxlJobLogGlueDao.findByJobId(jobId); + + if (jobInfo == null) { + throw new RuntimeException(I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + if (GlueTypeEnum.BEAN == GlueTypeEnum.match(jobInfo.getGlueType())) { + throw new RuntimeException(I18nUtil.getString("jobinfo_glue_gluetype_unvalid")); + } + + // valid permission + JobInfoController.validPermission(request, jobInfo.getJobGroup()); + + // Glue类型-字典 + model.addAttribute("GlueTypeEnum", GlueTypeEnum.values()); + + model.addAttribute("jobInfo", jobInfo); + model.addAttribute("jobLogGlues", jobLogGlues); + return "jobcode/jobcode.index"; + } + + @RequestMapping("/save") + @ResponseBody + public ReturnT save(Model model, int id, String glueSource, String glueRemark) { + // valid + if (glueRemark==null) { + return new ReturnT(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobinfo_glue_remark")) ); + } + if (glueRemark.length()<4 || glueRemark.length()>100) { + return new ReturnT(500, I18nUtil.getString("jobinfo_glue_remark_limit")); + } + XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(id); + if (exists_jobInfo == null) { + return new ReturnT(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + + // update new code + exists_jobInfo.setGlueSource(glueSource); + exists_jobInfo.setGlueRemark(glueRemark); + exists_jobInfo.setGlueUpdatetime(new Date()); + + exists_jobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(exists_jobInfo); + + // log old code + XxlJobLogGlue xxlJobLogGlue = new XxlJobLogGlue(); + xxlJobLogGlue.setJobId(exists_jobInfo.getId()); + xxlJobLogGlue.setGlueType(exists_jobInfo.getGlueType()); + xxlJobLogGlue.setGlueSource(glueSource); + xxlJobLogGlue.setGlueRemark(glueRemark); + + xxlJobLogGlue.setAddTime(new Date()); + xxlJobLogGlue.setUpdateTime(new Date()); + xxlJobLogGlueDao.save(xxlJobLogGlue); + + // remove code backup more than 30 + xxlJobLogGlueDao.removeOld(exists_jobInfo.getId(), 30); + + return ReturnT.SUCCESS; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java new file mode 100644 index 00000000..8e0c5a4d --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobGroupController.java @@ -0,0 +1,204 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobRegistry; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobRegistryDao; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.RegistryConfig; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * job group controller + * @author xuxueli 2016-10-02 20:52:56 + */ +@Controller +@RequestMapping("/jobgroup") +public class JobGroupController { + + @Resource + public XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobRegistryDao xxlJobRegistryDao; + + @RequestMapping + @PermissionLimit(adminuser = true) + public String index(Model model) { + return "jobgroup/jobgroup.index"; + } + + @RequestMapping("/pageList") + @ResponseBody + @PermissionLimit(adminuser = true) + public Map pageList(HttpServletRequest request, + @RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + String appname, String title) { + + // page query + List list = xxlJobGroupDao.pageList(start, length, appname, title); + int list_count = xxlJobGroupDao.pageListCount(start, length, appname, title); + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/save") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT save(XxlJobGroup xxlJobGroup){ + + // valid + if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input")+"AppName") ); + } + if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_appname_length") ); + } + if (xxlJobGroup.getAppname().contains(">") || xxlJobGroup.getAppname().contains("<")) { + return new ReturnT(500, "AppName"+I18nUtil.getString("system_unvalid") ); + } + if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) ); + } + if (xxlJobGroup.getTitle().contains(">") || xxlJobGroup.getTitle().contains("<")) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_title")+I18nUtil.getString("system_unvalid") ); + } + if (xxlJobGroup.getAddressType()!=0) { + if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_addressType_limit") ); + } + if (xxlJobGroup.getAddressList().contains(">") || xxlJobGroup.getAddressList().contains("<")) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_registryList")+I18nUtil.getString("system_unvalid") ); + } + + String[] addresss = xxlJobGroup.getAddressList().split(","); + for (String item: addresss) { + if (item==null || item.trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") ); + } + } + } + + // process + xxlJobGroup.setUpdateTime(new Date()); + + int ret = xxlJobGroupDao.save(xxlJobGroup); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + @RequestMapping("/update") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT update(XxlJobGroup xxlJobGroup){ + // valid + if (xxlJobGroup.getAppname()==null || xxlJobGroup.getAppname().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input")+"AppName") ); + } + if (xxlJobGroup.getAppname().length()<4 || xxlJobGroup.getAppname().length()>64) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_appname_length") ); + } + if (xxlJobGroup.getTitle()==null || xxlJobGroup.getTitle().trim().length()==0) { + return new ReturnT(500, (I18nUtil.getString("system_please_input") + I18nUtil.getString("jobgroup_field_title")) ); + } + if (xxlJobGroup.getAddressType() == 0) { + // 0=自动注册 + List registryList = findRegistryByAppName(xxlJobGroup.getAppname()); + String addressListStr = null; + if (registryList!=null && !registryList.isEmpty()) { + Collections.sort(registryList); + addressListStr = ""; + for (String item:registryList) { + addressListStr += item + ","; + } + addressListStr = addressListStr.substring(0, addressListStr.length()-1); + } + xxlJobGroup.setAddressList(addressListStr); + } else { + // 1=手动录入 + if (xxlJobGroup.getAddressList()==null || xxlJobGroup.getAddressList().trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_addressType_limit") ); + } + String[] addresss = xxlJobGroup.getAddressList().split(","); + for (String item: addresss) { + if (item==null || item.trim().length()==0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_field_registryList_unvalid") ); + } + } + } + + // process + xxlJobGroup.setUpdateTime(new Date()); + + int ret = xxlJobGroupDao.update(xxlJobGroup); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + private List findRegistryByAppName(String appnameParam){ + HashMap> appAddressMap = new HashMap>(); + List list = xxlJobRegistryDao.findAll(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (list != null) { + for (XxlJobRegistry item: list) { + if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) { + String appname = item.getRegistryKey(); + List registryList = appAddressMap.get(appname); + if (registryList == null) { + registryList = new ArrayList(); + } + + if (!registryList.contains(item.getRegistryValue())) { + registryList.add(item.getRegistryValue()); + } + appAddressMap.put(appname, registryList); + } + } + } + return appAddressMap.get(appnameParam); + } + + @RequestMapping("/remove") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT remove(int id){ + + // valid + int count = xxlJobInfoDao.pageListCount(0, 10, id, -1, null, null, null); + if (count > 0) { + return new ReturnT(500, I18nUtil.getString("jobgroup_del_limit_0") ); + } + + List allList = xxlJobGroupDao.findAll(); + if (allList.size() == 1) { + return new ReturnT(500, I18nUtil.getString("jobgroup_del_limit_1") ); + } + + int ret = xxlJobGroupDao.remove(id); + return (ret>0)?ReturnT.SUCCESS:ReturnT.FAIL; + } + + @RequestMapping("/loadById") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT loadById(int id){ + XxlJobGroup jobGroup = xxlJobGroupDao.load(id); + return jobGroup!=null?new ReturnT(jobGroup):new ReturnT(ReturnT.FAIL_CODE, null); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java new file mode 100644 index 00000000..7152278c --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobInfoController.java @@ -0,0 +1,184 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.cron.CronExpression; +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.thread.JobScheduleHelper; +import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.text.ParseException; +import java.util.*; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/jobinfo") +public class JobInfoController { + private static Logger logger = LoggerFactory.getLogger(JobInfoController.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobService xxlJobService; + + @RequestMapping + public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "-1") int jobGroup) { + + // 枚举-字典 + model.addAttribute("ExecutorRouteStrategyEnum", ExecutorRouteStrategyEnum.values()); // 路由策略-列表 + model.addAttribute("GlueTypeEnum", GlueTypeEnum.values()); // Glue类型-字典 + model.addAttribute("ExecutorBlockStrategyEnum", ExecutorBlockStrategyEnum.values()); // 阻塞处理策略-字典 + model.addAttribute("ScheduleTypeEnum", ScheduleTypeEnum.values()); // 调度类型 + model.addAttribute("MisfireStrategyEnum", MisfireStrategyEnum.values()); // 调度过期策略 + + // 执行器列表 + List jobGroupList_all = xxlJobGroupDao.findAll(); + + // filter group + List jobGroupList = filterJobGroupByRole(request, jobGroupList_all); + if (jobGroupList==null || jobGroupList.size()==0) { + throw new XxlJobException(I18nUtil.getString("jobgroup_empty")); + } + + model.addAttribute("JobGroupList", jobGroupList); + model.addAttribute("jobGroup", jobGroup); + + return "jobinfo/jobinfo.index"; + } + + public static List filterJobGroupByRole(HttpServletRequest request, List jobGroupList_all){ + List jobGroupList = new ArrayList<>(); + if (jobGroupList_all!=null && jobGroupList_all.size()>0) { + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getRole() == 1) { + jobGroupList = jobGroupList_all; + } else { + List groupIdStrs = new ArrayList<>(); + if (loginUser.getPermission()!=null && loginUser.getPermission().trim().length()>0) { + groupIdStrs = Arrays.asList(loginUser.getPermission().trim().split(",")); + } + for (XxlJobGroup groupItem:jobGroupList_all) { + if (groupIdStrs.contains(String.valueOf(groupItem.getId()))) { + jobGroupList.add(groupItem); + } + } + } + } + return jobGroupList; + } + public static void validPermission(HttpServletRequest request, int jobGroup) { + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (!loginUser.validPermission(jobGroup)) { + throw new RuntimeException(I18nUtil.getString("system_permission_limit") + "[username="+ loginUser.getUsername() +"]"); + } + } + + @RequestMapping("/pageList") + @ResponseBody + public Map pageList(@RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) { + + return xxlJobService.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + } + + @RequestMapping("/add") + @ResponseBody + public ReturnT add(XxlJobInfo jobInfo) { + return xxlJobService.add(jobInfo); + } + + @RequestMapping("/update") + @ResponseBody + public ReturnT update(XxlJobInfo jobInfo) { + return xxlJobService.update(jobInfo); + } + + @RequestMapping("/remove") + @ResponseBody + public ReturnT remove(int id) { + return xxlJobService.remove(id); + } + + @RequestMapping("/stop") + @ResponseBody + public ReturnT pause(int id) { + return xxlJobService.stop(id); + } + + @RequestMapping("/start") + @ResponseBody + public ReturnT start(int id) { + return xxlJobService.start(id); + } + + @RequestMapping("/trigger") + @ResponseBody + //@PermissionLimit(limit = false) + public ReturnT triggerJob(int id, String executorParam, String addressList) { + // force cover job param + if (executorParam == null) { + executorParam = ""; + } + + JobTriggerPoolHelper.trigger(id, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList); + return ReturnT.SUCCESS; + } + + @RequestMapping("/nextTriggerTime") + @ResponseBody + public ReturnT> nextTriggerTime(String scheduleType, String scheduleConf) { + + XxlJobInfo paramXxlJobInfo = new XxlJobInfo(); + paramXxlJobInfo.setScheduleType(scheduleType); + paramXxlJobInfo.setScheduleConf(scheduleConf); + + List result = new ArrayList<>(); + try { + Date lastTime = new Date(); + for (int i = 0; i < 5; i++) { + lastTime = JobScheduleHelper.generateNextValidTime(paramXxlJobInfo, lastTime); + if (lastTime != null) { + result.add(DateUtil.formatDateTime(lastTime)); + } else { + break; + } + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) + e.getMessage()); + } + return new ReturnT>(result); + + } + + + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java new file mode 100644 index 00000000..8a9176ea --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/JobLogController.java @@ -0,0 +1,237 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.core.complete.XxlJobCompleter; +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobInfoDao; +import com.xxl.job.admin.dao.XxlJobLogDao; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.KillParam; +import com.xxl.job.core.biz.model.LogParam; +import com.xxl.job.core.biz.model.LogResult; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * index controller + * @author xuxueli 2015-12-19 16:13:16 + */ +@Controller +@RequestMapping("/joblog") +public class JobLogController { + private static Logger logger = LoggerFactory.getLogger(JobLogController.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + public XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobLogDao xxlJobLogDao; + + @RequestMapping + public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "0") Integer jobId) { + + // 执行器列表 + List jobGroupList_all = xxlJobGroupDao.findAll(); + + // filter group + List jobGroupList = JobInfoController.filterJobGroupByRole(request, jobGroupList_all); + if (jobGroupList==null || jobGroupList.size()==0) { + throw new XxlJobException(I18nUtil.getString("jobgroup_empty")); + } + + model.addAttribute("JobGroupList", jobGroupList); + + // 任务 + if (jobId > 0) { + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(jobId); + if (jobInfo == null) { + throw new RuntimeException(I18nUtil.getString("jobinfo_field_id") + I18nUtil.getString("system_unvalid")); + } + + model.addAttribute("jobInfo", jobInfo); + + // valid permission + JobInfoController.validPermission(request, jobInfo.getJobGroup()); + } + + return "joblog/joblog.index"; + } + + @RequestMapping("/getJobsByGroup") + @ResponseBody + public ReturnT> getJobsByGroup(int jobGroup){ + List list = xxlJobInfoDao.getJobsByGroup(jobGroup); + return new ReturnT>(list); + } + + @RequestMapping("/pageList") + @ResponseBody + public Map pageList(HttpServletRequest request, + @RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + int jobGroup, int jobId, int logStatus, String filterTime) { + + // valid permission + JobInfoController.validPermission(request, jobGroup); // 仅管理员支持查询全部;普通用户仅支持查询有权限的 jobGroup + + // parse param + Date triggerTimeStart = null; + Date triggerTimeEnd = null; + if (filterTime!=null && filterTime.trim().length()>0) { + String[] temp = filterTime.split(" - "); + if (temp.length == 2) { + triggerTimeStart = DateUtil.parseDateTime(temp[0]); + triggerTimeEnd = DateUtil.parseDateTime(temp[1]); + } + } + + // page query + List list = xxlJobLogDao.pageList(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus); + int list_count = xxlJobLogDao.pageListCount(start, length, jobGroup, jobId, triggerTimeStart, triggerTimeEnd, logStatus); + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/logDetailPage") + public String logDetailPage(int id, Model model){ + + // base check + ReturnT logStatue = ReturnT.SUCCESS; + XxlJobLog jobLog = xxlJobLogDao.load(id); + if (jobLog == null) { + throw new RuntimeException(I18nUtil.getString("joblog_logid_unvalid")); + } + + model.addAttribute("triggerCode", jobLog.getTriggerCode()); + model.addAttribute("handleCode", jobLog.getHandleCode()); + model.addAttribute("logId", jobLog.getId()); + return "joblog/joblog.detail"; + } + + @RequestMapping("/logDetailCat") + @ResponseBody + public ReturnT logDetailCat(long logId, int fromLineNum){ + try { + // valid + XxlJobLog jobLog = xxlJobLogDao.load(logId); // todo, need to improve performance + if (jobLog == null) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("joblog_logid_unvalid")); + } + + // log cat + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(jobLog.getExecutorAddress()); + ReturnT logResult = executorBiz.log(new LogParam(jobLog.getTriggerTime().getTime(), logId, fromLineNum)); + + // is end + if (logResult.getContent()!=null && logResult.getContent().getFromLineNum() > logResult.getContent().getToLineNum()) { + if (jobLog.getHandleCode() > 0) { + logResult.getContent().setEnd(true); + } + } + + return logResult; + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT(ReturnT.FAIL_CODE, e.getMessage()); + } + } + + @RequestMapping("/logKill") + @ResponseBody + public ReturnT logKill(int id){ + // base check + XxlJobLog log = xxlJobLogDao.load(id); + XxlJobInfo jobInfo = xxlJobInfoDao.loadById(log.getJobId()); + if (jobInfo==null) { + return new ReturnT(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid")); + } + if (ReturnT.SUCCESS_CODE != log.getTriggerCode()) { + return new ReturnT(500, I18nUtil.getString("joblog_kill_log_limit")); + } + + // request of kill + ReturnT runResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(log.getExecutorAddress()); + runResult = executorBiz.kill(new KillParam(jobInfo.getId())); + } catch (Exception e) { + logger.error(e.getMessage(), e); + runResult = new ReturnT(500, e.getMessage()); + } + + if (ReturnT.SUCCESS_CODE == runResult.getCode()) { + log.setHandleCode(ReturnT.FAIL_CODE); + log.setHandleMsg( I18nUtil.getString("joblog_kill_log_byman")+":" + (runResult.getMsg()!=null?runResult.getMsg():"")); + log.setHandleTime(new Date()); + XxlJobCompleter.updateHandleInfoAndFinish(log); + return new ReturnT(runResult.getMsg()); + } else { + return new ReturnT(500, runResult.getMsg()); + } + } + + @RequestMapping("/clearLog") + @ResponseBody + public ReturnT clearLog(int jobGroup, int jobId, int type){ + + Date clearBeforeTime = null; + int clearBeforeNum = 0; + if (type == 1) { + clearBeforeTime = DateUtil.addMonths(new Date(), -1); // 清理一个月之前日志数据 + } else if (type == 2) { + clearBeforeTime = DateUtil.addMonths(new Date(), -3); // 清理三个月之前日志数据 + } else if (type == 3) { + clearBeforeTime = DateUtil.addMonths(new Date(), -6); // 清理六个月之前日志数据 + } else if (type == 4) { + clearBeforeTime = DateUtil.addYears(new Date(), -1); // 清理一年之前日志数据 + } else if (type == 5) { + clearBeforeNum = 1000; // 清理一千条以前日志数据 + } else if (type == 6) { + clearBeforeNum = 10000; // 清理一万条以前日志数据 + } else if (type == 7) { + clearBeforeNum = 30000; // 清理三万条以前日志数据 + } else if (type == 8) { + clearBeforeNum = 100000; // 清理十万条以前日志数据 + } else if (type == 9) { + clearBeforeNum = 0; // 清理所有日志数据 + } else { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("joblog_clean_type_unvalid")); + } + + List logIds = null; + do { + logIds = xxlJobLogDao.findClearLogIds(jobGroup, jobId, clearBeforeTime, clearBeforeNum, 1000); + if (logIds!=null && logIds.size()>0) { + xxlJobLogDao.clearLog(logIds); + } + } while (logIds!=null && logIds.size()>0); + + return ReturnT.SUCCESS; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java new file mode 100644 index 00000000..3f4c7559 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/UserController.java @@ -0,0 +1,179 @@ +package com.xxl.job.admin.controller; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.XxlJobGroupDao; +import com.xxl.job.admin.dao.XxlJobUserDao; +import com.xxl.job.admin.service.LoginService; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author xuxueli 2019-05-04 16:39:50 + */ +@Controller +@RequestMapping("/user") +public class UserController { + + @Resource + private XxlJobUserDao xxlJobUserDao; + @Resource + private XxlJobGroupDao xxlJobGroupDao; + + @RequestMapping + @PermissionLimit(adminuser = true) + public String index(Model model) { + + // 执行器列表 + List groupList = xxlJobGroupDao.findAll(); + model.addAttribute("groupList", groupList); + + return "user/user.index"; + } + + @RequestMapping("/pageList") + @ResponseBody + @PermissionLimit(adminuser = true) + public Map pageList(@RequestParam(required = false, defaultValue = "0") int start, + @RequestParam(required = false, defaultValue = "10") int length, + String username, int role) { + + // page list + List list = xxlJobUserDao.pageList(start, length, username, role); + int list_count = xxlJobUserDao.pageListCount(start, length, username, role); + + // filter + if (list!=null && list.size()>0) { + for (XxlJobUser item: list) { + item.setPassword(null); + } + } + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @RequestMapping("/add") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT add(XxlJobUser xxlJobUser) { + + // valid username + if (!StringUtils.hasText(xxlJobUser.getUsername())) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_username") ); + } + xxlJobUser.setUsername(xxlJobUser.getUsername().trim()); + if (!(xxlJobUser.getUsername().length()>=4 && xxlJobUser.getUsername().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // valid password + if (!StringUtils.hasText(xxlJobUser.getPassword())) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_please_input")+I18nUtil.getString("user_password") ); + } + xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); + if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // md5 password + xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes())); + + // check repeat + XxlJobUser existUser = xxlJobUserDao.loadByUserName(xxlJobUser.getUsername()); + if (existUser != null) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("user_username_repeat") ); + } + + // write + xxlJobUserDao.save(xxlJobUser); + return ReturnT.SUCCESS; + } + + @RequestMapping("/update") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT update(HttpServletRequest request, XxlJobUser xxlJobUser) { + + // avoid opt login seft + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getUsername().equals(xxlJobUser.getUsername())) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit")); + } + + // valid password + if (StringUtils.hasText(xxlJobUser.getPassword())) { + xxlJobUser.setPassword(xxlJobUser.getPassword().trim()); + if (!(xxlJobUser.getPassword().length()>=4 && xxlJobUser.getPassword().length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + // md5 password + xxlJobUser.setPassword(DigestUtils.md5DigestAsHex(xxlJobUser.getPassword().getBytes())); + } else { + xxlJobUser.setPassword(null); + } + + // write + xxlJobUserDao.update(xxlJobUser); + return ReturnT.SUCCESS; + } + + @RequestMapping("/remove") + @ResponseBody + @PermissionLimit(adminuser = true) + public ReturnT remove(HttpServletRequest request, int id) { + + // avoid opt login seft + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + if (loginUser.getId() == id) { + return new ReturnT(ReturnT.FAIL.getCode(), I18nUtil.getString("user_update_loginuser_limit")); + } + + xxlJobUserDao.delete(id); + return ReturnT.SUCCESS; + } + + @RequestMapping("/updatePwd") + @ResponseBody + public ReturnT updatePwd(HttpServletRequest request, String password){ + + // valid password + if (password==null || password.trim().length()==0){ + return new ReturnT(ReturnT.FAIL.getCode(), "密码不可为空"); + } + password = password.trim(); + if (!(password.length()>=4 && password.length()<=20)) { + return new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("system_lengh_limit")+"[4-20]" ); + } + + // md5 password + String md5Password = DigestUtils.md5DigestAsHex(password.getBytes()); + + // update pwd + XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY); + + // do write + XxlJobUser existUser = xxlJobUserDao.loadByUserName(loginUser.getUsername()); + existUser.setPassword(md5Password); + xxlJobUserDao.update(existUser); + + return ReturnT.SUCCESS; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java new file mode 100644 index 00000000..379efd46 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/annotation/PermissionLimit.java @@ -0,0 +1,29 @@ +package com.xxl.job.admin.controller.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 权限限制 + * @author xuxueli 2015-12-12 18:29:02 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface PermissionLimit { + + /** + * 登录拦截 (默认拦截) + */ + boolean limit() default true; + + /** + * 要求管理员权限 + * + * @return + */ + boolean adminuser() default false; + +} \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java new file mode 100644 index 00000000..592d4961 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/CookieInterceptor.java @@ -0,0 +1,42 @@ +package com.xxl.job.admin.controller.interceptor; + +import com.xxl.job.admin.core.util.FtlUtil; +import com.xxl.job.admin.core.util.I18nUtil; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.AsyncHandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; + +/** + * push cookies to model as cookieMap + * + * @author xuxueli 2015-12-12 18:09:04 + */ +@Component +public class CookieInterceptor implements AsyncHandlerInterceptor { + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + + // cookie + if (modelAndView!=null && request.getCookies()!=null && request.getCookies().length>0) { + HashMap cookieMap = new HashMap(); + for (Cookie ck : request.getCookies()) { + cookieMap.put(ck.getName(), ck); + } + modelAndView.addObject("cookieMap", cookieMap); + } + + // static method + if (modelAndView != null) { + modelAndView.addObject("I18nUtil", FtlUtil.generateStaticModel(I18nUtil.class.getName())); + } + + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java new file mode 100644 index 00000000..840f0ebc --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/PermissionInterceptor.java @@ -0,0 +1,59 @@ +package com.xxl.job.admin.controller.interceptor; + +import com.xxl.job.admin.controller.annotation.PermissionLimit; +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.service.LoginService; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.AsyncHandlerInterceptor; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 权限拦截 + * + * @author xuxueli 2015-12-12 18:09:04 + */ +@Component +public class PermissionInterceptor implements AsyncHandlerInterceptor { + + @Resource + private LoginService loginService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + if (!(handler instanceof HandlerMethod)) { + return true; // proceed with the next interceptor + } + + // if need login + boolean needLogin = true; + boolean needAdminuser = false; + HandlerMethod method = (HandlerMethod)handler; + PermissionLimit permission = method.getMethodAnnotation(PermissionLimit.class); + if (permission!=null) { + needLogin = permission.limit(); + needAdminuser = permission.adminuser(); + } + + if (needLogin) { + XxlJobUser loginUser = loginService.ifLogin(request, response); + if (loginUser == null) { + response.setStatus(302); + response.setHeader("location", request.getContextPath()+"/toLogin"); + return false; + } + if (needAdminuser && loginUser.getRole()!=1) { + throw new RuntimeException(I18nUtil.getString("system_permission_limit")); + } + request.setAttribute(LoginService.LOGIN_IDENTITY_KEY, loginUser); + } + + return true; // proceed with the next interceptor + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java new file mode 100644 index 00000000..0be6ba66 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/interceptor/WebMvcConfig.java @@ -0,0 +1,28 @@ +package com.xxl.job.admin.controller.interceptor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.Resource; + +/** + * web mvc config + * + * @author xuxueli 2018-04-02 20:48:20 + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Resource + private PermissionInterceptor permissionInterceptor; + @Resource + private CookieInterceptor cookieInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(permissionInterceptor).addPathPatterns("/**"); + registry.addInterceptor(cookieInterceptor).addPathPatterns("/**"); + } + +} \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java new file mode 100644 index 00000000..114407b6 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/controller/resolver/WebExceptionResolver.java @@ -0,0 +1,66 @@ +package com.xxl.job.admin.controller.resolver; + +import com.xxl.job.admin.core.exception.XxlJobException; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.admin.core.util.JacksonUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * common exception resolver + * + * @author xuxueli 2016-1-6 19:22:18 + */ +@Component +public class WebExceptionResolver implements HandlerExceptionResolver { + private static transient Logger logger = LoggerFactory.getLogger(WebExceptionResolver.class); + + @Override + public ModelAndView resolveException(HttpServletRequest request, + HttpServletResponse response, Object handler, Exception ex) { + + if (!(ex instanceof XxlJobException)) { + logger.error("WebExceptionResolver:{}", ex); + } + + // if json + boolean isJson = false; + if (handler instanceof HandlerMethod) { + HandlerMethod method = (HandlerMethod)handler; + ResponseBody responseBody = method.getMethodAnnotation(ResponseBody.class); + if (responseBody != null) { + isJson = true; + } + } + + // error result + ReturnT errorResult = new ReturnT(ReturnT.FAIL_CODE, ex.toString().replaceAll("\n", "
")); + + // response + ModelAndView mv = new ModelAndView(); + if (isJson) { + try { + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print(JacksonUtil.writeValueAsString(errorResult)); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return mv; + } else { + + mv.addObject("exceptionMsg", errorResult.getMsg()); + mv.setViewName("/common/common.exception"); + return mv; + } + } + +} \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java new file mode 100644 index 00000000..4165ff3a --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarm.java @@ -0,0 +1,20 @@ +package com.xxl.job.admin.core.alarm; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; + +/** + * @author xuxueli 2020-01-19 + */ +public interface JobAlarm { + + /** + * job alarm + * + * @param info + * @param jobLog + * @return + */ + public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog); + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java new file mode 100644 index 00000000..797dc900 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/JobAlarmer.java @@ -0,0 +1,65 @@ +package com.xxl.job.admin.core.alarm; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class JobAlarmer implements ApplicationContextAware, InitializingBean { + private static Logger logger = LoggerFactory.getLogger(JobAlarmer.class); + + private ApplicationContext applicationContext; + private List jobAlarmList; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() throws Exception { + Map serviceBeanMap = applicationContext.getBeansOfType(JobAlarm.class); + if (serviceBeanMap != null && serviceBeanMap.size() > 0) { + jobAlarmList = new ArrayList(serviceBeanMap.values()); + } + } + + /** + * job alarm + * + * @param info + * @param jobLog + * @return + */ + public boolean alarm(XxlJobInfo info, XxlJobLog jobLog) { + + boolean result = false; + if (jobAlarmList!=null && jobAlarmList.size()>0) { + result = true; // success means all-success + for (JobAlarm alarm: jobAlarmList) { + boolean resultItem = false; + try { + resultItem = alarm.doAlarm(info, jobLog); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + if (!resultItem) { + result = false; + } + } + } + + return result; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java new file mode 100644 index 00000000..16e52184 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/alarm/impl/EmailJobAlarm.java @@ -0,0 +1,118 @@ +package com.xxl.job.admin.core.alarm.impl; + +import com.xxl.job.admin.core.alarm.JobAlarm; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.ReturnT; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +import javax.mail.internet.MimeMessage; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * job alarm by email + * + * @author xuxueli 2020-01-19 + */ +@Component +public class EmailJobAlarm implements JobAlarm { + private static Logger logger = LoggerFactory.getLogger(EmailJobAlarm.class); + + /** + * fail alarm + * + * @param jobLog + */ + @Override + public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog){ + boolean alarmResult = true; + + // send monitor email + if (info!=null && info.getAlarmEmail()!=null && info.getAlarmEmail().trim().length()>0) { + + // alarmContent + String alarmContent = "Alarm Job LogId=" + jobLog.getId(); + if (jobLog.getTriggerCode() != ReturnT.SUCCESS_CODE) { + alarmContent += "
TriggerMsg=
" + jobLog.getTriggerMsg(); + } + if (jobLog.getHandleCode()>0 && jobLog.getHandleCode() != ReturnT.SUCCESS_CODE) { + alarmContent += "
HandleCode=" + jobLog.getHandleMsg(); + } + + // email info + XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(Integer.valueOf(info.getJobGroup())); + String personal = I18nUtil.getString("admin_name_full"); + String title = I18nUtil.getString("jobconf_monitor"); + String content = MessageFormat.format(loadEmailJobAlarmTemplate(), + group!=null?group.getTitle():"null", + info.getId(), + info.getJobDesc(), + alarmContent); + + Set emailSet = new HashSet(Arrays.asList(info.getAlarmEmail().split(","))); + for (String email: emailSet) { + + // make mail + try { + MimeMessage mimeMessage = XxlJobAdminConfig.getAdminConfig().getMailSender().createMimeMessage(); + + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); + helper.setFrom(XxlJobAdminConfig.getAdminConfig().getEmailFrom(), personal); + helper.setTo(email); + helper.setSubject(title); + helper.setText(content, true); + + XxlJobAdminConfig.getAdminConfig().getMailSender().send(mimeMessage); + } catch (Exception e) { + logger.error(">>>>>>>>>>> xxl-job, job fail alarm email send error, JobLogId:{}", jobLog.getId(), e); + + alarmResult = false; + } + + } + } + + return alarmResult; + } + + /** + * load email job alarm template + * + * @return + */ + private static final String loadEmailJobAlarmTemplate(){ + String mailBodyTemplate = "
" + I18nUtil.getString("jobconf_monitor_detail") + ":" + + "\n" + + " " + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
"+ I18nUtil.getString("jobinfo_field_jobgroup") +""+ I18nUtil.getString("jobinfo_field_id") +""+ I18nUtil.getString("jobinfo_field_jobdesc") +""+ I18nUtil.getString("jobconf_monitor_alarm_title") +""+ I18nUtil.getString("jobconf_monitor_alarm_content") +"
{0}{1}{2}"+ I18nUtil.getString("jobconf_monitor_alarm_type") +"{3}
"; + + return mailBodyTemplate; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java new file mode 100644 index 00000000..279ad7d1 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java @@ -0,0 +1,99 @@ +package com.xxl.job.admin.core.complete; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.context.XxlJobContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.MessageFormat; + +/** + * @author xuxueli 2020-10-30 20:43:10 + */ +public class XxlJobCompleter { + private static Logger logger = LoggerFactory.getLogger(XxlJobCompleter.class); + + /** + * common fresh handle entrance (limit only once) + * + * @param xxlJobLog + * @return + */ + public static int updateHandleInfoAndFinish(XxlJobLog xxlJobLog) { + + // finish + finishJob(xxlJobLog); + + // text最大64kb 避免长度过长 + if (xxlJobLog.getHandleMsg().length() > 15000) { + xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg().substring(0, 15000) ); + } + + // fresh handle + return XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateHandleInfo(xxlJobLog); + } + + + /** + * do somethind to finish job + */ + private static void finishJob(XxlJobLog xxlJobLog){ + + // 1、handle success, to trigger child job + String triggerChildMsg = null; + if (XxlJobContext.HANDLE_CODE_SUCCESS == xxlJobLog.getHandleCode()) { + XxlJobInfo xxlJobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(xxlJobLog.getJobId()); + if (xxlJobInfo!=null && xxlJobInfo.getChildJobId()!=null && xxlJobInfo.getChildJobId().trim().length()>0) { + triggerChildMsg = "

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_child_run") +"<<<<<<<<<<<
"; + + String[] childJobIds = xxlJobInfo.getChildJobId().split(","); + for (int i = 0; i < childJobIds.length; i++) { + int childJobId = (childJobIds[i]!=null && childJobIds[i].trim().length()>0 && isNumeric(childJobIds[i]))?Integer.valueOf(childJobIds[i]):-1; + if (childJobId > 0) { + + JobTriggerPoolHelper.trigger(childJobId, TriggerTypeEnum.PARENT, -1, null, null, null); + ReturnT triggerChildResult = ReturnT.SUCCESS; + + // add msg + triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg1"), + (i+1), + childJobIds.length, + childJobIds[i], + (triggerChildResult.getCode()==ReturnT.SUCCESS_CODE?I18nUtil.getString("system_success"):I18nUtil.getString("system_fail")), + triggerChildResult.getMsg()); + } else { + triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg2"), + (i+1), + childJobIds.length, + childJobIds[i]); + } + } + + } + } + + if (triggerChildMsg != null) { + xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg() + triggerChildMsg ); + } + + // 2、fix_delay trigger next + // on the way + + } + + private static boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java new file mode 100644 index 00000000..380b8a59 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java @@ -0,0 +1,158 @@ +package com.xxl.job.admin.core.conf; + +import com.xxl.job.admin.core.alarm.JobAlarmer; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.dao.*; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.sql.DataSource; +import java.util.Arrays; + +/** + * xxl-job config + * + * @author xuxueli 2017-04-28 + */ + +@Component +public class XxlJobAdminConfig implements InitializingBean, DisposableBean { + + private static XxlJobAdminConfig adminConfig = null; + public static XxlJobAdminConfig getAdminConfig() { + return adminConfig; + } + + + // ---------------------- XxlJobScheduler ---------------------- + + private XxlJobScheduler xxlJobScheduler; + + @Override + public void afterPropertiesSet() throws Exception { + adminConfig = this; + + xxlJobScheduler = new XxlJobScheduler(); + xxlJobScheduler.init(); + } + + @Override + public void destroy() throws Exception { + xxlJobScheduler.destroy(); + } + + + // ---------------------- XxlJobScheduler ---------------------- + + // conf + @Value("${xxl.job.i18n}") + private String i18n; + + @Value("${xxl.job.accessToken}") + private String accessToken; + + @Value("${spring.mail.from}") + private String emailFrom; + + @Value("${xxl.job.triggerpool.fast.max}") + private int triggerPoolFastMax; + + @Value("${xxl.job.triggerpool.slow.max}") + private int triggerPoolSlowMax; + + @Value("${xxl.job.logretentiondays}") + private int logretentiondays; + + // dao, service + + @Resource + private XxlJobLogDao xxlJobLogDao; + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + private XxlJobRegistryDao xxlJobRegistryDao; + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobLogReportDao xxlJobLogReportDao; + @Resource + private JavaMailSender mailSender; + @Resource + private DataSource dataSource; + @Resource + private JobAlarmer jobAlarmer; + + + public String getI18n() { + if (!Arrays.asList("zh_CN", "zh_TC", "en").contains(i18n)) { + return "zh_CN"; + } + return i18n; + } + + public String getAccessToken() { + return accessToken; + } + + public String getEmailFrom() { + return emailFrom; + } + + public int getTriggerPoolFastMax() { + if (triggerPoolFastMax < 200) { + return 200; + } + return triggerPoolFastMax; + } + + public int getTriggerPoolSlowMax() { + if (triggerPoolSlowMax < 100) { + return 100; + } + return triggerPoolSlowMax; + } + + public int getLogretentiondays() { + if (logretentiondays < 7) { + return -1; // Limit greater than or equal to 7, otherwise close + } + return logretentiondays; + } + + public XxlJobLogDao getXxlJobLogDao() { + return xxlJobLogDao; + } + + public XxlJobInfoDao getXxlJobInfoDao() { + return xxlJobInfoDao; + } + + public XxlJobRegistryDao getXxlJobRegistryDao() { + return xxlJobRegistryDao; + } + + public XxlJobGroupDao getXxlJobGroupDao() { + return xxlJobGroupDao; + } + + public XxlJobLogReportDao getXxlJobLogReportDao() { + return xxlJobLogReportDao; + } + + public JavaMailSender getMailSender() { + return mailSender; + } + + public DataSource getDataSource() { + return dataSource; + } + + public JobAlarmer getJobAlarmer() { + return jobAlarmer; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java new file mode 100644 index 00000000..fce23524 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java @@ -0,0 +1,1666 @@ +/* + * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. + * + * 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 + * + * http://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 com.xxl.job.admin.core.cron; + +import java.io.Serializable; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.SortedSet; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.TreeSet; + +/** + * Provides a parser and evaluator for unix-like cron expressions. Cron + * expressions provide the ability to specify complex time combinations such as + * "At 8:00am every Monday through Friday" or "At 1:30am every + * last Friday of the month". + *

+ * Cron expressions are comprised of 6 required fields and one optional field + * separated by white space. The fields respectively are described as follows: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Field Name Allowed Values Allowed Special Characters
Seconds  + * 0-59  + * , - * /
Minutes  + * 0-59  + * , - * /
Hours  + * 0-23  + * , - * /
Day-of-month  + * 1-31  + * , - * ? / L W
Month  + * 0-11 or JAN-DEC  + * , - * /
Day-of-Week  + * 1-7 or SUN-SAT  + * , - * ? / L #
Year (Optional)  + * empty, 1970-2199  + * , - * /
+ *

+ * The '*' character is used to specify all values. For example, "*" + * in the minute field means "every minute". + *

+ * The '?' character is allowed for the day-of-month and day-of-week fields. It + * is used to specify 'no specific value'. This is useful when you need to + * specify something in one of the two fields, but not the other. + *

+ * The '-' character is used to specify ranges For example "10-12" in + * the hour field means "the hours 10, 11 and 12". + *

+ * The ',' character is used to specify additional values. For example + * "MON,WED,FRI" in the day-of-week field means "the days Monday, + * Wednesday, and Friday". + *

+ * The '/' character is used to specify increments. For example "0/15" + * in the seconds field means "the seconds 0, 15, 30, and 45". And + * "5/15" in the seconds field means "the seconds 5, 20, 35, and + * 50". Specifying '*' before the '/' is equivalent to specifying 0 is + * the value to start with. Essentially, for each field in the expression, there + * is a set of numbers that can be turned on or off. For seconds and minutes, + * the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to + * 31, and for months 0 to 11 (JAN to DEC). The "/" character simply helps you turn + * on every "nth" value in the given set. Thus "7/6" in the + * month field only turns on month "7", it does NOT mean every 6th + * month, please note that subtlety. + *

+ * The 'L' character is allowed for the day-of-month and day-of-week fields. + * This character is short-hand for "last", but it has different + * meaning in each of the two fields. For example, the value "L" in + * the day-of-month field means "the last day of the month" - day 31 + * for January, day 28 for February on non-leap years. If used in the + * day-of-week field by itself, it simply means "7" or + * "SAT". But if used in the day-of-week field after another value, it + * means "the last xxx day of the month" - for example "6L" + * means "the last friday of the month". You can also specify an offset + * from the last day of the month, such as "L-3" which would mean the third-to-last + * day of the calendar month. When using the 'L' option, it is important not to + * specify lists, or ranges of values, as you'll get confusing/unexpected results. + *

+ * The 'W' character is allowed for the day-of-month field. This character + * is used to specify the weekday (Monday-Friday) nearest the given day. As an + * example, if you were to specify "15W" as the value for the + * day-of-month field, the meaning is: "the nearest weekday to the 15th of + * the month". So if the 15th is a Saturday, the trigger will fire on + * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the + * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th. + * However if you specify "1W" as the value for day-of-month, and the + * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not + * 'jump' over the boundary of a month's days. The 'W' character can only be + * specified when the day-of-month is a single day, not a range or list of days. + *

+ * The 'L' and 'W' characters can also be combined for the day-of-month + * expression to yield 'LW', which translates to "last weekday of the + * month". + *

+ * The '#' character is allowed for the day-of-week field. This character is + * used to specify "the nth" XXX day of the month. For example, the + * value of "6#3" in the day-of-week field means the third Friday of + * the month (day 6 = Friday and "#3" = the 3rd one in the month). + * Other examples: "2#1" = the first Monday of the month and + * "4#5" = the fifth Wednesday of the month. Note that if you specify + * "#5" and there is not 5 of the given day-of-week in the month, then + * no firing will occur that month. If the '#' character is used, there can + * only be one expression in the day-of-week field ("3#1,6#3" is + * not valid, since there are two expressions). + *

+ * + *

+ * The legal characters and the names of months and days of the week are not + * case sensitive. + * + *

+ * NOTES: + *

    + *
  • Support for specifying both a day-of-week and a day-of-month value is + * not complete (you'll need to use the '?' character in one of these fields). + *
  • + *
  • Overflowing ranges is supported - that is, having a larger number on + * the left hand side than the right. You might do 22-2 to catch 10 o'clock + * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is + * very important to note that overuse of overflowing ranges creates ranges + * that don't make sense and no effort has been made to determine which + * interpretation CronExpression chooses. An example would be + * "0 0 14-6 ? * FRI-MON".
  • + *
+ *

+ * + * + * @author Sharada Jambula, James House + * @author Contributions from Mads Henderson + * @author Refactoring from CronTrigger to CronExpression by Aaron Craven + * + * Borrowed from quartz v2.3.1 + * + */ +public final class CronExpression implements Serializable, Cloneable { + + private static final long serialVersionUID = 12423409423L; + + protected static final int SECOND = 0; + protected static final int MINUTE = 1; + protected static final int HOUR = 2; + protected static final int DAY_OF_MONTH = 3; + protected static final int MONTH = 4; + protected static final int DAY_OF_WEEK = 5; + protected static final int YEAR = 6; + protected static final int ALL_SPEC_INT = 99; // '*' + protected static final int NO_SPEC_INT = 98; // '?' + protected static final Integer ALL_SPEC = ALL_SPEC_INT; + protected static final Integer NO_SPEC = NO_SPEC_INT; + + protected static final Map monthMap = new HashMap(20); + protected static final Map dayMap = new HashMap(60); + static { + monthMap.put("JAN", 0); + monthMap.put("FEB", 1); + monthMap.put("MAR", 2); + monthMap.put("APR", 3); + monthMap.put("MAY", 4); + monthMap.put("JUN", 5); + monthMap.put("JUL", 6); + monthMap.put("AUG", 7); + monthMap.put("SEP", 8); + monthMap.put("OCT", 9); + monthMap.put("NOV", 10); + monthMap.put("DEC", 11); + + dayMap.put("SUN", 1); + dayMap.put("MON", 2); + dayMap.put("TUE", 3); + dayMap.put("WED", 4); + dayMap.put("THU", 5); + dayMap.put("FRI", 6); + dayMap.put("SAT", 7); + } + + private final String cronExpression; + private TimeZone timeZone = null; + protected transient TreeSet seconds; + protected transient TreeSet minutes; + protected transient TreeSet hours; + protected transient TreeSet daysOfMonth; + protected transient TreeSet months; + protected transient TreeSet daysOfWeek; + protected transient TreeSet years; + + protected transient boolean lastdayOfWeek = false; + protected transient int nthdayOfWeek = 0; + protected transient boolean lastdayOfMonth = false; + protected transient boolean nearestWeekday = false; + protected transient int lastdayOffset = 0; + protected transient boolean expressionParsed = false; + + public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; + + /** + * Constructs a new CronExpression based on the specified + * parameter. + * + * @param cronExpression String representation of the cron expression the + * new object should represent + * @throws java.text.ParseException + * if the string expression cannot be parsed into a valid + * CronExpression + */ + public CronExpression(String cronExpression) throws ParseException { + if (cronExpression == null) { + throw new IllegalArgumentException("cronExpression cannot be null"); + } + + this.cronExpression = cronExpression.toUpperCase(Locale.US); + + buildExpression(this.cronExpression); + } + + /** + * Constructs a new {@code CronExpression} as a copy of an existing + * instance. + * + * @param expression + * The existing cron expression to be copied + */ + public CronExpression(CronExpression expression) { + /* + * We don't call the other constructor here since we need to swallow the + * ParseException. We also elide some of the sanity checking as it is + * not logically trippable. + */ + this.cronExpression = expression.getCronExpression(); + try { + buildExpression(cronExpression); + } catch (ParseException ex) { + throw new AssertionError(); + } + if (expression.getTimeZone() != null) { + setTimeZone((TimeZone) expression.getTimeZone().clone()); + } + } + + /** + * Indicates whether the given date satisfies the cron expression. Note that + * milliseconds are ignored, so two Dates falling on different milliseconds + * of the same second will always have the same result here. + * + * @param date the date to evaluate + * @return a boolean indicating whether the given date satisfies the cron + * expression + */ + public boolean isSatisfiedBy(Date date) { + Calendar testDateCal = Calendar.getInstance(getTimeZone()); + testDateCal.setTime(date); + testDateCal.set(Calendar.MILLISECOND, 0); + Date originalDate = testDateCal.getTime(); + + testDateCal.add(Calendar.SECOND, -1); + + Date timeAfter = getTimeAfter(testDateCal.getTime()); + + return ((timeAfter != null) && (timeAfter.equals(originalDate))); + } + + /** + * Returns the next date/time after the given date/time which + * satisfies the cron expression. + * + * @param date the date/time at which to begin the search for the next valid + * date/time + * @return the next valid date/time + */ + public Date getNextValidTimeAfter(Date date) { + return getTimeAfter(date); + } + + /** + * Returns the next date/time after the given date/time which does + * not satisfy the expression + * + * @param date the date/time at which to begin the search for the next + * invalid date/time + * @return the next valid date/time + */ + public Date getNextInvalidTimeAfter(Date date) { + long difference = 1000; + + //move back to the nearest second so differences will be accurate + Calendar adjustCal = Calendar.getInstance(getTimeZone()); + adjustCal.setTime(date); + adjustCal.set(Calendar.MILLISECOND, 0); + Date lastDate = adjustCal.getTime(); + + Date newDate; + + //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution. + + //keep getting the next included time until it's farther than one second + // apart. At that point, lastDate is the last valid fire time. We return + // the second immediately following it. + while (difference == 1000) { + newDate = getTimeAfter(lastDate); + if(newDate == null) + break; + + difference = newDate.getTime() - lastDate.getTime(); + + if (difference == 1000) { + lastDate = newDate; + } + } + + return new Date(lastDate.getTime() + 1000); + } + + /** + * Returns the time zone for which this CronExpression + * will be resolved. + */ + public TimeZone getTimeZone() { + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } + + return timeZone; + } + + /** + * Sets the time zone for which this CronExpression + * will be resolved. + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Returns the string representation of the CronExpression + * + * @return a string representation of the CronExpression + */ + @Override + public String toString() { + return cronExpression; + } + + /** + * Indicates whether the specified cron expression can be parsed into a + * valid cron expression + * + * @param cronExpression the expression to evaluate + * @return a boolean indicating whether the given expression is a valid cron + * expression + */ + public static boolean isValidExpression(String cronExpression) { + + try { + new CronExpression(cronExpression); + } catch (ParseException pe) { + return false; + } + + return true; + } + + public static void validateExpression(String cronExpression) throws ParseException { + + new CronExpression(cronExpression); + } + + + //////////////////////////////////////////////////////////////////////////// + // + // Expression Parsing Functions + // + //////////////////////////////////////////////////////////////////////////// + + protected void buildExpression(String expression) throws ParseException { + expressionParsed = true; + + try { + + if (seconds == null) { + seconds = new TreeSet(); + } + if (minutes == null) { + minutes = new TreeSet(); + } + if (hours == null) { + hours = new TreeSet(); + } + if (daysOfMonth == null) { + daysOfMonth = new TreeSet(); + } + if (months == null) { + months = new TreeSet(); + } + if (daysOfWeek == null) { + daysOfWeek = new TreeSet(); + } + if (years == null) { + years = new TreeSet(); + } + + int exprOn = SECOND; + + StringTokenizer exprsTok = new StringTokenizer(expression, " \t", + false); + + while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { + String expr = exprsTok.nextToken().trim(); + + // throw an exception if L is used with other days of the month + if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); + } + // throw an exception if L is used with other days of the week + if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { + throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); + } + if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) { + throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); + } + + StringTokenizer vTok = new StringTokenizer(expr, ","); + while (vTok.hasMoreTokens()) { + String v = vTok.nextToken(); + storeExpressionVals(0, v, exprOn); + } + + exprOn++; + } + + if (exprOn <= DAY_OF_WEEK) { + throw new ParseException("Unexpected end of expression.", + expression.length()); + } + + if (exprOn <= YEAR) { + storeExpressionVals(0, "*", YEAR); + } + + TreeSet dow = getSet(DAY_OF_WEEK); + TreeSet dom = getSet(DAY_OF_MONTH); + + // Copying the logic from the UnsupportedOperationException below + boolean dayOfMSpec = !dom.contains(NO_SPEC); + boolean dayOfWSpec = !dow.contains(NO_SPEC); + + if (!dayOfMSpec || dayOfWSpec) { + if (!dayOfWSpec || dayOfMSpec) { + throw new ParseException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); + } + } + } catch (ParseException pe) { + throw pe; + } catch (Exception e) { + throw new ParseException("Illegal cron expression format (" + + e.toString() + ")", 0); + } + } + + protected int storeExpressionVals(int pos, String s, int type) + throws ParseException { + + int incr = 0; + int i = skipWhiteSpace(pos, s); + if (i >= s.length()) { + return i; + } + char c = s.charAt(i); + if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { + String sub = s.substring(i, i + 3); + int sval = -1; + int eval = -1; + if (type == MONTH) { + sval = getMonthNumber(sub) + 1; + if (sval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getMonthNumber(sub) + 1; + if (eval <= 0) { + throw new ParseException("Invalid Month value: '" + sub + "'", i); + } + } + } + } else if (type == DAY_OF_WEEK) { + sval = getDayOfWeekNumber(sub); + if (sval < 0) { + throw new ParseException("Invalid Day-of-Week value: '" + + sub + "'", i); + } + if (s.length() > i + 3) { + c = s.charAt(i + 3); + if (c == '-') { + i += 4; + sub = s.substring(i, i + 3); + eval = getDayOfWeekNumber(sub); + if (eval < 0) { + throw new ParseException( + "Invalid Day-of-Week value: '" + sub + + "'", i); + } + } else if (c == '#') { + try { + i += 4; + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + } else if (c == 'L') { + lastdayOfWeek = true; + i++; + } + } + + } else { + throw new ParseException( + "Illegal characters for this position: '" + sub + "'", + i); + } + if (eval != -1) { + incr = 1; + } + addToSet(sval, eval, incr, type); + return (i + 3); + } + + if (c == '?') { + i++; + if ((i + 1) < s.length() + && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { + throw new ParseException("Illegal character after '?': " + + s.charAt(i), i); + } + if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { + throw new ParseException( + "'?' can only be specified for Day-of-Month or Day-of-Week.", + i); + } + if (type == DAY_OF_WEEK && !lastdayOfMonth) { + int val = daysOfMonth.last(); + if (val == NO_SPEC_INT) { + throw new ParseException( + "'?' can only be specified for Day-of-Month -OR- Day-of-Week.", + i); + } + } + + addToSet(NO_SPEC_INT, -1, 0, type); + return i; + } + + if (c == '*' || c == '/') { + if (c == '*' && (i + 1) >= s.length()) { + addToSet(ALL_SPEC_INT, -1, incr, type); + return i + 1; + } else if (c == '/' + && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s + .charAt(i + 1) == '\t')) { + throw new ParseException("'/' must be followed by an integer.", i); + } else if (c == '*') { + i++; + } + c = s.charAt(i); + if (c == '/') { // is an increment specified? + i++; + if (i >= s.length()) { + throw new ParseException("Unexpected end of string.", i); + } + + incr = getNumericValue(s, i); + + i++; + if (incr > 10) { + i++; + } + checkIncrementRange(incr, type, i); + } else { + incr = 1; + } + + addToSet(ALL_SPEC_INT, -1, incr, type); + return i; + } else if (c == 'L') { + i++; + if (type == DAY_OF_MONTH) { + lastdayOfMonth = true; + } + if (type == DAY_OF_WEEK) { + addToSet(7, 7, 0, type); + } + if(type == DAY_OF_MONTH && s.length() > i) { + c = s.charAt(i); + if(c == '-') { + ValueSet vs = getValue(0, s, i+1); + lastdayOffset = vs.value; + if(lastdayOffset > 30) + throw new ParseException("Offset from last day must be <= 30", i+1); + i = vs.pos; + } + if(s.length() > i) { + c = s.charAt(i); + if(c == 'W') { + nearestWeekday = true; + i++; + } + } + } + return i; + } else if (c >= '0' && c <= '9') { + int val = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, -1, -1, type); + } else { + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(val, s, i); + val = vs.value; + i = vs.pos; + } + i = checkNext(i, s, val, type); + return i; + } + } else { + throw new ParseException("Unexpected character: " + c, i); + } + + return i; + } + + private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException { + if (incr > 59 && (type == SECOND || type == MINUTE)) { + throw new ParseException("Increment > 60 : " + incr, idxPos); + } else if (incr > 23 && (type == HOUR)) { + throw new ParseException("Increment > 24 : " + incr, idxPos); + } else if (incr > 31 && (type == DAY_OF_MONTH)) { + throw new ParseException("Increment > 31 : " + incr, idxPos); + } else if (incr > 7 && (type == DAY_OF_WEEK)) { + throw new ParseException("Increment > 7 : " + incr, idxPos); + } else if (incr > 12 && (type == MONTH)) { + throw new ParseException("Increment > 12 : " + incr, idxPos); + } + } + + protected int checkNext(int pos, String s, int val, int type) + throws ParseException { + + int end = -1; + int i = pos; + + if (i >= s.length()) { + addToSet(val, end, -1, type); + return i; + } + + char c = s.charAt(pos); + + if (c == 'L') { + if (type == DAY_OF_WEEK) { + if(val < 1 || val > 7) + throw new ParseException("Day-of-Week values must be between 1 and 7", -1); + lastdayOfWeek = true; + } else { + throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); + } + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == 'W') { + if (type == DAY_OF_MONTH) { + nearestWeekday = true; + } else { + throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); + } + if(val > 31) + throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '#') { + if (type != DAY_OF_WEEK) { + throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); + } + i++; + try { + nthdayOfWeek = Integer.parseInt(s.substring(i)); + if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { + throw new Exception(); + } + } catch (Exception e) { + throw new ParseException( + "A numeric value between 1 and 5 must follow the '#' option", + i); + } + + TreeSet set = getSet(type); + set.add(val); + i++; + return i; + } + + if (c == '-') { + i++; + c = s.charAt(i); + int v = Integer.parseInt(String.valueOf(c)); + end = v; + i++; + if (i >= s.length()) { + addToSet(val, end, 1, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v, s, i); + end = vs.value; + i = vs.pos; + } + if (i < s.length() && ((c = s.charAt(i)) == '/')) { + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + addToSet(val, end, v2, type); + return i; + } + } else { + addToSet(val, end, 1, type); + return i; + } + } + + if (c == '/') { + if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t') { + throw new ParseException("'/' must be followed by an integer.", i); + } + + i++; + c = s.charAt(i); + int v2 = Integer.parseInt(String.valueOf(c)); + i++; + if (i >= s.length()) { + checkIncrementRange(v2, type, i); + addToSet(val, end, v2, type); + return i; + } + c = s.charAt(i); + if (c >= '0' && c <= '9') { + ValueSet vs = getValue(v2, s, i); + int v3 = vs.value; + checkIncrementRange(v3, type, i); + addToSet(val, end, v3, type); + i = vs.pos; + return i; + } else { + throw new ParseException("Unexpected character '" + c + "' after '/'", i); + } + } + + addToSet(val, end, 0, type); + i++; + return i; + } + + public String getCronExpression() { + return cronExpression; + } + + public String getExpressionSummary() { + StringBuilder buf = new StringBuilder(); + + buf.append("seconds: "); + buf.append(getExpressionSetSummary(seconds)); + buf.append("\n"); + buf.append("minutes: "); + buf.append(getExpressionSetSummary(minutes)); + buf.append("\n"); + buf.append("hours: "); + buf.append(getExpressionSetSummary(hours)); + buf.append("\n"); + buf.append("daysOfMonth: "); + buf.append(getExpressionSetSummary(daysOfMonth)); + buf.append("\n"); + buf.append("months: "); + buf.append(getExpressionSetSummary(months)); + buf.append("\n"); + buf.append("daysOfWeek: "); + buf.append(getExpressionSetSummary(daysOfWeek)); + buf.append("\n"); + buf.append("lastdayOfWeek: "); + buf.append(lastdayOfWeek); + buf.append("\n"); + buf.append("nearestWeekday: "); + buf.append(nearestWeekday); + buf.append("\n"); + buf.append("NthDayOfWeek: "); + buf.append(nthdayOfWeek); + buf.append("\n"); + buf.append("lastdayOfMonth: "); + buf.append(lastdayOfMonth); + buf.append("\n"); + buf.append("years: "); + buf.append(getExpressionSetSummary(years)); + buf.append("\n"); + + return buf.toString(); + } + + protected String getExpressionSetSummary(java.util.Set set) { + + if (set.contains(NO_SPEC)) { + return "?"; + } + if (set.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator itr = set.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + protected String getExpressionSetSummary(java.util.ArrayList list) { + + if (list.contains(NO_SPEC)) { + return "?"; + } + if (list.contains(ALL_SPEC)) { + return "*"; + } + + StringBuilder buf = new StringBuilder(); + + Iterator itr = list.iterator(); + boolean first = true; + while (itr.hasNext()) { + Integer iVal = itr.next(); + String val = iVal.toString(); + if (!first) { + buf.append(","); + } + buf.append(val); + first = false; + } + + return buf.toString(); + } + + protected int skipWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) { + } + + return i; + } + + protected int findNextWhiteSpace(int i, String s) { + for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) { + } + + return i; + } + + protected void addToSet(int val, int end, int incr, int type) + throws ParseException { + + TreeSet set = getSet(type); + + if (type == SECOND || type == MINUTE) { + if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Minute and Second values must be between 0 and 59", + -1); + } + } else if (type == HOUR) { + if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Hour values must be between 0 and 23", -1); + } + } else if (type == DAY_OF_MONTH) { + if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day of month values must be between 1 and 31", -1); + } + } else if (type == MONTH) { + if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) { + throw new ParseException( + "Month values must be between 1 and 12", -1); + } + } else if (type == DAY_OF_WEEK) { + if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) + && (val != NO_SPEC_INT)) { + throw new ParseException( + "Day-of-Week values must be between 1 and 7", -1); + } + } + + if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) { + if (val != -1) { + set.add(val); + } else { + set.add(NO_SPEC); + } + + return; + } + + int startAt = val; + int stopAt = end; + + if (val == ALL_SPEC_INT && incr <= 0) { + incr = 1; + set.add(ALL_SPEC); // put in a marker, but also fill values + } + + if (type == SECOND || type == MINUTE) { + if (stopAt == -1) { + stopAt = 59; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == HOUR) { + if (stopAt == -1) { + stopAt = 23; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 0; + } + } else if (type == DAY_OF_MONTH) { + if (stopAt == -1) { + stopAt = 31; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == MONTH) { + if (stopAt == -1) { + stopAt = 12; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == DAY_OF_WEEK) { + if (stopAt == -1) { + stopAt = 7; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1; + } + } else if (type == YEAR) { + if (stopAt == -1) { + stopAt = MAX_YEAR; + } + if (startAt == -1 || startAt == ALL_SPEC_INT) { + startAt = 1970; + } + } + + // if the end of the range is before the start, then we need to overflow into + // the next day, month etc. This is done by adding the maximum amount for that + // type, and using modulus max to determine the value being added. + int max = -1; + if (stopAt < startAt) { + switch (type) { + case SECOND : max = 60; break; + case MINUTE : max = 60; break; + case HOUR : max = 24; break; + case MONTH : max = 12; break; + case DAY_OF_WEEK : max = 7; break; + case DAY_OF_MONTH : max = 31; break; + case YEAR : throw new IllegalArgumentException("Start year must be less than stop year"); + default : throw new IllegalArgumentException("Unexpected type encountered"); + } + stopAt += max; + } + + for (int i = startAt; i <= stopAt; i += incr) { + if (max == -1) { + // ie: there's no max to overflow over + set.add(i); + } else { + // take the modulus to get the real value + int i2 = i % max; + + // 1-indexed ranges should not include 0, and should include their max + if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) { + i2 = max; + } + + set.add(i2); + } + } + } + + TreeSet getSet(int type) { + switch (type) { + case SECOND: + return seconds; + case MINUTE: + return minutes; + case HOUR: + return hours; + case DAY_OF_MONTH: + return daysOfMonth; + case MONTH: + return months; + case DAY_OF_WEEK: + return daysOfWeek; + case YEAR: + return years; + default: + return null; + } + } + + protected ValueSet getValue(int v, String s, int i) { + char c = s.charAt(i); + StringBuilder s1 = new StringBuilder(String.valueOf(v)); + while (c >= '0' && c <= '9') { + s1.append(c); + i++; + if (i >= s.length()) { + break; + } + c = s.charAt(i); + } + ValueSet val = new ValueSet(); + + val.pos = (i < s.length()) ? i : i + 1; + val.value = Integer.parseInt(s1.toString()); + return val; + } + + protected int getNumericValue(String s, int i) { + int endOfVal = findNextWhiteSpace(i, s); + String val = s.substring(i, endOfVal); + return Integer.parseInt(val); + } + + protected int getMonthNumber(String s) { + Integer integer = monthMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + protected int getDayOfWeekNumber(String s) { + Integer integer = dayMap.get(s); + + if (integer == null) { + return -1; + } + + return integer; + } + + //////////////////////////////////////////////////////////////////////////// + // + // Computation Functions + // + //////////////////////////////////////////////////////////////////////////// + + public Date getTimeAfter(Date afterTime) { + + // Computation is based on Gregorian year only. + Calendar cl = new java.util.GregorianCalendar(getTimeZone()); + + // move ahead one second, since we're computing the time *after* the + // given time + afterTime = new Date(afterTime.getTime() + 1000); + // CronTrigger does not deal with milliseconds + cl.setTime(afterTime); + cl.set(Calendar.MILLISECOND, 0); + + boolean gotOne = false; + // loop until we've computed the next time, or we've past the endTime + while (!gotOne) { + + //if (endTime != null && cl.getTime().after(endTime)) return null; + if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop... + return null; + } + + SortedSet st = null; + int t = 0; + + int sec = cl.get(Calendar.SECOND); + int min = cl.get(Calendar.MINUTE); + + // get second................................................. + st = seconds.tailSet(sec); + if (st != null && st.size() != 0) { + sec = st.first(); + } else { + sec = seconds.first(); + min++; + cl.set(Calendar.MINUTE, min); + } + cl.set(Calendar.SECOND, sec); + + min = cl.get(Calendar.MINUTE); + int hr = cl.get(Calendar.HOUR_OF_DAY); + t = -1; + + // get minute................................................. + st = minutes.tailSet(min); + if (st != null && st.size() != 0) { + t = min; + min = st.first(); + } else { + min = minutes.first(); + hr++; + } + if (min != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, min); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.MINUTE, min); + + hr = cl.get(Calendar.HOUR_OF_DAY); + int day = cl.get(Calendar.DAY_OF_MONTH); + t = -1; + + // get hour................................................... + st = hours.tailSet(hr); + if (st != null && st.size() != 0) { + t = hr; + hr = st.first(); + } else { + hr = hours.first(); + day++; + } + if (hr != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + setCalendarHour(cl, hr); + continue; + } + cl.set(Calendar.HOUR_OF_DAY, hr); + + day = cl.get(Calendar.DAY_OF_MONTH); + int mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + t = -1; + int tmon = mon; + + // get day................................................... + boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); + boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); + if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule + st = daysOfMonth.tailSet(day); + if (lastdayOfMonth) { + if(!nearestWeekday) { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + if(t > day) { + mon++; + if(mon > 12) { + mon = 1; + tmon = 3333; // ensure test of mon != tmon further below fails + cl.add(Calendar.YEAR, 1); + } + day = 1; + } + } else { + t = day; + day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + day -= lastdayOffset; + + java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if(dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if(dow == Calendar.SATURDAY) { + day -= 1; + } else if(dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if(dow == Calendar.SUNDAY) { + day += 1; + } + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if(nTime.before(afterTime)) { + day = 1; + mon++; + } + } + } else if(nearestWeekday) { + t = day; + day = daysOfMonth.first(); + + java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone()); + tcal.set(Calendar.SECOND, 0); + tcal.set(Calendar.MINUTE, 0); + tcal.set(Calendar.HOUR_OF_DAY, 0); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); + + int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + int dow = tcal.get(Calendar.DAY_OF_WEEK); + + if(dow == Calendar.SATURDAY && day == 1) { + day += 2; + } else if(dow == Calendar.SATURDAY) { + day -= 1; + } else if(dow == Calendar.SUNDAY && day == ldom) { + day -= 2; + } else if(dow == Calendar.SUNDAY) { + day += 1; + } + + + tcal.set(Calendar.SECOND, sec); + tcal.set(Calendar.MINUTE, min); + tcal.set(Calendar.HOUR_OF_DAY, hr); + tcal.set(Calendar.DAY_OF_MONTH, day); + tcal.set(Calendar.MONTH, mon - 1); + Date nTime = tcal.getTime(); + if(nTime.before(afterTime)) { + day = daysOfMonth.first(); + mon++; + } + } else if (st != null && st.size() != 0) { + t = day; + day = st.first(); + // make sure we don't over-run a short month, such as february + int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + if (day > lastDay) { + day = daysOfMonth.first(); + mon++; + } + } else { + day = daysOfMonth.first(); + mon++; + } + + if (day != t || mon != tmon) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we + // are 1-based + continue; + } + } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule + if (lastdayOfWeek) { // are we looking for the last XXX day of + // the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // did we already miss the + // last one? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } + + // find date of last occurrence of this day in this month... + while ((day + daysToAdd + 7) <= lDay) { + daysToAdd += 7; + } + + day += daysToAdd; + + if (daysToAdd > 0) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are not promoting the month + continue; + } + + } else if (nthdayOfWeek != 0) { + // are we looking for the Nth XXX day in the month? + int dow = daysOfWeek.first(); // desired + // d-o-w + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } else if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + boolean dayShifted = false; + if (daysToAdd > 0) { + dayShifted = true; + } + + day += daysToAdd; + int weekOfMonth = day / 7; + if (day % 7 > 0) { + weekOfMonth++; + } + + daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; + day += daysToAdd; + if (daysToAdd < 0 + || day > getLastDayOfMonth(mon, cl + .get(Calendar.YEAR))) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0 || dayShifted) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' here because we are NOT promoting the month + continue; + } + } else { + int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w + int dow = daysOfWeek.first(); // desired + // d-o-w + st = daysOfWeek.tailSet(cDow); + if (st != null && st.size() > 0) { + dow = st.first(); + } + + int daysToAdd = 0; + if (cDow < dow) { + daysToAdd = dow - cDow; + } + if (cDow > dow) { + daysToAdd = dow + (7 - cDow); + } + + int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); + + if (day + daysToAdd > lDay) { // will we pass the end of + // the month? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon); + // no '- 1' here because we are promoting the month + continue; + } else if (daysToAdd > 0) { // are we swithing days? + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, + // and we are 1-based + continue; + } + } + } else { // dayOfWSpec && !dayOfMSpec + throw new UnsupportedOperationException( + "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); + } + cl.set(Calendar.DAY_OF_MONTH, day); + + mon = cl.get(Calendar.MONTH) + 1; + // '+ 1' because calendar is 0-based for this field, and we are + // 1-based + int year = cl.get(Calendar.YEAR); + t = -1; + + // test for expressions that never generate a valid fire date, + // but keep looping... + if (year > MAX_YEAR) { + return null; + } + + // get month................................................... + st = months.tailSet(mon); + if (st != null && st.size() != 0) { + t = mon; + mon = st.first(); + } else { + mon = months.first(); + year++; + } + if (mon != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.MONTH, mon - 1); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + + year = cl.get(Calendar.YEAR); + t = -1; + + // get year................................................... + st = years.tailSet(year); + if (st != null && st.size() != 0) { + t = year; + year = st.first(); + } else { + return null; // ran out of years... + } + + if (year != t) { + cl.set(Calendar.SECOND, 0); + cl.set(Calendar.MINUTE, 0); + cl.set(Calendar.HOUR_OF_DAY, 0); + cl.set(Calendar.DAY_OF_MONTH, 1); + cl.set(Calendar.MONTH, 0); + // '- 1' because calendar is 0-based for this field, and we are + // 1-based + cl.set(Calendar.YEAR, year); + continue; + } + cl.set(Calendar.YEAR, year); + + gotOne = true; + } // while( !done ) + + return cl.getTime(); + } + + /** + * Advance the calendar to the particular hour paying particular attention + * to daylight saving problems. + * + * @param cal the calendar to operate on + * @param hour the hour to set + */ + protected void setCalendarHour(Calendar cal, int hour) { + cal.set(java.util.Calendar.HOUR_OF_DAY, hour); + if (cal.get(java.util.Calendar.HOUR_OF_DAY) != hour && hour != 24) { + cal.set(java.util.Calendar.HOUR_OF_DAY, hour + 1); + } + } + + /** + * NOT YET IMPLEMENTED: Returns the time before the given time + * that the CronExpression matches. + */ + public Date getTimeBefore(Date endTime) { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + /** + * NOT YET IMPLEMENTED: Returns the final time that the + * CronExpression will match. + */ + public Date getFinalFireTime() { + // FUTURE_TODO: implement QUARTZ-423 + return null; + } + + protected boolean isLeapYear(int year) { + return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)); + } + + protected int getLastDayOfMonth(int monthNum, int year) { + + switch (monthNum) { + case 1: + return 31; + case 2: + return (isLeapYear(year)) ? 29 : 28; + case 3: + return 31; + case 4: + return 30; + case 5: + return 31; + case 6: + return 30; + case 7: + return 31; + case 8: + return 31; + case 9: + return 30; + case 10: + return 31; + case 11: + return 30; + case 12: + return 31; + default: + throw new IllegalArgumentException("Illegal month number: " + + monthNum); + } + } + + + private void readObject(java.io.ObjectInputStream stream) + throws java.io.IOException, ClassNotFoundException { + + stream.defaultReadObject(); + try { + buildExpression(cronExpression); + } catch (Exception ignore) { + } // never happens + } + + @Override + @Deprecated + public Object clone() { + return new CronExpression(this); + } +} + +class ValueSet { + public int value; + + public int pos; +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java new file mode 100644 index 00000000..faa6063c --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/exception/XxlJobException.java @@ -0,0 +1,14 @@ +package com.xxl.job.admin.core.exception; + +/** + * @author xuxueli 2019-05-04 23:19:29 + */ +public class XxlJobException extends RuntimeException { + + public XxlJobException() { + } + public XxlJobException(String message) { + super(message); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java new file mode 100644 index 00000000..dde4b399 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobGroup.java @@ -0,0 +1,77 @@ +package com.xxl.job.admin.core.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +public class XxlJobGroup { + + private int id; + private String appname; + private String title; + private int addressType; // 执行器地址类型:0=自动注册、1=手动录入 + private String addressList; // 执行器地址列表,多地址逗号分隔(手动录入) + private Date updateTime; + + // registry list + private List registryList; // 执行器地址列表(系统注册) + public List getRegistryList() { + if (addressList!=null && addressList.trim().length()>0) { + registryList = new ArrayList(Arrays.asList(addressList.split(","))); + } + return registryList; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getAppname() { + return appname; + } + + public void setAppname(String appname) { + this.appname = appname; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getAddressType() { + return addressType; + } + + public void setAddressType(int addressType) { + this.addressType = addressType; + } + + public String getAddressList() { + return addressList; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + public void setAddressList(String addressList) { + this.addressList = addressList; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java new file mode 100644 index 00000000..e47b6dc6 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobInfo.java @@ -0,0 +1,237 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job info + * + * @author xuxueli 2016-1-12 18:25:49 + */ +public class XxlJobInfo { + + private int id; // 主键ID + + private int jobGroup; // 执行器主键ID + private String jobDesc; + + private Date addTime; + private Date updateTime; + + private String author; // 负责人 + private String alarmEmail; // 报警邮件 + + private String scheduleType; // 调度类型 + private String scheduleConf; // 调度配置,值含义取决于调度类型 + private String misfireStrategy; // 调度过期策略 + + private String executorRouteStrategy; // 执行器路由策略 + private String executorHandler; // 执行器,任务Handler名称 + private String executorParam; // 执行器,任务参数 + private String executorBlockStrategy; // 阻塞处理策略 + private int executorTimeout; // 任务执行超时时间,单位秒 + private int executorFailRetryCount; // 失败重试次数 + + private String glueType; // GLUE类型 #com.xxl.job.core.glue.GlueTypeEnum + private String glueSource; // GLUE源代码 + private String glueRemark; // GLUE备注 + private Date glueUpdatetime; // GLUE更新时间 + + private String childJobId; // 子任务ID,多个逗号分隔 + + private int triggerStatus; // 调度状态:0-停止,1-运行 + private long triggerLastTime; // 上次调度时间 + private long triggerNextTime; // 下次调度时间 + + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getJobGroup() { + return jobGroup; + } + + public void setJobGroup(int jobGroup) { + this.jobGroup = jobGroup; + } + + public String getJobDesc() { + return jobDesc; + } + + public void setJobDesc(String jobDesc) { + this.jobDesc = jobDesc; + } + + public Date getAddTime() { + return addTime; + } + + public void setAddTime(Date addTime) { + this.addTime = addTime; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getAlarmEmail() { + return alarmEmail; + } + + public void setAlarmEmail(String alarmEmail) { + this.alarmEmail = alarmEmail; + } + + public String getScheduleType() { + return scheduleType; + } + + public void setScheduleType(String scheduleType) { + this.scheduleType = scheduleType; + } + + public String getScheduleConf() { + return scheduleConf; + } + + public void setScheduleConf(String scheduleConf) { + this.scheduleConf = scheduleConf; + } + + public String getMisfireStrategy() { + return misfireStrategy; + } + + public void setMisfireStrategy(String misfireStrategy) { + this.misfireStrategy = misfireStrategy; + } + + public String getExecutorRouteStrategy() { + return executorRouteStrategy; + } + + public void setExecutorRouteStrategy(String executorRouteStrategy) { + this.executorRouteStrategy = executorRouteStrategy; + } + + public String getExecutorHandler() { + return executorHandler; + } + + public void setExecutorHandler(String executorHandler) { + this.executorHandler = executorHandler; + } + + public String getExecutorParam() { + return executorParam; + } + + public void setExecutorParam(String executorParam) { + this.executorParam = executorParam; + } + + public String getExecutorBlockStrategy() { + return executorBlockStrategy; + } + + public void setExecutorBlockStrategy(String executorBlockStrategy) { + this.executorBlockStrategy = executorBlockStrategy; + } + + public int getExecutorTimeout() { + return executorTimeout; + } + + public void setExecutorTimeout(int executorTimeout) { + this.executorTimeout = executorTimeout; + } + + public int getExecutorFailRetryCount() { + return executorFailRetryCount; + } + + public void setExecutorFailRetryCount(int executorFailRetryCount) { + this.executorFailRetryCount = executorFailRetryCount; + } + + public String getGlueType() { + return glueType; + } + + public void setGlueType(String glueType) { + this.glueType = glueType; + } + + public String getGlueSource() { + return glueSource; + } + + public void setGlueSource(String glueSource) { + this.glueSource = glueSource; + } + + public String getGlueRemark() { + return glueRemark; + } + + public void setGlueRemark(String glueRemark) { + this.glueRemark = glueRemark; + } + + public Date getGlueUpdatetime() { + return glueUpdatetime; + } + + public void setGlueUpdatetime(Date glueUpdatetime) { + this.glueUpdatetime = glueUpdatetime; + } + + public String getChildJobId() { + return childJobId; + } + + public void setChildJobId(String childJobId) { + this.childJobId = childJobId; + } + + public int getTriggerStatus() { + return triggerStatus; + } + + public void setTriggerStatus(int triggerStatus) { + this.triggerStatus = triggerStatus; + } + + public long getTriggerLastTime() { + return triggerLastTime; + } + + public void setTriggerLastTime(long triggerLastTime) { + this.triggerLastTime = triggerLastTime; + } + + public long getTriggerNextTime() { + return triggerNextTime; + } + + public void setTriggerNextTime(long triggerNextTime) { + this.triggerNextTime = triggerNextTime; + } +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java new file mode 100644 index 00000000..7d3072aa --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLog.java @@ -0,0 +1,157 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job log, used to track trigger process + * @author xuxueli 2015-12-19 23:19:09 + */ +public class XxlJobLog { + + private long id; + + // job info + private int jobGroup; + private int jobId; + + // execute info + private String executorAddress; + private String executorHandler; + private String executorParam; + private String executorShardingParam; + private int executorFailRetryCount; + + // trigger info + private Date triggerTime; + private int triggerCode; + private String triggerMsg; + + // handle info + private Date handleTime; + private int handleCode; + private String handleMsg; + + // alarm info + private int alarmStatus; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public int getJobGroup() { + return jobGroup; + } + + public void setJobGroup(int jobGroup) { + this.jobGroup = jobGroup; + } + + public int getJobId() { + return jobId; + } + + public void setJobId(int jobId) { + this.jobId = jobId; + } + + public String getExecutorAddress() { + return executorAddress; + } + + public void setExecutorAddress(String executorAddress) { + this.executorAddress = executorAddress; + } + + public String getExecutorHandler() { + return executorHandler; + } + + public void setExecutorHandler(String executorHandler) { + this.executorHandler = executorHandler; + } + + public String getExecutorParam() { + return executorParam; + } + + public void setExecutorParam(String executorParam) { + this.executorParam = executorParam; + } + + public String getExecutorShardingParam() { + return executorShardingParam; + } + + public void setExecutorShardingParam(String executorShardingParam) { + this.executorShardingParam = executorShardingParam; + } + + public int getExecutorFailRetryCount() { + return executorFailRetryCount; + } + + public void setExecutorFailRetryCount(int executorFailRetryCount) { + this.executorFailRetryCount = executorFailRetryCount; + } + + public Date getTriggerTime() { + return triggerTime; + } + + public void setTriggerTime(Date triggerTime) { + this.triggerTime = triggerTime; + } + + public int getTriggerCode() { + return triggerCode; + } + + public void setTriggerCode(int triggerCode) { + this.triggerCode = triggerCode; + } + + public String getTriggerMsg() { + return triggerMsg; + } + + public void setTriggerMsg(String triggerMsg) { + this.triggerMsg = triggerMsg; + } + + public Date getHandleTime() { + return handleTime; + } + + public void setHandleTime(Date handleTime) { + this.handleTime = handleTime; + } + + public int getHandleCode() { + return handleCode; + } + + public void setHandleCode(int handleCode) { + this.handleCode = handleCode; + } + + public String getHandleMsg() { + return handleMsg; + } + + public void setHandleMsg(String handleMsg) { + this.handleMsg = handleMsg; + } + + public int getAlarmStatus() { + return alarmStatus; + } + + public void setAlarmStatus(int alarmStatus) { + this.alarmStatus = alarmStatus; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java new file mode 100644 index 00000000..2f59ffa8 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogGlue.java @@ -0,0 +1,75 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * xxl-job log for glue, used to track job code process + * @author xuxueli 2016-5-19 17:57:46 + */ +public class XxlJobLogGlue { + + private int id; + private int jobId; // 任务主键ID + private String glueType; // GLUE类型 #com.xxl.job.core.glue.GlueTypeEnum + private String glueSource; + private String glueRemark; + private Date addTime; + private Date updateTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getJobId() { + return jobId; + } + + public void setJobId(int jobId) { + this.jobId = jobId; + } + + public String getGlueType() { + return glueType; + } + + public void setGlueType(String glueType) { + this.glueType = glueType; + } + + public String getGlueSource() { + return glueSource; + } + + public void setGlueSource(String glueSource) { + this.glueSource = glueSource; + } + + public String getGlueRemark() { + return glueRemark; + } + + public void setGlueRemark(String glueRemark) { + this.glueRemark = glueRemark; + } + + public Date getAddTime() { + return addTime; + } + + public void setAddTime(Date addTime) { + this.addTime = addTime; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java new file mode 100644 index 00000000..e58ff1a9 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobLogReport.java @@ -0,0 +1,54 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +public class XxlJobLogReport { + + private int id; + + private Date triggerDay; + + private int runningCount; + private int sucCount; + private int failCount; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public Date getTriggerDay() { + return triggerDay; + } + + public void setTriggerDay(Date triggerDay) { + this.triggerDay = triggerDay; + } + + public int getRunningCount() { + return runningCount; + } + + public void setRunningCount(int runningCount) { + this.runningCount = runningCount; + } + + public int getSucCount() { + return sucCount; + } + + public void setSucCount(int sucCount) { + this.sucCount = sucCount; + } + + public int getFailCount() { + return failCount; + } + + public void setFailCount(int failCount) { + this.failCount = failCount; + } +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java new file mode 100644 index 00000000..924d6d33 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobRegistry.java @@ -0,0 +1,55 @@ +package com.xxl.job.admin.core.model; + +import java.util.Date; + +/** + * Created by xuxueli on 16/9/30. + */ +public class XxlJobRegistry { + + private int id; + private String registryGroup; + private String registryKey; + private String registryValue; + private Date updateTime; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getRegistryGroup() { + return registryGroup; + } + + public void setRegistryGroup(String registryGroup) { + this.registryGroup = registryGroup; + } + + public String getRegistryKey() { + return registryKey; + } + + public void setRegistryKey(String registryKey) { + this.registryKey = registryKey; + } + + public String getRegistryValue() { + return registryValue; + } + + public void setRegistryValue(String registryValue) { + this.registryValue = registryValue; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java new file mode 100644 index 00000000..db17327a --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/model/XxlJobUser.java @@ -0,0 +1,73 @@ +package com.xxl.job.admin.core.model; + +import org.springframework.util.StringUtils; + +/** + * @author xuxueli 2019-05-04 16:43:12 + */ +public class XxlJobUser { + + private int id; + private String username; // 账号 + private String password; // 密码 + private int role; // 角色:0-普通用户、1-管理员 + private String permission; // 权限:执行器ID列表,多个逗号分割 + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getRole() { + return role; + } + + public void setRole(int role) { + this.role = role; + } + + public String getPermission() { + return permission; + } + + public void setPermission(String permission) { + this.permission = permission; + } + + // plugin + public boolean validPermission(int jobGroup){ + if (this.role == 1) { + return true; + } else { + if (StringUtils.hasText(this.permission)) { + for (String permissionItem : this.permission.split(",")) { + if (String.valueOf(jobGroup).equals(permissionItem)) { + return true; + } + } + } + return false; + } + + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java new file mode 100644 index 00000000..b2dd1515 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/RemoteHttpJobBean.java @@ -0,0 +1,32 @@ +//package com.xxl.job.admin.core.jobbean; +// +//import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +//import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +//import org.quartz.JobExecutionContext; +//import org.quartz.JobExecutionException; +//import org.quartz.JobKey; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.scheduling.quartz.QuartzJobBean; +// +///** +// * http job bean +// * “@DisallowConcurrentExecution” disable concurrent, thread size can not be only one, better given more +// * @author xuxueli 2015-12-17 18:20:34 +// */ +////@DisallowConcurrentExecution +//public class RemoteHttpJobBean extends QuartzJobBean { +// private static Logger logger = LoggerFactory.getLogger(RemoteHttpJobBean.class); +// +// @Override +// protected void executeInternal(JobExecutionContext context) +// throws JobExecutionException { +// +// // load jobId +// JobKey jobKey = context.getTrigger().getJobKey(); +// Integer jobId = Integer.valueOf(jobKey.getName()); +// +// +// } +// +//} \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java new file mode 100644 index 00000000..1e62aa19 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobDynamicScheduler.java @@ -0,0 +1,413 @@ +//package com.xxl.job.admin.core.schedule; +// +//import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +//import com.xxl.job.admin.core.jobbean.RemoteHttpJobBean; +//import com.xxl.job.admin.core.model.XxlJobInfo; +//import com.xxl.job.admin.core.thread.JobFailMonitorHelper; +//import com.xxl.job.admin.core.thread.JobRegistryMonitorHelper; +//import com.xxl.job.admin.core.thread.JobTriggerPoolHelper; +//import com.xxl.job.admin.core.util.I18nUtil; +//import com.xxl.job.core.biz.AdminBiz; +//import com.xxl.job.core.biz.ExecutorBiz; +//import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +//import com.xxl.rpc.remoting.invoker.XxlRpcInvokerFactory; +//import com.xxl.rpc.remoting.invoker.call.CallType; +//import com.xxl.rpc.remoting.invoker.reference.XxlRpcReferenceBean; +//import com.xxl.rpc.remoting.invoker.route.LoadBalance; +//import com.xxl.rpc.remoting.net.NetEnum; +//import com.xxl.rpc.remoting.net.impl.servlet.server.ServletServerHandler; +//import com.xxl.rpc.remoting.provider.XxlRpcProviderFactory; +//import com.xxl.rpc.serialize.Serializer; +//import org.quartz.*; +//import org.quartz.Trigger.TriggerState; +//import org.quartz.impl.triggers.CronTriggerImpl; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.util.Assert; +// +//import javax.servlet.ServletException; +//import javax.servlet.http.HttpServletRequest; +//import javax.servlet.http.HttpServletResponse; +//import java.io.IOException; +//import java.util.Date; +//import java.util.concurrent.ConcurrentHashMap; +// +///** +// * base quartz scheduler util +// * @author xuxueli 2015-12-19 16:13:53 +// */ +//public final class XxlJobDynamicScheduler { +// private static final Logger logger = LoggerFactory.getLogger(XxlJobDynamicScheduler_old.class); +// +// // ---------------------- param ---------------------- +// +// // scheduler +// private static Scheduler scheduler; +// public void setScheduler(Scheduler scheduler) { +// XxlJobDynamicScheduler_old.scheduler = scheduler; +// } +// +// +// // ---------------------- init + destroy ---------------------- +// public void start() throws Exception { +// // valid +// Assert.notNull(scheduler, "quartz scheduler is null"); +// +// // init i18n +// initI18n(); +// +// // admin registry monitor run +// JobRegistryMonitorHelper.getInstance().start(); +// +// // admin monitor run +// JobFailMonitorHelper.getInstance().start(); +// +// // admin-server +// initRpcProvider(); +// +// logger.info(">>>>>>>>> init xxl-job admin success."); +// } +// +// +// public void destroy() throws Exception { +// // admin trigger pool stop +// JobTriggerPoolHelper.toStop(); +// +// // admin registry stop +// JobRegistryMonitorHelper.getInstance().toStop(); +// +// // admin monitor stop +// JobFailMonitorHelper.getInstance().toStop(); +// +// // admin-server +// stopRpcProvider(); +// } +// +// +// // ---------------------- I18n ---------------------- +// +// private void initI18n(){ +// for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) { +// item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name()))); +// } +// } +// +// +// // ---------------------- admin rpc provider (no server version) ---------------------- +// private static ServletServerHandler servletServerHandler; +// private void initRpcProvider(){ +// // init +// XxlRpcProviderFactory xxlRpcProviderFactory = new XxlRpcProviderFactory(); +// xxlRpcProviderFactory.initConfig( +// NetEnum.NETTY_HTTP, +// Serializer.SerializeEnum.HESSIAN.getSerializer(), +// null, +// 0, +// XxlJobAdminConfig.getAdminConfig().getAccessToken(), +// null, +// null); +// +// // add services +// xxlRpcProviderFactory.addService(AdminBiz.class.getName(), null, XxlJobAdminConfig.getAdminConfig().getAdminBiz()); +// +// // servlet handler +// servletServerHandler = new ServletServerHandler(xxlRpcProviderFactory); +// } +// private void stopRpcProvider() throws Exception { +// XxlRpcInvokerFactory.getInstance().stop(); +// } +// public static void invokeAdminService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { +// servletServerHandler.handle(null, request, response); +// } +// +// +// // ---------------------- executor-client ---------------------- +// private static ConcurrentHashMap executorBizRepository = new ConcurrentHashMap(); +// public static ExecutorBiz getExecutorBiz(String address) throws Exception { +// // valid +// if (address==null || address.trim().length()==0) { +// return null; +// } +// +// // load-cache +// address = address.trim(); +// ExecutorBiz executorBiz = executorBizRepository.get(address); +// if (executorBiz != null) { +// return executorBiz; +// } +// +// // set-cache +// executorBiz = (ExecutorBiz) new XxlRpcReferenceBean( +// NetEnum.NETTY_HTTP, +// Serializer.SerializeEnum.HESSIAN.getSerializer(), +// CallType.SYNC, +// LoadBalance.ROUND, +// ExecutorBiz.class, +// null, +// 5000, +// address, +// XxlJobAdminConfig.getAdminConfig().getAccessToken(), +// null, +// null).getObject(); +// +// executorBizRepository.put(address, executorBiz); +// return executorBiz; +// } +// +// +// // ---------------------- schedule util ---------------------- +// +// /** +// * fill job info +// * +// * @param jobInfo +// */ +// public static void fillJobInfo(XxlJobInfo jobInfo) { +// +// String name = String.valueOf(jobInfo.getId()); +// +// // trigger key +// TriggerKey triggerKey = TriggerKey.triggerKey(name); +// try { +// +// // trigger cron +// Trigger trigger = scheduler.getTrigger(triggerKey); +// if (trigger!=null && trigger instanceof CronTriggerImpl) { +// String cronExpression = ((CronTriggerImpl) trigger).getCronExpression(); +// jobInfo.setJobCron(cronExpression); +// } +// +// // trigger state +// TriggerState triggerState = scheduler.getTriggerState(triggerKey); +// if (triggerState!=null) { +// jobInfo.setJobStatus(triggerState.name()); +// } +// +// //JobKey jobKey = new JobKey(jobInfo.getJobName(), String.valueOf(jobInfo.getJobGroup())); +// //JobDetail jobDetail = scheduler.getJobDetail(jobKey); +// //String jobClass = jobDetail.getJobClass().getName(); +// +// } catch (SchedulerException e) { +// logger.error(e.getMessage(), e); +// } +// } +// +// +// /** +// * add trigger + job +// * +// * @param jobName +// * @param cronExpression +// * @return +// * @throws SchedulerException +// */ +// public static boolean addJob(String jobName, String cronExpression) throws SchedulerException { +// // 1、job key +// TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// JobKey jobKey = new JobKey(jobName); +// +// // 2、valid +// if (scheduler.checkExists(triggerKey)) { +// return true; // PASS +// } +// +// // 3、corn trigger +// CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing(); // withMisfireHandlingInstructionDoNothing 忽略掉调度终止过程中忽略的调度 +// CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build(); +// +// // 4、job detail +// Class jobClass_ = RemoteHttpJobBean.class; // Class.forName(jobInfo.getJobClass()); +// JobDetail jobDetail = JobBuilder.newJob(jobClass_).withIdentity(jobKey).build(); +// +// /*if (jobInfo.getJobData()!=null) { +// JobDataMap jobDataMap = jobDetail.getJobDataMap(); +// jobDataMap.putAll(JacksonUtil.readValue(jobInfo.getJobData(), Map.class)); +// // JobExecutionContext context.getMergedJobDataMap().get("mailGuid"); +// }*/ +// +// // 5、schedule job +// Date date = scheduler.scheduleJob(jobDetail, cronTrigger); +// +// logger.info(">>>>>>>>>>> addJob success(quartz), jobDetail:{}, cronTrigger:{}, date:{}", jobDetail, cronTrigger, date); +// return true; +// } +// +// +// /** +// * remove trigger + job +// * +// * @param jobName +// * @return +// * @throws SchedulerException +// */ +// public static boolean removeJob(String jobName) throws SchedulerException { +// +// JobKey jobKey = new JobKey(jobName); +// scheduler.deleteJob(jobKey); +// +// /*TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// if (scheduler.checkExists(triggerKey)) { +// scheduler.unscheduleJob(triggerKey); // trigger + job +// }*/ +// +// logger.info(">>>>>>>>>>> removeJob success(quartz), jobKey:{}", jobKey); +// return true; +// } +// +// +// /** +// * updateJobCron +// * +// * @param jobName +// * @param cronExpression +// * @return +// * @throws SchedulerException +// */ +// public static boolean updateJobCron(String jobName, String cronExpression) throws SchedulerException { +// +// // 1、job key +// TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// +// // 2、valid +// if (!scheduler.checkExists(triggerKey)) { +// return true; // PASS +// } +// +// CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(triggerKey); +// +// // 3、avoid repeat cron +// String oldCron = oldTrigger.getCronExpression(); +// if (oldCron.equals(cronExpression)){ +// return true; // PASS +// } +// +// // 4、new cron trigger +// CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing(); +// oldTrigger = oldTrigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build(); +// +// // 5、rescheduleJob +// scheduler.rescheduleJob(triggerKey, oldTrigger); +// +// /* +// JobKey jobKey = new JobKey(jobName); +// +// // old job detail +// JobDetail jobDetail = scheduler.getJobDetail(jobKey); +// +// // new trigger +// HashSet triggerSet = new HashSet(); +// triggerSet.add(cronTrigger); +// // cover trigger of job detail +// scheduler.scheduleJob(jobDetail, triggerSet, true);*/ +// +// logger.info(">>>>>>>>>>> resumeJob success, JobName:{}", jobName); +// return true; +// } +// +// +// /** +// * pause +// * +// * @param jobName +// * @return +// * @throws SchedulerException +// */ +// /*public static boolean pauseJob(String jobName) throws SchedulerException { +// +// TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// +// boolean result = false; +// if (scheduler.checkExists(triggerKey)) { +// scheduler.pauseTrigger(triggerKey); +// result = true; +// } +// +// logger.info(">>>>>>>>>>> pauseJob {}, triggerKey:{}", (result?"success":"fail"),triggerKey); +// return result; +// }*/ +// +// +// /** +// * resume +// * +// * @param jobName +// * @return +// * @throws SchedulerException +// */ +// /*public static boolean resumeJob(String jobName) throws SchedulerException { +// +// TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// +// boolean result = false; +// if (scheduler.checkExists(triggerKey)) { +// scheduler.resumeTrigger(triggerKey); +// result = true; +// } +// +// logger.info(">>>>>>>>>>> resumeJob {}, triggerKey:{}", (result?"success":"fail"), triggerKey); +// return result; +// }*/ +// +// +// /** +// * run +// * +// * @param jobName +// * @return +// * @throws SchedulerException +// */ +// /*public static boolean triggerJob(String jobName) throws SchedulerException { +// // TriggerKey : name + group +// JobKey jobKey = new JobKey(jobName); +// TriggerKey triggerKey = TriggerKey.triggerKey(jobName); +// +// boolean result = false; +// if (scheduler.checkExists(triggerKey)) { +// scheduler.triggerJob(jobKey); +// result = true; +// logger.info(">>>>>>>>>>> runJob success, jobKey:{}", jobKey); +// } else { +// logger.info(">>>>>>>>>>> runJob fail, jobKey:{}", jobKey); +// } +// return result; +// }*/ +// +// +// /** +// * finaAllJobList +// * +// * @return +// *//* +// @Deprecated +// public static List> finaAllJobList(){ +// List> jobList = new ArrayList>(); +// +// try { +// if (scheduler.getJobGroupNames()==null || scheduler.getJobGroupNames().size()==0) { +// return null; +// } +// String groupName = scheduler.getJobGroupNames().get(0); +// Set jobKeys = scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName)); +// if (jobKeys!=null && jobKeys.size()>0) { +// for (JobKey jobKey : jobKeys) { +// TriggerKey triggerKey = TriggerKey.triggerKey(jobKey.getName(), Scheduler.DEFAULT_GROUP); +// Trigger trigger = scheduler.getTrigger(triggerKey); +// JobDetail jobDetail = scheduler.getJobDetail(jobKey); +// TriggerState triggerState = scheduler.getTriggerState(triggerKey); +// Map jobMap = new HashMap(); +// jobMap.put("TriggerKey", triggerKey); +// jobMap.put("Trigger", trigger); +// jobMap.put("JobDetail", jobDetail); +// jobMap.put("TriggerState", triggerState); +// jobList.add(jobMap); +// } +// } +// +// } catch (SchedulerException e) { +// logger.error(e.getMessage(), e); +// return null; +// } +// return jobList; +// }*/ +// +//} \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java new file mode 100644 index 00000000..ad074307 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/old/XxlJobThreadPool.java @@ -0,0 +1,58 @@ +//package com.xxl.job.admin.core.quartz; +// +//import org.quartz.SchedulerConfigException; +//import org.quartz.spi.ThreadPool; +// +///** +// * single thread pool, for async trigger +// * +// * @author xuxueli 2019-03-06 +// */ +//public class XxlJobThreadPool implements ThreadPool { +// +// @Override +// public boolean runInThread(Runnable runnable) { +// +// // async run +// runnable.run(); +// return true; +// +// //return false; +// } +// +// @Override +// public int blockForAvailableThreads() { +// return 1; +// } +// +// @Override +// public void initialize() throws SchedulerConfigException { +// +// } +// +// @Override +// public void shutdown(boolean waitForJobsToComplete) { +// +// } +// +// @Override +// public int getPoolSize() { +// return 1; +// } +// +// @Override +// public void setInstanceId(String schedInstId) { +// +// } +// +// @Override +// public void setInstanceName(String schedName) { +// +// } +// +// // support +// public void setThreadCount(int count) { +// // +// } +// +//} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java new file mode 100644 index 00000000..7fff93a9 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouteStrategyEnum.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route; + +import com.xxl.job.admin.core.route.strategy.*; +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * Created by xuxueli on 17/3/10. + */ +public enum ExecutorRouteStrategyEnum { + + FIRST(I18nUtil.getString("jobconf_route_first"), new ExecutorRouteFirst()), + LAST(I18nUtil.getString("jobconf_route_last"), new ExecutorRouteLast()), + ROUND(I18nUtil.getString("jobconf_route_round"), new ExecutorRouteRound()), + RANDOM(I18nUtil.getString("jobconf_route_random"), new ExecutorRouteRandom()), + CONSISTENT_HASH(I18nUtil.getString("jobconf_route_consistenthash"), new ExecutorRouteConsistentHash()), + LEAST_FREQUENTLY_USED(I18nUtil.getString("jobconf_route_lfu"), new ExecutorRouteLFU()), + LEAST_RECENTLY_USED(I18nUtil.getString("jobconf_route_lru"), new ExecutorRouteLRU()), + FAILOVER(I18nUtil.getString("jobconf_route_failover"), new ExecutorRouteFailover()), + BUSYOVER(I18nUtil.getString("jobconf_route_busyover"), new ExecutorRouteBusyover()), + SHARDING_BROADCAST(I18nUtil.getString("jobconf_route_shard"), null); + + ExecutorRouteStrategyEnum(String title, ExecutorRouter router) { + this.title = title; + this.router = router; + } + + private String title; + private ExecutorRouter router; + + public String getTitle() { + return title; + } + public ExecutorRouter getRouter() { + return router; + } + + public static ExecutorRouteStrategyEnum match(String name, ExecutorRouteStrategyEnum defaultItem){ + if (name != null) { + for (ExecutorRouteStrategyEnum item: ExecutorRouteStrategyEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + } + return defaultItem; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java new file mode 100644 index 00000000..5de9a1d0 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/ExecutorRouter.java @@ -0,0 +1,24 @@ +package com.xxl.job.admin.core.route; + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public abstract class ExecutorRouter { + protected static Logger logger = LoggerFactory.getLogger(ExecutorRouter.class); + + /** + * route address + * + * @param addressList + * @return ReturnT.content=address + */ + public abstract ReturnT route(TriggerParam triggerParam, List addressList); + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java new file mode 100644 index 00000000..868560fc --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteBusyover.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.IdleBeatParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteBusyover extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + StringBuffer idleBeatResultSB = new StringBuffer(); + for (String address : addressList) { + // beat + ReturnT idleBeatResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId())); + } catch (Exception e) { + logger.error(e.getMessage(), e); + idleBeatResult = new ReturnT(ReturnT.FAIL_CODE, ""+e ); + } + idleBeatResultSB.append( (idleBeatResultSB.length()>0)?"

":"") + .append(I18nUtil.getString("jobconf_idleBeat") + ":") + .append("
address:").append(address) + .append("
code:").append(idleBeatResult.getCode()) + .append("
msg:").append(idleBeatResult.getMsg()); + + // beat success + if (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) { + idleBeatResult.setMsg(idleBeatResultSB.toString()); + idleBeatResult.setContent(address); + return idleBeatResult; + } + } + + return new ReturnT(ReturnT.FAIL_CODE, idleBeatResultSB.toString()); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java new file mode 100644 index 00000000..41ac671c --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteConsistentHash.java @@ -0,0 +1,85 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * 分组下机器地址相同,不同JOB均匀散列在不同机器上,保证分组下机器分配JOB平均;且每个JOB固定调度其中一台机器; + * a、virtual node:解决不均衡问题 + * b、hash method replace hashCode:String的hashCode可能重复,需要进一步扩大hashCode的取值范围 + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteConsistentHash extends ExecutorRouter { + + private static int VIRTUAL_NODE_NUM = 100; + + /** + * get hash code on 2^32 ring (md5散列的方式计算hash值) + * @param key + * @return + */ + private static long hash(String key) { + + // md5 byte + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 not supported", e); + } + md5.reset(); + byte[] keyBytes = null; + try { + keyBytes = key.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Unknown string :" + key, e); + } + + md5.update(keyBytes); + byte[] digest = md5.digest(); + + // hash code, Truncate to 32-bits + long hashCode = ((long) (digest[3] & 0xFF) << 24) + | ((long) (digest[2] & 0xFF) << 16) + | ((long) (digest[1] & 0xFF) << 8) + | (digest[0] & 0xFF); + + long truncateHashCode = hashCode & 0xffffffffL; + return truncateHashCode; + } + + public String hashJob(int jobId, List addressList) { + + // ------A1------A2-------A3------ + // -----------J1------------------ + TreeMap addressRing = new TreeMap(); + for (String address: addressList) { + for (int i = 0; i < VIRTUAL_NODE_NUM; i++) { + long addressHash = hash("SHARD-" + address + "-NODE-" + i); + addressRing.put(addressHash, address); + } + } + + long jobHash = hash(String.valueOf(jobId)); + SortedMap lastRing = addressRing.tailMap(jobHash); + if (!lastRing.isEmpty()) { + return lastRing.get(lastRing.firstKey()); + } + return addressRing.firstEntry().getValue(); + } + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = hashJob(triggerParam.getJobId(), addressList); + return new ReturnT(address); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java new file mode 100644 index 00000000..a2e4c909 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFailover.java @@ -0,0 +1,48 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteFailover extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + + StringBuffer beatResultSB = new StringBuffer(); + for (String address : addressList) { + // beat + ReturnT beatResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + beatResult = executorBiz.beat(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + beatResult = new ReturnT(ReturnT.FAIL_CODE, ""+e ); + } + beatResultSB.append( (beatResultSB.length()>0)?"

":"") + .append(I18nUtil.getString("jobconf_beat") + ":") + .append("
address:").append(address) + .append("
code:").append(beatResult.getCode()) + .append("
msg:").append(beatResult.getMsg()); + + // beat success + if (beatResult.getCode() == ReturnT.SUCCESS_CODE) { + + beatResult.setMsg(beatResultSB.toString()); + beatResult.setContent(address); + return beatResult; + } + } + return new ReturnT(ReturnT.FAIL_CODE, beatResultSB.toString()); + + } +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java new file mode 100644 index 00000000..de4d7afb --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteFirst.java @@ -0,0 +1,19 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteFirst extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList){ + return new ReturnT(addressList.get(0)); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java new file mode 100644 index 00000000..9df19726 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLFU.java @@ -0,0 +1,79 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 单个JOB对应的每个执行器,使用频率最低的优先被选举 + * a(*)、LFU(Least Frequently Used):最不经常使用,频率/次数 + * b、LRU(Least Recently Used):最近最久未使用,时间 + * + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLFU extends ExecutorRouter { + + private static ConcurrentMap> jobLfuMap = new ConcurrentHashMap>(); + private static long CACHE_VALID_TIME = 0; + + public String route(int jobId, List addressList) { + + // cache clear + if (System.currentTimeMillis() > CACHE_VALID_TIME) { + jobLfuMap.clear(); + CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; + } + + // lfu item init + HashMap lfuItemMap = jobLfuMap.get(jobId); // Key排序可以用TreeMap+构造入参Compare;Value排序暂时只能通过ArrayList; + if (lfuItemMap == null) { + lfuItemMap = new HashMap(); + jobLfuMap.putIfAbsent(jobId, lfuItemMap); // 避免重复覆盖 + } + + // put new + for (String address: addressList) { + if (!lfuItemMap.containsKey(address) || lfuItemMap.get(address) >1000000 ) { + lfuItemMap.put(address, new Random().nextInt(addressList.size())); // 初始化时主动Random一次,缓解首次压力 + } + } + // remove old + List delKeys = new ArrayList<>(); + for (String existKey: lfuItemMap.keySet()) { + if (!addressList.contains(existKey)) { + delKeys.add(existKey); + } + } + if (delKeys.size() > 0) { + for (String delKey: delKeys) { + lfuItemMap.remove(delKey); + } + } + + // load least userd count address + List> lfuItemList = new ArrayList>(lfuItemMap.entrySet()); + Collections.sort(lfuItemList, new Comparator>() { + @Override + public int compare(Map.Entry o1, Map.Entry o2) { + return o1.getValue().compareTo(o2.getValue()); + } + }); + + Map.Entry addressItem = lfuItemList.get(0); + String minAddress = addressItem.getKey(); + addressItem.setValue(addressItem.getValue() + 1); + + return addressItem.getKey(); + } + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = route(triggerParam.getJobId(), addressList); + return new ReturnT(address); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java new file mode 100644 index 00000000..2d540067 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLRU.java @@ -0,0 +1,76 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 单个JOB对应的每个执行器,最久为使用的优先被选举 + * a、LFU(Least Frequently Used):最不经常使用,频率/次数 + * b(*)、LRU(Least Recently Used):最近最久未使用,时间 + * + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLRU extends ExecutorRouter { + + private static ConcurrentMap> jobLRUMap = new ConcurrentHashMap>(); + private static long CACHE_VALID_TIME = 0; + + public String route(int jobId, List addressList) { + + // cache clear + if (System.currentTimeMillis() > CACHE_VALID_TIME) { + jobLRUMap.clear(); + CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; + } + + // init lru + LinkedHashMap lruItem = jobLRUMap.get(jobId); + if (lruItem == null) { + /** + * LinkedHashMap + * a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期; + * b、removeEldestEntry:新增元素时将会调用,返回true时会删除最老元素;可封装LinkedHashMap并重写该方法,比如定义最大容量,超出是返回true即可实现固定长度的LRU算法; + */ + lruItem = new LinkedHashMap(16, 0.75f, true); + jobLRUMap.putIfAbsent(jobId, lruItem); + } + + // put new + for (String address: addressList) { + if (!lruItem.containsKey(address)) { + lruItem.put(address, address); + } + } + // remove old + List delKeys = new ArrayList<>(); + for (String existKey: lruItem.keySet()) { + if (!addressList.contains(existKey)) { + delKeys.add(existKey); + } + } + if (delKeys.size() > 0) { + for (String delKey: delKeys) { + lruItem.remove(delKey); + } + } + + // load + String eldestKey = lruItem.entrySet().iterator().next().getKey(); + String eldestValue = lruItem.get(eldestKey); + return eldestValue; + } + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = route(triggerParam.getJobId(), addressList); + return new ReturnT(address); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java new file mode 100644 index 00000000..4ff3cf6b --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteLast.java @@ -0,0 +1,19 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteLast extends ExecutorRouter { + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + return new ReturnT(addressList.get(addressList.size()-1)); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java new file mode 100644 index 00000000..5ea4a384 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRandom.java @@ -0,0 +1,23 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; +import java.util.Random; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteRandom extends ExecutorRouter { + + private static Random localRandom = new Random(); + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = addressList.get(localRandom.nextInt(addressList.size())); + return new ReturnT(address); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java new file mode 100644 index 00000000..d0ea2baa --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/route/strategy/ExecutorRouteRound.java @@ -0,0 +1,46 @@ +package com.xxl.job.admin.core.route.strategy; + +import com.xxl.job.admin.core.route.ExecutorRouter; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by xuxueli on 17/3/10. + */ +public class ExecutorRouteRound extends ExecutorRouter { + + private static ConcurrentMap routeCountEachJob = new ConcurrentHashMap<>(); + private static long CACHE_VALID_TIME = 0; + + private static int count(int jobId) { + // cache clear + if (System.currentTimeMillis() > CACHE_VALID_TIME) { + routeCountEachJob.clear(); + CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; + } + + AtomicInteger count = routeCountEachJob.get(jobId); + if (count == null || count.get() > 1000000) { + // 初始化时主动Random一次,缓解首次压力 + count = new AtomicInteger(new Random().nextInt(100)); + } else { + // count++ + count.addAndGet(1); + } + routeCountEachJob.put(jobId, count); + return count.get(); + } + + @Override + public ReturnT route(TriggerParam triggerParam, List addressList) { + String address = addressList.get(count(triggerParam.getJobId())%addressList.size()); + return new ReturnT(address); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java new file mode 100644 index 00000000..0b9b4a9c --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/MisfireStrategyEnum.java @@ -0,0 +1,39 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * @author xuxueli 2020-10-29 21:11:23 + */ +public enum MisfireStrategyEnum { + + /** + * do nothing + */ + DO_NOTHING(I18nUtil.getString("misfire_strategy_do_nothing")), + + /** + * fire once now + */ + FIRE_ONCE_NOW(I18nUtil.getString("misfire_strategy_fire_once_now")); + + private String title; + + MisfireStrategyEnum(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public static MisfireStrategyEnum match(String name, MisfireStrategyEnum defaultItem){ + for (MisfireStrategyEnum item: MisfireStrategyEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + return defaultItem; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java new file mode 100644 index 00000000..aa334fda --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/ScheduleTypeEnum.java @@ -0,0 +1,46 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * @author xuxueli 2020-10-29 21:11:23 + */ +public enum ScheduleTypeEnum { + + NONE(I18nUtil.getString("schedule_type_none")), + + /** + * schedule by cron + */ + CRON(I18nUtil.getString("schedule_type_cron")), + + /** + * schedule by fixed rate (in seconds) + */ + FIX_RATE(I18nUtil.getString("schedule_type_fix_rate")), + + /** + * schedule by fix delay (in seconds), after the last time + */ + /*FIX_DELAY(I18nUtil.getString("schedule_type_fix_delay"))*/; + + private String title; + + ScheduleTypeEnum(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public static ScheduleTypeEnum match(String name, ScheduleTypeEnum defaultItem){ + for (ScheduleTypeEnum item: ScheduleTypeEnum.values()) { + if (item.name().equals(name)) { + return item; + } + } + return defaultItem; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java new file mode 100644 index 00000000..bb2cda8b --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/scheduler/XxlJobScheduler.java @@ -0,0 +1,101 @@ +package com.xxl.job.admin.core.scheduler; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.thread.*; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.client.ExecutorBizClient; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * @author xuxueli 2018-10-28 00:18:17 + */ + +public class XxlJobScheduler { + private static final Logger logger = LoggerFactory.getLogger(XxlJobScheduler.class); + + + public void init() throws Exception { + // init i18n + initI18n(); + + // admin trigger pool start + JobTriggerPoolHelper.toStart(); + + // admin registry monitor run + JobRegistryHelper.getInstance().start(); + + // admin fail-monitor run + JobFailMonitorHelper.getInstance().start(); + + // admin lose-monitor run ( depend on JobTriggerPoolHelper ) + JobCompleteHelper.getInstance().start(); + + // admin log report start + JobLogReportHelper.getInstance().start(); + + // start-schedule ( depend on JobTriggerPoolHelper ) + JobScheduleHelper.getInstance().start(); + + logger.info(">>>>>>>>> init xxl-job admin success."); + } + + + public void destroy() throws Exception { + + // stop-schedule + JobScheduleHelper.getInstance().toStop(); + + // admin log report stop + JobLogReportHelper.getInstance().toStop(); + + // admin lose-monitor stop + JobCompleteHelper.getInstance().toStop(); + + // admin fail-monitor stop + JobFailMonitorHelper.getInstance().toStop(); + + // admin registry stop + JobRegistryHelper.getInstance().toStop(); + + // admin trigger pool stop + JobTriggerPoolHelper.toStop(); + + } + + // ---------------------- I18n ---------------------- + + private void initI18n(){ + for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) { + item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name()))); + } + } + + // ---------------------- executor-client ---------------------- + private static ConcurrentMap executorBizRepository = new ConcurrentHashMap(); + public static ExecutorBiz getExecutorBiz(String address) throws Exception { + // valid + if (address==null || address.trim().length()==0) { + return null; + } + + // load-cache + address = address.trim(); + ExecutorBiz executorBiz = executorBizRepository.get(address); + if (executorBiz != null) { + return executorBiz; + } + + // set-cache + executorBiz = new ExecutorBizClient(address, XxlJobAdminConfig.getAdminConfig().getAccessToken()); + + executorBizRepository.put(address, executorBiz); + return executorBiz; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java new file mode 100644 index 00000000..5698926a --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobCompleteHelper.java @@ -0,0 +1,184 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.complete.XxlJobCompleter; +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.*; + +/** + * job lose-monitor instance + * + * @author xuxueli 2015-9-1 18:05:56 + */ +public class JobCompleteHelper { + private static Logger logger = LoggerFactory.getLogger(JobCompleteHelper.class); + + private static JobCompleteHelper instance = new JobCompleteHelper(); + public static JobCompleteHelper getInstance(){ + return instance; + } + + // ---------------------- monitor ---------------------- + + private ThreadPoolExecutor callbackThreadPool = null; + private Thread monitorThread; + private volatile boolean toStop = false; + public void start(){ + + // for callback + callbackThreadPool = new ThreadPoolExecutor( + 2, + 20, + 30L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(3000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobLosedMonitorHelper-callbackThreadPool-" + r.hashCode()); + } + }, + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + r.run(); + logger.warn(">>>>>>>>>>> xxl-job, callback too fast, match threadpool rejected handler(run now)."); + } + }); + + + // for monitor + monitorThread = new Thread(new Runnable() { + + @Override + public void run() { + + // wait for JobTriggerPoolHelper-init + try { + TimeUnit.MILLISECONDS.sleep(50); + } catch (InterruptedException e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + // monitor + while (!toStop) { + try { + // 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败; + Date losedTime = DateUtil.addMinutes(new Date(), -10); + List losedJobIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLostJobIds(losedTime); + + if (losedJobIds!=null && losedJobIds.size()>0) { + for (Long logId: losedJobIds) { + + XxlJobLog jobLog = new XxlJobLog(); + jobLog.setId(logId); + + jobLog.setHandleTime(new Date()); + jobLog.setHandleCode(ReturnT.FAIL_CODE); + jobLog.setHandleMsg( I18nUtil.getString("joblog_lost_fail") ); + + XxlJobCompleter.updateHandleInfoAndFinish(jobLog); + } + + } + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e); + } + } + + try { + TimeUnit.SECONDS.sleep(60); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, JobLosedMonitorHelper stop"); + + } + }); + monitorThread.setDaemon(true); + monitorThread.setName("xxl-job, admin JobLosedMonitorHelper"); + monitorThread.start(); + } + + public void toStop(){ + toStop = true; + + // stop registryOrRemoveThreadPool + callbackThreadPool.shutdownNow(); + + // stop monitorThread (interrupt and wait) + monitorThread.interrupt(); + try { + monitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + + // ---------------------- helper ---------------------- + + public ReturnT callback(List callbackParamList) { + + callbackThreadPool.execute(new Runnable() { + @Override + public void run() { + for (HandleCallbackParam handleCallbackParam: callbackParamList) { + ReturnT callbackResult = callback(handleCallbackParam); + logger.debug(">>>>>>>>> JobApiController.callback {}, handleCallbackParam={}, callbackResult={}", + (callbackResult.getCode()== ReturnT.SUCCESS_CODE?"success":"fail"), handleCallbackParam, callbackResult); + } + } + }); + + return ReturnT.SUCCESS; + } + + private ReturnT callback(HandleCallbackParam handleCallbackParam) { + // valid log item + XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(handleCallbackParam.getLogId()); + if (log == null) { + return new ReturnT(ReturnT.FAIL_CODE, "log item not found."); + } + if (log.getHandleCode() > 0) { + return new ReturnT(ReturnT.FAIL_CODE, "log repeate callback."); // avoid repeat callback, trigger child job etc + } + + // handle msg + StringBuffer handleMsg = new StringBuffer(); + if (log.getHandleMsg()!=null) { + handleMsg.append(log.getHandleMsg()).append("
"); + } + if (handleCallbackParam.getHandleMsg() != null) { + handleMsg.append(handleCallbackParam.getHandleMsg()); + } + + // success, save log + log.setHandleTime(new Date()); + log.setHandleCode(handleCallbackParam.getHandleCode()); + log.setHandleMsg(handleMsg.toString()); + XxlJobCompleter.updateHandleInfoAndFinish(log); + + return ReturnT.SUCCESS; + } + + + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java new file mode 100644 index 00000000..8409d7b3 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobFailMonitorHelper.java @@ -0,0 +1,110 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.util.I18nUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * job monitor instance + * + * @author xuxueli 2015-9-1 18:05:56 + */ +public class JobFailMonitorHelper { + private static Logger logger = LoggerFactory.getLogger(JobFailMonitorHelper.class); + + private static JobFailMonitorHelper instance = new JobFailMonitorHelper(); + public static JobFailMonitorHelper getInstance(){ + return instance; + } + + // ---------------------- monitor ---------------------- + + private Thread monitorThread; + private volatile boolean toStop = false; + public void start(){ + monitorThread = new Thread(new Runnable() { + + @Override + public void run() { + + // monitor + while (!toStop) { + try { + + List failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000); + if (failLogIds!=null && !failLogIds.isEmpty()) { + for (long failLogId: failLogIds) { + + // lock log + int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, 0, -1); + if (lockRet < 1) { + continue; + } + XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId); + XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(log.getJobId()); + + // 1、fail retry monitor + if (log.getExecutorFailRetryCount() > 0) { + JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), null); + String retryMsg = "

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_type_retry") +"<<<<<<<<<<<
"; + log.setTriggerMsg(log.getTriggerMsg() + retryMsg); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(log); + } + + // 2、fail alarm monitor + int newAlarmStatus = 0; // 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败 + if (info != null) { + boolean alarmResult = XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log); + newAlarmStatus = alarmResult?2:3; + } else { + newAlarmStatus = 1; + } + + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, -1, newAlarmStatus); + } + } + + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e); + } + } + + try { + TimeUnit.SECONDS.sleep(10); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, job fail monitor thread stop"); + + } + }); + monitorThread.setDaemon(true); + monitorThread.setName("xxl-job, admin JobFailMonitorHelper"); + monitorThread.start(); + } + + public void toStop(){ + toStop = true; + // interrupt and wait + monitorThread.interrupt(); + try { + monitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java new file mode 100644 index 00000000..2387a0c4 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobLogReportHelper.java @@ -0,0 +1,152 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobLogReport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * job log report helper + * + * @author xuxueli 2019-11-22 + */ +public class JobLogReportHelper { + private static Logger logger = LoggerFactory.getLogger(JobLogReportHelper.class); + + private static JobLogReportHelper instance = new JobLogReportHelper(); + public static JobLogReportHelper getInstance(){ + return instance; + } + + + private Thread logrThread; + private volatile boolean toStop = false; + public void start(){ + logrThread = new Thread(new Runnable() { + + @Override + public void run() { + + // last clean log time + long lastCleanLogTime = 0; + + + while (!toStop) { + + // 1、log-report refresh: refresh log report in 3 days + try { + + for (int i = 0; i < 3; i++) { + + // today + Calendar itemDay = Calendar.getInstance(); + itemDay.add(Calendar.DAY_OF_MONTH, -i); + itemDay.set(Calendar.HOUR_OF_DAY, 0); + itemDay.set(Calendar.MINUTE, 0); + itemDay.set(Calendar.SECOND, 0); + itemDay.set(Calendar.MILLISECOND, 0); + + Date todayFrom = itemDay.getTime(); + + itemDay.set(Calendar.HOUR_OF_DAY, 23); + itemDay.set(Calendar.MINUTE, 59); + itemDay.set(Calendar.SECOND, 59); + itemDay.set(Calendar.MILLISECOND, 999); + + Date todayTo = itemDay.getTime(); + + // refresh log-report every minute + XxlJobLogReport xxlJobLogReport = new XxlJobLogReport(); + xxlJobLogReport.setTriggerDay(todayFrom); + xxlJobLogReport.setRunningCount(0); + xxlJobLogReport.setSucCount(0); + xxlJobLogReport.setFailCount(0); + + Map triggerCountMap = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLogReport(todayFrom, todayTo); + if (triggerCountMap!=null && triggerCountMap.size()>0) { + int triggerDayCount = triggerCountMap.containsKey("triggerDayCount")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCount"))):0; + int triggerDayCountRunning = triggerCountMap.containsKey("triggerDayCountRunning")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountRunning"))):0; + int triggerDayCountSuc = triggerCountMap.containsKey("triggerDayCountSuc")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountSuc"))):0; + int triggerDayCountFail = triggerDayCount - triggerDayCountRunning - triggerDayCountSuc; + + xxlJobLogReport.setRunningCount(triggerDayCountRunning); + xxlJobLogReport.setSucCount(triggerDayCountSuc); + xxlJobLogReport.setFailCount(triggerDayCountFail); + } + + // do refresh + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().update(xxlJobLogReport); + if (ret < 1) { + XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().save(xxlJobLogReport); + } + } + + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job log report thread error:{}", e); + } + } + + // 2、log-clean: switch open & once each day + if (XxlJobAdminConfig.getAdminConfig().getLogretentiondays()>0 + && System.currentTimeMillis() - lastCleanLogTime > 24*60*60*1000) { + + // expire-time + Calendar expiredDay = Calendar.getInstance(); + expiredDay.add(Calendar.DAY_OF_MONTH, -1 * XxlJobAdminConfig.getAdminConfig().getLogretentiondays()); + expiredDay.set(Calendar.HOUR_OF_DAY, 0); + expiredDay.set(Calendar.MINUTE, 0); + expiredDay.set(Calendar.SECOND, 0); + expiredDay.set(Calendar.MILLISECOND, 0); + Date clearBeforeTime = expiredDay.getTime(); + + // clean expired log + List logIds = null; + do { + logIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findClearLogIds(0, 0, clearBeforeTime, 0, 1000); + if (logIds!=null && logIds.size()>0) { + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().clearLog(logIds); + } + } while (logIds!=null && logIds.size()>0); + + // update clean time + lastCleanLogTime = System.currentTimeMillis(); + } + + try { + TimeUnit.MINUTES.sleep(1); + } catch (Exception e) { + if (!toStop) { + logger.error(e.getMessage(), e); + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, job log report thread stop"); + + } + }); + logrThread.setDaemon(true); + logrThread.setName("xxl-job, admin JobLogReportHelper"); + logrThread.start(); + } + + public void toStop(){ + toStop = true; + // interrupt and wait + logrThread.interrupt(); + try { + logrThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java new file mode 100644 index 00000000..37edfd98 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobRegistryHelper.java @@ -0,0 +1,204 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobRegistry; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.RegistryConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.concurrent.*; + +/** + * job registry instance + * @author xuxueli 2016-10-02 19:10:24 + */ +public class JobRegistryHelper { + private static Logger logger = LoggerFactory.getLogger(JobRegistryHelper.class); + + private static JobRegistryHelper instance = new JobRegistryHelper(); + public static JobRegistryHelper getInstance(){ + return instance; + } + + private ThreadPoolExecutor registryOrRemoveThreadPool = null; + private Thread registryMonitorThread; + private volatile boolean toStop = false; + + public void start(){ + + // for registry or remove + registryOrRemoveThreadPool = new ThreadPoolExecutor( + 2, + 10, + 30L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(2000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobRegistryMonitorHelper-registryOrRemoveThreadPool-" + r.hashCode()); + } + }, + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + r.run(); + logger.warn(">>>>>>>>>>> xxl-job, registry or remove too fast, match threadpool rejected handler(run now)."); + } + }); + + // for monitor + registryMonitorThread = new Thread(new Runnable() { + @Override + public void run() { + while (!toStop) { + try { + // auto registry group + List groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0); + if (groupList!=null && !groupList.isEmpty()) { + + // remove dead address (admin/executor) + List ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (ids!=null && ids.size()>0) { + XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids); + } + + // fresh online address (admin/executor) + HashMap> appAddressMap = new HashMap>(); + List list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date()); + if (list != null) { + for (XxlJobRegistry item: list) { + if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) { + String appname = item.getRegistryKey(); + List registryList = appAddressMap.get(appname); + if (registryList == null) { + registryList = new ArrayList(); + } + + if (!registryList.contains(item.getRegistryValue())) { + registryList.add(item.getRegistryValue()); + } + appAddressMap.put(appname, registryList); + } + } + } + + // fresh group address + for (XxlJobGroup group: groupList) { + List registryList = appAddressMap.get(group.getAppname()); + String addressListStr = null; + if (registryList!=null && !registryList.isEmpty()) { + Collections.sort(registryList); + StringBuilder addressListSB = new StringBuilder(); + for (String item:registryList) { + addressListSB.append(item).append(","); + } + addressListStr = addressListSB.toString(); + addressListStr = addressListStr.substring(0, addressListStr.length()-1); + } + group.setAddressList(addressListStr); + group.setUpdateTime(new Date()); + + XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group); + } + } + } catch (Exception e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e); + } + } + try { + TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT); + } catch (InterruptedException e) { + if (!toStop) { + logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e); + } + } + } + logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop"); + } + }); + registryMonitorThread.setDaemon(true); + registryMonitorThread.setName("xxl-job, admin JobRegistryMonitorHelper-registryMonitorThread"); + registryMonitorThread.start(); + } + + public void toStop(){ + toStop = true; + + // stop registryOrRemoveThreadPool + registryOrRemoveThreadPool.shutdownNow(); + + // stop monitir (interrupt and wait) + registryMonitorThread.interrupt(); + try { + registryMonitorThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + + // ---------------------- helper ---------------------- + + public ReturnT registry(RegistryParam registryParam) { + + // valid + if (!StringUtils.hasText(registryParam.getRegistryGroup()) + || !StringUtils.hasText(registryParam.getRegistryKey()) + || !StringUtils.hasText(registryParam.getRegistryValue())) { + return new ReturnT(ReturnT.FAIL_CODE, "Illegal Argument."); + } + + // async execute + registryOrRemoveThreadPool.execute(new Runnable() { + @Override + public void run() { + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date()); + if (ret < 1) { + XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date()); + + // fresh + freshGroupRegistryInfo(registryParam); + } + } + }); + + return ReturnT.SUCCESS; + } + + public ReturnT registryRemove(RegistryParam registryParam) { + + // valid + if (!StringUtils.hasText(registryParam.getRegistryGroup()) + || !StringUtils.hasText(registryParam.getRegistryKey()) + || !StringUtils.hasText(registryParam.getRegistryValue())) { + return new ReturnT(ReturnT.FAIL_CODE, "Illegal Argument."); + } + + // async execute + registryOrRemoveThreadPool.execute(new Runnable() { + @Override + public void run() { + int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryDelete(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue()); + if (ret > 0) { + // fresh + freshGroupRegistryInfo(registryParam); + } + } + }); + + return ReturnT.SUCCESS; + } + + private void freshGroupRegistryInfo(RegistryParam registryParam){ + // Under consideration, prevent affecting core tables + } + + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java new file mode 100644 index 00000000..831bcf6a --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobScheduleHelper.java @@ -0,0 +1,369 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.cron.CronExpression; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * @author xuxueli 2019-05-21 + */ +public class JobScheduleHelper { + private static Logger logger = LoggerFactory.getLogger(JobScheduleHelper.class); + + private static JobScheduleHelper instance = new JobScheduleHelper(); + public static JobScheduleHelper getInstance(){ + return instance; + } + + public static final long PRE_READ_MS = 5000; // pre read + + private Thread scheduleThread; + private Thread ringThread; + private volatile boolean scheduleThreadToStop = false; + private volatile boolean ringThreadToStop = false; + private volatile static Map> ringData = new ConcurrentHashMap<>(); + + public void start(){ + + // schedule thread + scheduleThread = new Thread(new Runnable() { + @Override + public void run() { + + try { + TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 ); + } catch (InterruptedException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + logger.info(">>>>>>>>> init xxl-job admin scheduler success."); + + // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20) + int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20; + + while (!scheduleThreadToStop) { + + // Scan Job + long start = System.currentTimeMillis(); + + Connection conn = null; + Boolean connAutoCommit = null; + PreparedStatement preparedStatement = null; + + boolean preReadSuc = true; + try { + + conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection(); + connAutoCommit = conn.getAutoCommit(); + conn.setAutoCommit(false); + + preparedStatement = conn.prepareStatement( "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" ); + preparedStatement.execute(); + + // tx start + + // 1、pre read + long nowTime = System.currentTimeMillis(); + List scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount); + if (scheduleList!=null && scheduleList.size()>0) { + // 2、push time-ring + for (XxlJobInfo jobInfo: scheduleList) { + + // time-ring jump + if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) { + // 2.1、trigger-expire > 5s:pass && make next-trigger-time + logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId()); + + // 1、misfire match + MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING); + if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) { + // FIRE_ONCE_NOW 》 trigger + JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null); + logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() ); + } + + // 2、fresh next + refreshNextValidTime(jobInfo, new Date()); + + } else if (nowTime > jobInfo.getTriggerNextTime()) { + // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time + + // 1、trigger + JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null); + logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() ); + + // 2、fresh next + refreshNextValidTime(jobInfo, new Date()); + + // next-trigger-time in 5s, pre-read again + if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) { + + // 1、make ring second + int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); + + // 2、push time ring + pushTimeRing(ringSecond, jobInfo.getId()); + + // 3、fresh next + refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); + + } + + } else { + // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time + + // 1、make ring second + int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); + + // 2、push time ring + pushTimeRing(ringSecond, jobInfo.getId()); + + // 3、fresh next + refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); + + } + + } + + // 3、update trigger info + for (XxlJobInfo jobInfo: scheduleList) { + XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo); + } + + } else { + preReadSuc = false; + } + + // tx stop + + + } catch (Exception e) { + if (!scheduleThreadToStop) { + logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e); + } + } finally { + + // commit + if (conn != null) { + try { + conn.commit(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + try { + conn.setAutoCommit(connAutoCommit); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + try { + conn.close(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + + // close PreparedStatement + if (null != preparedStatement) { + try { + preparedStatement.close(); + } catch (SQLException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + } + long cost = System.currentTimeMillis()-start; + + + // Wait seconds, align second + if (cost < 1000) { // scan-overtime, not wait + try { + // pre-read period: success > scan each second; fail > skip this period; + TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000); + } catch (InterruptedException e) { + if (!scheduleThreadToStop) { + logger.error(e.getMessage(), e); + } + } + } + + } + + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop"); + } + }); + scheduleThread.setDaemon(true); + scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread"); + scheduleThread.start(); + + + // ring thread + ringThread = new Thread(new Runnable() { + @Override + public void run() { + + while (!ringThreadToStop) { + + // align second + try { + TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000); + } catch (InterruptedException e) { + if (!ringThreadToStop) { + logger.error(e.getMessage(), e); + } + } + + try { + // second data + List ringItemData = new ArrayList<>(); + int nowSecond = Calendar.getInstance().get(Calendar.SECOND); // 避免处理耗时太长,跨过刻度,向前校验一个刻度; + for (int i = 0; i < 2; i++) { + List tmpData = ringData.remove( (nowSecond+60-i)%60 ); + if (tmpData != null) { + ringItemData.addAll(tmpData); + } + } + + // ring trigger + logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) ); + if (ringItemData.size() > 0) { + // do trigger + for (int jobId: ringItemData) { + // do trigger + JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null); + } + // clear + ringItemData.clear(); + } + } catch (Exception e) { + if (!ringThreadToStop) { + logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e); + } + } + } + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop"); + } + }); + ringThread.setDaemon(true); + ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread"); + ringThread.start(); + } + + private void refreshNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception { + Date nextValidTime = generateNextValidTime(jobInfo, fromTime); + if (nextValidTime != null) { + jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime()); + jobInfo.setTriggerNextTime(nextValidTime.getTime()); + } else { + jobInfo.setTriggerStatus(0); + jobInfo.setTriggerLastTime(0); + jobInfo.setTriggerNextTime(0); + logger.warn(">>>>>>>>>>> xxl-job, refreshNextValidTime fail for job: jobId={}, scheduleType={}, scheduleConf={}", + jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf()); + } + } + + private void pushTimeRing(int ringSecond, int jobId){ + // push async ring + List ringItemData = ringData.get(ringSecond); + if (ringItemData == null) { + ringItemData = new ArrayList(); + ringData.put(ringSecond, ringItemData); + } + ringItemData.add(jobId); + + logger.debug(">>>>>>>>>>> xxl-job, schedule push time-ring : " + ringSecond + " = " + Arrays.asList(ringItemData) ); + } + + public void toStop(){ + + // 1、stop schedule + scheduleThreadToStop = true; + try { + TimeUnit.SECONDS.sleep(1); // wait + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + if (scheduleThread.getState() != Thread.State.TERMINATED){ + // interrupt and wait + scheduleThread.interrupt(); + try { + scheduleThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + // if has ring data + boolean hasRingData = false; + if (!ringData.isEmpty()) { + for (int second : ringData.keySet()) { + List tmpData = ringData.get(second); + if (tmpData!=null && tmpData.size()>0) { + hasRingData = true; + break; + } + } + } + if (hasRingData) { + try { + TimeUnit.SECONDS.sleep(8); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + // stop ring (wait job-in-memory stop) + ringThreadToStop = true; + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + if (ringThread.getState() != Thread.State.TERMINATED){ + // interrupt and wait + ringThread.interrupt(); + try { + ringThread.join(); + } catch (InterruptedException e) { + logger.error(e.getMessage(), e); + } + } + + logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper stop"); + } + + + // ---------------------- tools ---------------------- + public static Date generateNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception { + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (ScheduleTypeEnum.CRON == scheduleTypeEnum) { + Date nextValidTime = new CronExpression(jobInfo.getScheduleConf()).getNextValidTimeAfter(fromTime); + return nextValidTime; + } else if (ScheduleTypeEnum.FIX_RATE == scheduleTypeEnum /*|| ScheduleTypeEnum.FIX_DELAY == scheduleTypeEnum*/) { + return new Date(fromTime.getTime() + Integer.valueOf(jobInfo.getScheduleConf())*1000 ); + } + return null; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java new file mode 100644 index 00000000..398713dd --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/thread/JobTriggerPoolHelper.java @@ -0,0 +1,150 @@ +package com.xxl.job.admin.core.thread; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.trigger.TriggerTypeEnum; +import com.xxl.job.admin.core.trigger.XxlJobTrigger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * job trigger thread pool helper + * + * @author xuxueli 2018-07-03 21:08:07 + */ +public class JobTriggerPoolHelper { + private static Logger logger = LoggerFactory.getLogger(JobTriggerPoolHelper.class); + + + // ---------------------- trigger pool ---------------------- + + // fast/slow thread pool + private ThreadPoolExecutor fastTriggerPool = null; + private ThreadPoolExecutor slowTriggerPool = null; + + public void start(){ + fastTriggerPool = new ThreadPoolExecutor( + 10, + XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(), + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(1000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode()); + } + }); + + slowTriggerPool = new ThreadPoolExecutor( + 10, + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax(), + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(2000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-slowTriggerPool-" + r.hashCode()); + } + }); + } + + + public void stop() { + //triggerPool.shutdown(); + fastTriggerPool.shutdownNow(); + slowTriggerPool.shutdownNow(); + logger.info(">>>>>>>>> xxl-job trigger thread pool shutdown success."); + } + + + // job timeout count + private volatile long minTim = System.currentTimeMillis()/60000; // ms > min + private volatile ConcurrentMap jobTimeoutCountMap = new ConcurrentHashMap<>(); + + + /** + * add trigger + */ + public void addTrigger(final int jobId, + final TriggerTypeEnum triggerType, + final int failRetryCount, + final String executorShardingParam, + final String executorParam, + final String addressList) { + + // choose thread pool + ThreadPoolExecutor triggerPool_ = fastTriggerPool; + AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId); + if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) { // job-timeout 10 times in 1 min + triggerPool_ = slowTriggerPool; + } + + // trigger + triggerPool_.execute(new Runnable() { + @Override + public void run() { + + long start = System.currentTimeMillis(); + + try { + // do trigger + XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + + // check timeout-count-map + long minTim_now = System.currentTimeMillis()/60000; + if (minTim != minTim_now) { + minTim = minTim_now; + jobTimeoutCountMap.clear(); + } + + // incr timeout-count-map + long cost = System.currentTimeMillis()-start; + if (cost > 500) { // ob-timeout threshold 500ms + AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1)); + if (timeoutCount != null) { + timeoutCount.incrementAndGet(); + } + } + + } + + } + }); + } + + + + // ---------------------- helper ---------------------- + + private static JobTriggerPoolHelper helper = new JobTriggerPoolHelper(); + + public static void toStart() { + helper.start(); + } + public static void toStop() { + helper.stop(); + } + + /** + * @param jobId + * @param triggerType + * @param failRetryCount + * >=0: use this param + * <0: use param from job info config + * @param executorShardingParam + * @param executorParam + * null: use job param + * not null: cover job param + */ + public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) { + helper.addTrigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java new file mode 100644 index 00000000..446c90e9 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/TriggerTypeEnum.java @@ -0,0 +1,27 @@ +package com.xxl.job.admin.core.trigger; + +import com.xxl.job.admin.core.util.I18nUtil; + +/** + * trigger type enum + * + * @author xuxueli 2018-09-16 04:56:41 + */ +public enum TriggerTypeEnum { + + MANUAL(I18nUtil.getString("jobconf_trigger_type_manual")), + CRON(I18nUtil.getString("jobconf_trigger_type_cron")), + RETRY(I18nUtil.getString("jobconf_trigger_type_retry")), + PARENT(I18nUtil.getString("jobconf_trigger_type_parent")), + API(I18nUtil.getString("jobconf_trigger_type_api")), + MISFIRE(I18nUtil.getString("jobconf_trigger_type_misfire")); + + private TriggerTypeEnum(String title){ + this.title = title; + } + private String title; + public String getTitle() { + return title; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java new file mode 100644 index 00000000..748befc6 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/trigger/XxlJobTrigger.java @@ -0,0 +1,226 @@ +package com.xxl.job.admin.core.trigger; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLog; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.XxlJobScheduler; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.core.biz.ExecutorBiz; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.biz.model.TriggerParam; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.util.IpUtil; +import com.xxl.job.core.util.ThrowableUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +/** + * xxl-job trigger + * Created by xuxueli on 17/7/13. + */ +public class XxlJobTrigger { + private static Logger logger = LoggerFactory.getLogger(XxlJobTrigger.class); + + /** + * trigger job + * + * @param jobId + * @param triggerType + * @param failRetryCount + * >=0: use this param + * <0: use param from job info config + * @param executorShardingParam + * @param executorParam + * null: use job param + * not null: cover job param + * @param addressList + * null: use executor addressList + * not null: cover + */ + public static void trigger(int jobId, + TriggerTypeEnum triggerType, + int failRetryCount, + String executorShardingParam, + String executorParam, + String addressList) { + + // load data + XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(jobId); + if (jobInfo == null) { + logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId); + return; + } + if (executorParam != null) { + jobInfo.setExecutorParam(executorParam); + } + int finalFailRetryCount = failRetryCount>=0?failRetryCount:jobInfo.getExecutorFailRetryCount(); + XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup()); + + // cover addressList + if (addressList!=null && addressList.trim().length()>0) { + group.setAddressType(1); + group.setAddressList(addressList.trim()); + } + + // sharding param + int[] shardingParam = null; + if (executorShardingParam!=null){ + String[] shardingArr = executorShardingParam.split("/"); + if (shardingArr.length==2 && isNumeric(shardingArr[0]) && isNumeric(shardingArr[1])) { + shardingParam = new int[2]; + shardingParam[0] = Integer.valueOf(shardingArr[0]); + shardingParam[1] = Integer.valueOf(shardingArr[1]); + } + } + if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) + && group.getRegistryList()!=null && !group.getRegistryList().isEmpty() + && shardingParam==null) { + for (int i = 0; i < group.getRegistryList().size(); i++) { + processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size()); + } + } else { + if (shardingParam == null) { + shardingParam = new int[]{0, 1}; + } + processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]); + } + + } + + private static boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * @param group job group, registry list may be empty + * @param jobInfo + * @param finalFailRetryCount + * @param triggerType + * @param index sharding index + * @param total sharding index + */ + private static void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total){ + + // param + ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION); // block strategy + ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null); // route strategy + String shardingParam = (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==executorRouteStrategyEnum)?String.valueOf(index).concat("/").concat(String.valueOf(total)):null; + + // 1、save log-id + XxlJobLog jobLog = new XxlJobLog(); + jobLog.setJobGroup(jobInfo.getJobGroup()); + jobLog.setJobId(jobInfo.getId()); + jobLog.setTriggerTime(new Date()); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().save(jobLog); + logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId()); + + // 2、init trigger-param + TriggerParam triggerParam = new TriggerParam(); + triggerParam.setJobId(jobInfo.getId()); + triggerParam.setExecutorHandler(jobInfo.getExecutorHandler()); + triggerParam.setExecutorParams(jobInfo.getExecutorParam()); + triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy()); + triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout()); + triggerParam.setLogId(jobLog.getId()); + triggerParam.setLogDateTime(jobLog.getTriggerTime().getTime()); + triggerParam.setGlueType(jobInfo.getGlueType()); + triggerParam.setGlueSource(jobInfo.getGlueSource()); + triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime()); + triggerParam.setBroadcastIndex(index); + triggerParam.setBroadcastTotal(total); + + // 3、init address + String address = null; + ReturnT routeAddressResult = null; + if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) { + if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) { + if (index < group.getRegistryList().size()) { + address = group.getRegistryList().get(index); + } else { + address = group.getRegistryList().get(0); + } + } else { + routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList()); + if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) { + address = routeAddressResult.getContent(); + } + } + } else { + routeAddressResult = new ReturnT(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty")); + } + + // 4、trigger remote executor + ReturnT triggerResult = null; + if (address != null) { + triggerResult = runExecutor(triggerParam, address); + } else { + triggerResult = new ReturnT(ReturnT.FAIL_CODE, null); + } + + // 5、collection trigger info + StringBuffer triggerMsgSb = new StringBuffer(); + triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_type")).append(":").append(triggerType.getTitle()); + triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp()); + triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":") + .append( (group.getAddressType() == 0)?I18nUtil.getString("jobgroup_field_addressType_0"):I18nUtil.getString("jobgroup_field_addressType_1") ); + triggerMsgSb.append("
").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList()); + triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle()); + if (shardingParam != null) { + triggerMsgSb.append("("+shardingParam+")"); + } + triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle()); + triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout()); + triggerMsgSb.append("
").append(I18nUtil.getString("jobinfo_field_executorFailRetryCount")).append(":").append(finalFailRetryCount); + + triggerMsgSb.append("

>>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<<
") + .append((routeAddressResult!=null&&routeAddressResult.getMsg()!=null)?routeAddressResult.getMsg()+"

":"").append(triggerResult.getMsg()!=null?triggerResult.getMsg():""); + + // 6、save log trigger-info + jobLog.setExecutorAddress(address); + jobLog.setExecutorHandler(jobInfo.getExecutorHandler()); + jobLog.setExecutorParam(jobInfo.getExecutorParam()); + jobLog.setExecutorShardingParam(shardingParam); + jobLog.setExecutorFailRetryCount(finalFailRetryCount); + //jobLog.setTriggerTime(); + jobLog.setTriggerCode(triggerResult.getCode()); + jobLog.setTriggerMsg(triggerMsgSb.toString()); + XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(jobLog); + + logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId()); + } + + /** + * run executor + * @param triggerParam + * @param address + * @return + */ + public static ReturnT runExecutor(TriggerParam triggerParam, String address){ + ReturnT runResult = null; + try { + ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); + runResult = executorBiz.run(triggerParam); + } catch (Exception e) { + logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e); + runResult = new ReturnT(ReturnT.FAIL_CODE, ThrowableUtil.toString(e)); + } + + StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":"); + runResultSB.append("
address:").append(address); + runResultSB.append("
code:").append(runResult.getCode()); + runResultSB.append("
msg:").append(runResult.getMsg()); + + runResult.setMsg(runResultSB.toString()); + return runResult; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java new file mode 100644 index 00000000..a1523aa4 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/CookieUtil.java @@ -0,0 +1,98 @@ +package com.xxl.job.admin.core.util; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Cookie.Util + * + * @author xuxueli 2015-12-12 18:01:06 + */ +public class CookieUtil { + + // 默认缓存时间,单位/秒, 2H + private static final int COOKIE_MAX_AGE = Integer.MAX_VALUE; + // 保存路径,根路径 + private static final String COOKIE_PATH = "/"; + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param ifRemember + */ + public static void set(HttpServletResponse response, String key, String value, boolean ifRemember) { + int age = ifRemember?COOKIE_MAX_AGE:-1; + set(response, key, value, null, COOKIE_PATH, age, true); + } + + /** + * 保存 + * + * @param response + * @param key + * @param value + * @param maxAge + */ + private static void set(HttpServletResponse response, String key, String value, String domain, String path, int maxAge, boolean isHttpOnly) { + Cookie cookie = new Cookie(key, value); + if (domain != null) { + cookie.setDomain(domain); + } + cookie.setPath(path); + cookie.setMaxAge(maxAge); + cookie.setHttpOnly(isHttpOnly); + response.addCookie(cookie); + } + + /** + * 查询value + * + * @param request + * @param key + * @return + */ + public static String getValue(HttpServletRequest request, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + return cookie.getValue(); + } + return null; + } + + /** + * 查询Cookie + * + * @param request + * @param key + */ + private static Cookie get(HttpServletRequest request, String key) { + Cookie[] arr_cookie = request.getCookies(); + if (arr_cookie != null && arr_cookie.length > 0) { + for (Cookie cookie : arr_cookie) { + if (cookie.getName().equals(key)) { + return cookie; + } + } + } + return null; + } + + /** + * 删除Cookie + * + * @param request + * @param response + * @param key + */ + public static void remove(HttpServletRequest request, HttpServletResponse response, String key) { + Cookie cookie = get(request, key); + if (cookie != null) { + set(response, key, "", null, COOKIE_PATH, 0, true); + } + } + +} \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java new file mode 100644 index 00000000..e90af434 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/FtlUtil.java @@ -0,0 +1,31 @@ +package com.xxl.job.admin.core.util; + +import freemarker.ext.beans.BeansWrapper; +import freemarker.ext.beans.BeansWrapperBuilder; +import freemarker.template.Configuration; +import freemarker.template.TemplateHashModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ftl util + * + * @author xuxueli 2018-01-17 20:37:48 + */ +public class FtlUtil { + private static Logger logger = LoggerFactory.getLogger(FtlUtil.class); + + private static BeansWrapper wrapper = new BeansWrapperBuilder(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS).build(); //BeansWrapper.getDefaultInstance(); + + public static TemplateHashModel generateStaticModel(String packageName) { + try { + TemplateHashModel staticModels = wrapper.getStaticModels(); + TemplateHashModel fileStatics = (TemplateHashModel) staticModels.get(packageName); + return fileStatics; + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + return null; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java new file mode 100644 index 00000000..772a96ec --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/I18nUtil.java @@ -0,0 +1,79 @@ +package com.xxl.job.admin.core.util; + +import com.xxl.job.admin.core.conf.XxlJobAdminConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * i18n util + * + * @author xuxueli 2018-01-17 20:39:06 + */ +public class I18nUtil { + private static Logger logger = LoggerFactory.getLogger(I18nUtil.class); + + private static Properties prop = null; + public static Properties loadI18nProp(){ + if (prop != null) { + return prop; + } + try { + // build i18n prop + String i18n = XxlJobAdminConfig.getAdminConfig().getI18n(); + String i18nFile = MessageFormat.format("i18n/message_{0}.properties", i18n); + + // load prop + Resource resource = new ClassPathResource(i18nFile); + EncodedResource encodedResource = new EncodedResource(resource,"UTF-8"); + prop = PropertiesLoaderUtils.loadProperties(encodedResource); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return prop; + } + + /** + * get val of i18n key + * + * @param key + * @return + */ + public static String getString(String key) { + return loadI18nProp().getProperty(key); + } + + /** + * get mult val of i18n mult key, as json + * + * @param keys + * @return + */ + public static String getMultString(String... keys) { + Map map = new HashMap(); + + Properties prop = loadI18nProp(); + if (keys!=null && keys.length>0) { + for (String key: keys) { + map.put(key, prop.getProperty(key)); + } + } else { + for (String key: prop.stringPropertyNames()) { + map.put(key, prop.getProperty(key)); + } + } + + String json = JacksonUtil.writeValueAsString(map); + return json; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java new file mode 100644 index 00000000..4f4ea3cc --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/JacksonUtil.java @@ -0,0 +1,92 @@ +package com.xxl.job.admin.core.util; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Jackson util + * + * 1、obj need private and set/get; + * 2、do not support inner class; + * + * @author xuxueli 2015-9-25 18:02:56 + */ +public class JacksonUtil { + private static Logger logger = LoggerFactory.getLogger(JacksonUtil.class); + + private final static ObjectMapper objectMapper = new ObjectMapper(); + public static ObjectMapper getInstance() { + return objectMapper; + } + + /** + * bean、array、List、Map --> json + * + * @param obj + * @return json string + * @throws Exception + */ + public static String writeValueAsString(Object obj) { + try { + return getInstance().writeValueAsString(obj); + } catch (JsonGenerationException e) { + logger.error(e.getMessage(), e); + } catch (JsonMappingException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return null; + } + + /** + * string --> bean、Map、List(array) + * + * @param jsonStr + * @param clazz + * @return obj + * @throws Exception + */ + public static T readValue(String jsonStr, Class clazz) { + try { + return getInstance().readValue(jsonStr, clazz); + } catch (JsonParseException e) { + logger.error(e.getMessage(), e); + } catch (JsonMappingException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return null; + } + + /** + * string --> List... + * + * @param jsonStr + * @param parametrized + * @param parameterClasses + * @param + * @return + */ + public static T readValue(String jsonStr, Class parametrized, Class... parameterClasses) { + try { + JavaType javaType = getInstance().getTypeFactory().constructParametricType(parametrized, parameterClasses); + return getInstance().readValue(jsonStr, javaType); + } catch (JsonParseException e) { + logger.error(e.getMessage(), e); + } catch (JsonMappingException e) { + logger.error(e.getMessage(), e); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return null; + } +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java new file mode 100644 index 00000000..fbab0613 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/core/util/LocalCacheUtil.java @@ -0,0 +1,133 @@ +package com.xxl.job.admin.core.util; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * local cache tool + * + * @author xuxueli 2018-01-22 21:37:34 + */ +public class LocalCacheUtil { + + private static ConcurrentMap cacheRepository = new ConcurrentHashMap(); // 类型建议用抽象父类,兼容性更好; + private static class LocalCacheData{ + private String key; + private Object val; + private long timeoutTime; + + public LocalCacheData() { + } + + public LocalCacheData(String key, Object val, long timeoutTime) { + this.key = key; + this.val = val; + this.timeoutTime = timeoutTime; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Object getVal() { + return val; + } + + public void setVal(Object val) { + this.val = val; + } + + public long getTimeoutTime() { + return timeoutTime; + } + + public void setTimeoutTime(long timeoutTime) { + this.timeoutTime = timeoutTime; + } + } + + + /** + * set cache + * + * @param key + * @param val + * @param cacheTime + * @return + */ + public static boolean set(String key, Object val, long cacheTime){ + + // clean timeout cache, before set new cache (avoid cache too much) + cleanTimeoutCache(); + + // set new cache + if (key==null || key.trim().length()==0) { + return false; + } + if (val == null) { + remove(key); + } + if (cacheTime <= 0) { + remove(key); + } + long timeoutTime = System.currentTimeMillis() + cacheTime; + LocalCacheData localCacheData = new LocalCacheData(key, val, timeoutTime); + cacheRepository.put(localCacheData.getKey(), localCacheData); + return true; + } + + /** + * remove cache + * + * @param key + * @return + */ + public static boolean remove(String key){ + if (key==null || key.trim().length()==0) { + return false; + } + cacheRepository.remove(key); + return true; + } + + /** + * get cache + * + * @param key + * @return + */ + public static Object get(String key){ + if (key==null || key.trim().length()==0) { + return null; + } + LocalCacheData localCacheData = cacheRepository.get(key); + if (localCacheData!=null && System.currentTimeMillis()=localCacheData.getTimeoutTime()) { + cacheRepository.remove(key); + } + } + } + return true; + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java new file mode 100644 index 00000000..b608d9fb --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobGroupDao.java @@ -0,0 +1,37 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobGroup; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +@Mapper +public interface XxlJobGroupDao { + + public List findAll(); + + public List findByAddressType(@Param("addressType") int addressType); + + public int save(XxlJobGroup xxlJobGroup); + + public int update(XxlJobGroup xxlJobGroup); + + public int remove(@Param("id") int id); + + public XxlJobGroup load(@Param("id") int id); + + public List pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("appname") String appname, + @Param("title") String title); + + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("appname") String appname, + @Param("title") String title); + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java new file mode 100644 index 00000000..d640efff --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobInfoDao.java @@ -0,0 +1,49 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobInfo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + + +/** + * job info + * @author xuxueli 2016-1-12 18:03:45 + */ +@Mapper +public interface XxlJobInfoDao { + + public List pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("triggerStatus") int triggerStatus, + @Param("jobDesc") String jobDesc, + @Param("executorHandler") String executorHandler, + @Param("author") String author); + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("triggerStatus") int triggerStatus, + @Param("jobDesc") String jobDesc, + @Param("executorHandler") String executorHandler, + @Param("author") String author); + + public int save(XxlJobInfo info); + + public XxlJobInfo loadById(@Param("id") int id); + + public int update(XxlJobInfo xxlJobInfo); + + public int delete(@Param("id") long id); + + public List getJobsByGroup(@Param("jobGroup") int jobGroup); + + public int findAllCount(); + + public List scheduleJobQuery(@Param("maxNextTime") long maxNextTime, @Param("pagesize") int pagesize ); + + public int scheduleUpdate(XxlJobInfo xxlJobInfo); + + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java new file mode 100644 index 00000000..62fa3b4f --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogDao.java @@ -0,0 +1,62 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLog; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * job log + * @author xuxueli 2016-1-12 18:03:06 + */ +@Mapper +public interface XxlJobLogDao { + + // exist jobId not use jobGroup, not exist use jobGroup + public List pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("jobId") int jobId, + @Param("triggerTimeStart") Date triggerTimeStart, + @Param("triggerTimeEnd") Date triggerTimeEnd, + @Param("logStatus") int logStatus); + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("jobGroup") int jobGroup, + @Param("jobId") int jobId, + @Param("triggerTimeStart") Date triggerTimeStart, + @Param("triggerTimeEnd") Date triggerTimeEnd, + @Param("logStatus") int logStatus); + + public XxlJobLog load(@Param("id") long id); + + public long save(XxlJobLog xxlJobLog); + + public int updateTriggerInfo(XxlJobLog xxlJobLog); + + public int updateHandleInfo(XxlJobLog xxlJobLog); + + public int delete(@Param("jobId") int jobId); + + public Map findLogReport(@Param("from") Date from, + @Param("to") Date to); + + public List findClearLogIds(@Param("jobGroup") int jobGroup, + @Param("jobId") int jobId, + @Param("clearBeforeTime") Date clearBeforeTime, + @Param("clearBeforeNum") int clearBeforeNum, + @Param("pagesize") int pagesize); + public int clearLog(@Param("logIds") List logIds); + + public List findFailJobLogIds(@Param("pagesize") int pagesize); + + public int updateAlarmStatus(@Param("logId") long logId, + @Param("oldAlarmStatus") int oldAlarmStatus, + @Param("newAlarmStatus") int newAlarmStatus); + + public List findLostJobIds(@Param("losedTime") Date losedTime); + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java new file mode 100644 index 00000000..3028aed2 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogGlueDao.java @@ -0,0 +1,24 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLogGlue; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * job log for glue + * @author xuxueli 2016-5-19 18:04:56 + */ +@Mapper +public interface XxlJobLogGlueDao { + + public int save(XxlJobLogGlue xxlJobLogGlue); + + public List findByJobId(@Param("jobId") int jobId); + + public int removeOld(@Param("jobId") int jobId, @Param("limit") int limit); + + public int deleteByJobId(@Param("jobId") int jobId); + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java new file mode 100644 index 00000000..f4b3dc81 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobLogReportDao.java @@ -0,0 +1,26 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobLogReport; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * job log + * @author xuxueli 2019-11-22 + */ +@Mapper +public interface XxlJobLogReportDao { + + public int save(XxlJobLogReport xxlJobLogReport); + + public int update(XxlJobLogReport xxlJobLogReport); + + public List queryLogReport(@Param("triggerDayFrom") Date triggerDayFrom, + @Param("triggerDayTo") Date triggerDayTo); + + public XxlJobLogReport queryLogReportTotal(); + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java new file mode 100644 index 00000000..1005c46c --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobRegistryDao.java @@ -0,0 +1,38 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobRegistry; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * Created by xuxueli on 16/9/30. + */ +@Mapper +public interface XxlJobRegistryDao { + + public List findDead(@Param("timeout") int timeout, + @Param("nowTime") Date nowTime); + + public int removeDead(@Param("ids") List ids); + + public List findAll(@Param("timeout") int timeout, + @Param("nowTime") Date nowTime); + + public int registryUpdate(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue, + @Param("updateTime") Date updateTime); + + public int registrySave(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue, + @Param("updateTime") Date updateTime); + + public int registryDelete(@Param("registryGroup") String registryGroup, + @Param("registryKey") String registryKey, + @Param("registryValue") String registryValue); + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java new file mode 100644 index 00000000..e8404947 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/dao/XxlJobUserDao.java @@ -0,0 +1,31 @@ +package com.xxl.job.admin.dao; + +import com.xxl.job.admin.core.model.XxlJobUser; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +/** + * @author xuxueli 2019-05-04 16:44:59 + */ +@Mapper +public interface XxlJobUserDao { + + public List pageList(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("username") String username, + @Param("role") int role); + public int pageListCount(@Param("offset") int offset, + @Param("pagesize") int pagesize, + @Param("username") String username, + @Param("role") int role); + + public XxlJobUser loadByUserName(@Param("username") String username); + + public int save(XxlJobUser xxlJobUser); + + public int update(XxlJobUser xxlJobUser); + + public int delete(@Param("id") int id); + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java new file mode 100644 index 00000000..e1cf2e44 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/LoginService.java @@ -0,0 +1,107 @@ +package com.xxl.job.admin.service; + +import com.xxl.job.admin.core.model.XxlJobUser; +import com.xxl.job.admin.core.util.CookieUtil; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.core.util.JacksonUtil; +import com.xxl.job.admin.dao.XxlJobUserDao; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.DigestUtils; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.math.BigInteger; + +/** + * @author xuxueli 2019-05-04 22:13:264 + */ +@Configuration +public class LoginService { + + public static final String LOGIN_IDENTITY_KEY = "XXL_JOB_LOGIN_IDENTITY"; + + @Resource + private XxlJobUserDao xxlJobUserDao; + + + private String makeToken(XxlJobUser xxlJobUser){ + String tokenJson = JacksonUtil.writeValueAsString(xxlJobUser); + String tokenHex = new BigInteger(tokenJson.getBytes()).toString(16); + return tokenHex; + } + private XxlJobUser parseToken(String tokenHex){ + XxlJobUser xxlJobUser = null; + if (tokenHex != null) { + String tokenJson = new String(new BigInteger(tokenHex, 16).toByteArray()); // username_password(md5) + xxlJobUser = JacksonUtil.readValue(tokenJson, XxlJobUser.class); + } + return xxlJobUser; + } + + + public ReturnT login(HttpServletRequest request, HttpServletResponse response, String username, String password, boolean ifRemember){ + + // param + if (username==null || username.trim().length()==0 || password==null || password.trim().length()==0){ + return new ReturnT(500, I18nUtil.getString("login_param_empty")); + } + + // valid passowrd + XxlJobUser xxlJobUser = xxlJobUserDao.loadByUserName(username); + if (xxlJobUser == null) { + return new ReturnT(500, I18nUtil.getString("login_param_unvalid")); + } + String passwordMd5 = DigestUtils.md5DigestAsHex(password.getBytes()); + if (!passwordMd5.equals(xxlJobUser.getPassword())) { + return new ReturnT(500, I18nUtil.getString("login_param_unvalid")); + } + + String loginToken = makeToken(xxlJobUser); + + // do login + CookieUtil.set(response, LOGIN_IDENTITY_KEY, loginToken, ifRemember); + return ReturnT.SUCCESS; + } + + /** + * logout + * + * @param request + * @param response + */ + public ReturnT logout(HttpServletRequest request, HttpServletResponse response){ + CookieUtil.remove(request, response, LOGIN_IDENTITY_KEY); + return ReturnT.SUCCESS; + } + + /** + * logout + * + * @param request + * @return + */ + public XxlJobUser ifLogin(HttpServletRequest request, HttpServletResponse response){ + String cookieToken = CookieUtil.getValue(request, LOGIN_IDENTITY_KEY); + if (cookieToken != null) { + XxlJobUser cookieUser = null; + try { + cookieUser = parseToken(cookieToken); + } catch (Exception e) { + logout(request, response); + } + if (cookieUser != null) { + XxlJobUser dbUser = xxlJobUserDao.loadByUserName(cookieUser.getUsername()); + if (dbUser != null) { + if (cookieUser.getPassword().equals(dbUser.getPassword())) { + return dbUser; + } + } + } + } + return null; + } + + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java new file mode 100644 index 00000000..61da3a27 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/XxlJobService.java @@ -0,0 +1,86 @@ +package com.xxl.job.admin.service; + + +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.core.biz.model.ReturnT; + +import java.util.Date; +import java.util.Map; + +/** + * core job action for xxl-job + * + * @author xuxueli 2016-5-28 15:30:33 + */ +public interface XxlJobService { + + /** + * page list + * + * @param start + * @param length + * @param jobGroup + * @param jobDesc + * @param executorHandler + * @param author + * @return + */ + public Map pageList(int start, int length, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author); + + /** + * add job + * + * @param jobInfo + * @return + */ + public ReturnT add(XxlJobInfo jobInfo); + + /** + * update job + * + * @param jobInfo + * @return + */ + public ReturnT update(XxlJobInfo jobInfo); + + /** + * remove job + * * + * @param id + * @return + */ + public ReturnT remove(int id); + + /** + * start job + * + * @param id + * @return + */ + public ReturnT start(int id); + + /** + * stop job + * + * @param id + * @return + */ + public ReturnT stop(int id); + + /** + * dashboard info + * + * @return + */ + public Map dashboardInfo(); + + /** + * chart info + * + * @param startDate + * @param endDate + * @return + */ + public ReturnT> chartInfo(Date startDate, Date endDate); + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java new file mode 100644 index 00000000..3c01e94d --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/AdminBizImpl.java @@ -0,0 +1,35 @@ +package com.xxl.job.admin.service.impl; + +import com.xxl.job.admin.core.thread.JobCompleteHelper; +import com.xxl.job.admin.core.thread.JobRegistryHelper; +import com.xxl.job.core.biz.AdminBiz; +import com.xxl.job.core.biz.model.HandleCallbackParam; +import com.xxl.job.core.biz.model.RegistryParam; +import com.xxl.job.core.biz.model.ReturnT; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author xuxueli 2017-07-27 21:54:20 + */ +@Service +public class AdminBizImpl implements AdminBiz { + + + @Override + public ReturnT callback(List callbackParamList) { + return JobCompleteHelper.getInstance().callback(callbackParamList); + } + + @Override + public ReturnT registry(RegistryParam registryParam) { + return JobRegistryHelper.getInstance().registry(registryParam); + } + + @Override + public ReturnT registryRemove(RegistryParam registryParam) { + return JobRegistryHelper.getInstance().registryRemove(registryParam); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java new file mode 100644 index 00000000..530ee41c --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/java/com/xxl/job/admin/service/impl/XxlJobServiceImpl.java @@ -0,0 +1,434 @@ +package com.xxl.job.admin.service.impl; + +import com.xxl.job.admin.core.cron.CronExpression; +import com.xxl.job.admin.core.model.XxlJobGroup; +import com.xxl.job.admin.core.model.XxlJobInfo; +import com.xxl.job.admin.core.model.XxlJobLogReport; +import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum; +import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum; +import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum; +import com.xxl.job.admin.core.thread.JobScheduleHelper; +import com.xxl.job.admin.core.util.I18nUtil; +import com.xxl.job.admin.dao.*; +import com.xxl.job.admin.service.XxlJobService; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.enums.ExecutorBlockStrategyEnum; +import com.xxl.job.core.glue.GlueTypeEnum; +import com.xxl.job.core.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.text.MessageFormat; +import java.util.*; + +/** + * core job action for xxl-job + * @author xuxueli 2016-5-28 15:30:33 + */ +@Service +public class XxlJobServiceImpl implements XxlJobService { + private static Logger logger = LoggerFactory.getLogger(XxlJobServiceImpl.class); + + @Resource + private XxlJobGroupDao xxlJobGroupDao; + @Resource + private XxlJobInfoDao xxlJobInfoDao; + @Resource + public XxlJobLogDao xxlJobLogDao; + @Resource + private XxlJobLogGlueDao xxlJobLogGlueDao; + @Resource + private XxlJobLogReportDao xxlJobLogReportDao; + + @Override + public Map pageList(int start, int length, int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) { + + // page list + List list = xxlJobInfoDao.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + int list_count = xxlJobInfoDao.pageListCount(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author); + + // package result + Map maps = new HashMap(); + maps.put("recordsTotal", list_count); // 总记录数 + maps.put("recordsFiltered", list_count); // 过滤后的总记录数 + maps.put("data", list); // 分页列表 + return maps; + } + + @Override + public ReturnT add(XxlJobInfo jobInfo) { + + // valid base + XxlJobGroup group = xxlJobGroupDao.load(jobInfo.getJobGroup()); + if (group == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_choose")+I18nUtil.getString("jobinfo_field_jobgroup")) ); + } + if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); + } + if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); + } + + // valid trigger + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (scheduleTypeEnum == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + if (scheduleTypeEnum == ScheduleTypeEnum.CRON) { + if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) { + return new ReturnT(ReturnT.FAIL_CODE, "Cron"+I18nUtil.getString("system_unvalid")); + } + } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE/* || scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) { + if (jobInfo.getScheduleConf() == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")) ); + } + try { + int fixSecond = Integer.valueOf(jobInfo.getScheduleConf()); + if (fixSecond < 1) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } catch (Exception e) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + // valid job + if (GlueTypeEnum.match(jobInfo.getGlueType()) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_gluetype")+I18nUtil.getString("system_unvalid")) ); + } + if (GlueTypeEnum.BEAN==GlueTypeEnum.match(jobInfo.getGlueType()) && (jobInfo.getExecutorHandler()==null || jobInfo.getExecutorHandler().trim().length()==0) ) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+"JobHandler") ); + } + // 》fix "\r" in shell + if (GlueTypeEnum.GLUE_SHELL==GlueTypeEnum.match(jobInfo.getGlueType()) && jobInfo.getGlueSource()!=null) { + jobInfo.setGlueSource(jobInfo.getGlueSource().replaceAll("\r", "")); + } + + // valid advanced + if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) ); + } + if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) ); + } + if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) ); + } + + // 》ChildJobId valid + if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) { + String[] childJobIds = jobInfo.getChildJobId().split(","); + for (String childJobIdItem: childJobIds) { + if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) { + XxlJobInfo childJobInfo = xxlJobInfoDao.loadById(Integer.parseInt(childJobIdItem)); + if (childJobInfo==null) { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem)); + } + } else { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_unvalid")), childJobIdItem)); + } + } + + // join , avoid "xxx,," + String temp = ""; + for (String item:childJobIds) { + temp += item + ","; + } + temp = temp.substring(0, temp.length()-1); + + jobInfo.setChildJobId(temp); + } + + // add in db + jobInfo.setAddTime(new Date()); + jobInfo.setUpdateTime(new Date()); + jobInfo.setGlueUpdatetime(new Date()); + xxlJobInfoDao.save(jobInfo); + if (jobInfo.getId() < 1) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_add")+I18nUtil.getString("system_fail")) ); + } + + return new ReturnT(String.valueOf(jobInfo.getId())); + } + + private boolean isNumeric(String str){ + try { + int result = Integer.valueOf(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + @Override + public ReturnT update(XxlJobInfo jobInfo) { + + // valid base + if (jobInfo.getJobDesc()==null || jobInfo.getJobDesc().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_jobdesc")) ); + } + if (jobInfo.getAuthor()==null || jobInfo.getAuthor().trim().length()==0) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input")+I18nUtil.getString("jobinfo_field_author")) ); + } + + // valid trigger + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null); + if (scheduleTypeEnum == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + if (scheduleTypeEnum == ScheduleTypeEnum.CRON) { + if (jobInfo.getScheduleConf()==null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) { + return new ReturnT(ReturnT.FAIL_CODE, "Cron"+I18nUtil.getString("system_unvalid") ); + } + } else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE /*|| scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY*/) { + if (jobInfo.getScheduleConf() == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + try { + int fixSecond = Integer.valueOf(jobInfo.getScheduleConf()); + if (fixSecond < 1) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } catch (Exception e) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + // valid advanced + if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorRouteStrategy")+I18nUtil.getString("system_unvalid")) ); + } + if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("misfire_strategy")+I18nUtil.getString("system_unvalid")) ); + } + if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_executorBlockStrategy")+I18nUtil.getString("system_unvalid")) ); + } + + // 》ChildJobId valid + if (jobInfo.getChildJobId()!=null && jobInfo.getChildJobId().trim().length()>0) { + String[] childJobIds = jobInfo.getChildJobId().split(","); + for (String childJobIdItem: childJobIds) { + if (childJobIdItem!=null && childJobIdItem.trim().length()>0 && isNumeric(childJobIdItem)) { + XxlJobInfo childJobInfo = xxlJobInfoDao.loadById(Integer.parseInt(childJobIdItem)); + if (childJobInfo==null) { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_not_found")), childJobIdItem)); + } + } else { + return new ReturnT(ReturnT.FAIL_CODE, + MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId")+"({0})"+I18nUtil.getString("system_unvalid")), childJobIdItem)); + } + } + + // join , avoid "xxx,," + String temp = ""; + for (String item:childJobIds) { + temp += item + ","; + } + temp = temp.substring(0, temp.length()-1); + + jobInfo.setChildJobId(temp); + } + + // group valid + XxlJobGroup jobGroup = xxlJobGroupDao.load(jobInfo.getJobGroup()); + if (jobGroup == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_jobgroup")+I18nUtil.getString("system_unvalid")) ); + } + + // stage job info + XxlJobInfo exists_jobInfo = xxlJobInfoDao.loadById(jobInfo.getId()); + if (exists_jobInfo == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("jobinfo_field_id")+I18nUtil.getString("system_not_found")) ); + } + + // next trigger time (5s后生效,避开预读周期) + long nextTriggerTime = exists_jobInfo.getTriggerNextTime(); + boolean scheduleDataNotChanged = jobInfo.getScheduleType().equals(exists_jobInfo.getScheduleType()) && jobInfo.getScheduleConf().equals(exists_jobInfo.getScheduleConf()); + if (exists_jobInfo.getTriggerStatus() == 1 && !scheduleDataNotChanged) { + try { + Date nextValidTime = JobScheduleHelper.generateNextValidTime(jobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS)); + if (nextValidTime == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + nextTriggerTime = nextValidTime.getTime(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + } + + exists_jobInfo.setJobGroup(jobInfo.getJobGroup()); + exists_jobInfo.setJobDesc(jobInfo.getJobDesc()); + exists_jobInfo.setAuthor(jobInfo.getAuthor()); + exists_jobInfo.setAlarmEmail(jobInfo.getAlarmEmail()); + exists_jobInfo.setScheduleType(jobInfo.getScheduleType()); + exists_jobInfo.setScheduleConf(jobInfo.getScheduleConf()); + exists_jobInfo.setMisfireStrategy(jobInfo.getMisfireStrategy()); + exists_jobInfo.setExecutorRouteStrategy(jobInfo.getExecutorRouteStrategy()); + exists_jobInfo.setExecutorHandler(jobInfo.getExecutorHandler()); + exists_jobInfo.setExecutorParam(jobInfo.getExecutorParam()); + exists_jobInfo.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy()); + exists_jobInfo.setExecutorTimeout(jobInfo.getExecutorTimeout()); + exists_jobInfo.setExecutorFailRetryCount(jobInfo.getExecutorFailRetryCount()); + exists_jobInfo.setChildJobId(jobInfo.getChildJobId()); + exists_jobInfo.setTriggerNextTime(nextTriggerTime); + + exists_jobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(exists_jobInfo); + + + return ReturnT.SUCCESS; + } + + @Override + public ReturnT remove(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + if (xxlJobInfo == null) { + return ReturnT.SUCCESS; + } + + xxlJobInfoDao.delete(id); + xxlJobLogDao.delete(id); + xxlJobLogGlueDao.deleteByJobId(id); + return ReturnT.SUCCESS; + } + + @Override + public ReturnT start(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + + // valid + ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(xxlJobInfo.getScheduleType(), ScheduleTypeEnum.NONE); + if (ScheduleTypeEnum.NONE == scheduleTypeEnum) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type_none_limit_start")) ); + } + + // next trigger time (5s后生效,避开预读周期) + long nextTriggerTime = 0; + try { + Date nextValidTime = JobScheduleHelper.generateNextValidTime(xxlJobInfo, new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS)); + if (nextValidTime == null) { + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + nextTriggerTime = nextValidTime.getTime(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + return new ReturnT(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) ); + } + + xxlJobInfo.setTriggerStatus(1); + xxlJobInfo.setTriggerLastTime(0); + xxlJobInfo.setTriggerNextTime(nextTriggerTime); + + xxlJobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(xxlJobInfo); + return ReturnT.SUCCESS; + } + + @Override + public ReturnT stop(int id) { + XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id); + + xxlJobInfo.setTriggerStatus(0); + xxlJobInfo.setTriggerLastTime(0); + xxlJobInfo.setTriggerNextTime(0); + + xxlJobInfo.setUpdateTime(new Date()); + xxlJobInfoDao.update(xxlJobInfo); + return ReturnT.SUCCESS; + } + + @Override + public Map dashboardInfo() { + + int jobInfoCount = xxlJobInfoDao.findAllCount(); + int jobLogCount = 0; + int jobLogSuccessCount = 0; + XxlJobLogReport xxlJobLogReport = xxlJobLogReportDao.queryLogReportTotal(); + if (xxlJobLogReport != null) { + jobLogCount = xxlJobLogReport.getRunningCount() + xxlJobLogReport.getSucCount() + xxlJobLogReport.getFailCount(); + jobLogSuccessCount = xxlJobLogReport.getSucCount(); + } + + // executor count + Set executorAddressSet = new HashSet(); + List groupList = xxlJobGroupDao.findAll(); + + if (groupList!=null && !groupList.isEmpty()) { + for (XxlJobGroup group: groupList) { + if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) { + executorAddressSet.addAll(group.getRegistryList()); + } + } + } + + int executorCount = executorAddressSet.size(); + + Map dashboardMap = new HashMap(); + dashboardMap.put("jobInfoCount", jobInfoCount); + dashboardMap.put("jobLogCount", jobLogCount); + dashboardMap.put("jobLogSuccessCount", jobLogSuccessCount); + dashboardMap.put("executorCount", executorCount); + return dashboardMap; + } + + @Override + public ReturnT> chartInfo(Date startDate, Date endDate) { + + // process + List triggerDayList = new ArrayList(); + List triggerDayCountRunningList = new ArrayList(); + List triggerDayCountSucList = new ArrayList(); + List triggerDayCountFailList = new ArrayList(); + int triggerCountRunningTotal = 0; + int triggerCountSucTotal = 0; + int triggerCountFailTotal = 0; + + List logReportList = xxlJobLogReportDao.queryLogReport(startDate, endDate); + + if (logReportList!=null && logReportList.size()>0) { + for (XxlJobLogReport item: logReportList) { + String day = DateUtil.formatDate(item.getTriggerDay()); + int triggerDayCountRunning = item.getRunningCount(); + int triggerDayCountSuc = item.getSucCount(); + int triggerDayCountFail = item.getFailCount(); + + triggerDayList.add(day); + triggerDayCountRunningList.add(triggerDayCountRunning); + triggerDayCountSucList.add(triggerDayCountSuc); + triggerDayCountFailList.add(triggerDayCountFail); + + triggerCountRunningTotal += triggerDayCountRunning; + triggerCountSucTotal += triggerDayCountSuc; + triggerCountFailTotal += triggerDayCountFail; + } + } else { + for (int i = -6; i <= 0; i++) { + triggerDayList.add(DateUtil.formatDate(DateUtil.addDays(new Date(), i))); + triggerDayCountRunningList.add(0); + triggerDayCountSucList.add(0); + triggerDayCountFailList.add(0); + } + } + + Map result = new HashMap(); + result.put("triggerDayList", triggerDayList); + result.put("triggerDayCountRunningList", triggerDayCountRunningList); + result.put("triggerDayCountSucList", triggerDayCountSucList); + result.put("triggerDayCountFailList", triggerDayCountFailList); + + result.put("triggerCountRunningTotal", triggerCountRunningTotal); + result.put("triggerCountSucTotal", triggerCountSucTotal); + result.put("triggerCountFailTotal", triggerCountFailTotal); + + return new ReturnT>(result); + } + +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/application.properties b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/application.properties new file mode 100644 index 00000000..1e5249f5 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/application.properties @@ -0,0 +1,66 @@ +### web +server.port=8080 +server.servlet.context-path=/xxl-job-admin + +### actuator +management.server.servlet.context-path=/actuator +management.health.mail.enabled=false + +### resources +spring.mvc.servlet.load-on-startup=0 +spring.mvc.static-path-pattern=/static/** +spring.resources.static-locations=classpath:/static/ + +### freemarker +spring.freemarker.templateLoaderPath=classpath:/templates/ +spring.freemarker.suffix=.ftl +spring.freemarker.charset=UTF-8 +spring.freemarker.request-context-attribute=request +spring.freemarker.settings.number_format=0.########## + +### mybatis +mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml +#mybatis.type-aliases-package=com.xxl.job.admin.core.model + +### xxl-job, datasource +spring.datasource.url=jdbc:mysql://10.0.76.29:13306/cpq01?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai +spring.datasource.username=easygo +spring.datasource.password=VR#test123 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + + +### datasource-pool +spring.datasource.type=com.zaxxer.hikari.HikariDataSource +spring.datasource.hikari.minimum-idle=10 +spring.datasource.hikari.maximum-pool-size=30 +spring.datasource.hikari.auto-commit=true +spring.datasource.hikari.idle-timeout=30000 +spring.datasource.hikari.pool-name=HikariCP +spring.datasource.hikari.max-lifetime=900000 +spring.datasource.hikari.connection-timeout=10000 +spring.datasource.hikari.connection-test-query=SELECT 1 +spring.datasource.hikari.validation-timeout=1000 + +### xxl-job, email +spring.mail.host=smtp.qq.com +spring.mail.port=25 +spring.mail.username=xxx@qq.com +spring.mail.from=xxx@qq.com +spring.mail.password=xxx +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true +spring.mail.properties.mail.smtp.starttls.required=true +spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory + +### xxl-job, access token +xxl.job.accessToken=default_token + +### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en") +xxl.job.i18n=zh_CN + +## xxl-job, triggerpool max size +xxl.job.triggerpool.fast.max=200 +xxl.job.triggerpool.slow.max=100 + +### xxl-job, log retention days +xxl.job.logretentiondays=30 diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/i18n/message_en.properties b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/i18n/message_en.properties new file mode 100644 index 00000000..589d9eb4 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/i18n/message_en.properties @@ -0,0 +1,276 @@ +admin_name=Scheduling Center +admin_name_full=Distributed Task Scheduling Platform XXL-JOB +admin_version=2.4.0 +admin_i18n=en + +## system +system_tips=System message +system_ok=Confirm +system_close=Close +system_save=Save +system_cancel=Cancel +system_search=Search +system_status=Status +system_opt=Operate +system_please_input=please input +system_please_choose=please choose +system_success=success +system_fail=fail +system_add_suc=add success +system_add_fail=add fail +system_update_suc=update success +system_update_fail=update fail +system_all=All +system_api_error=net error +system_show=Show +system_empty=Empty +system_opt_suc=operate success +system_opt_fail=operate fail +system_opt_edit=Edit +system_opt_del=Delete +system_opt_copy=Copy +system_unvalid=illegal +system_not_found=not exist +system_nav=Navigation +system_digits=digits +system_lengh_limit=Length limit +system_permission_limit=Permission limit +system_welcome=Welcome + +## daterangepicker +daterangepicker_ranges_recent_hour=recent one hour +daterangepicker_ranges_today=today +daterangepicker_ranges_yesterday=yesterday +daterangepicker_ranges_this_month=this month +daterangepicker_ranges_last_month=last month +daterangepicker_ranges_recent_week=recent one week +daterangepicker_ranges_recent_month=recent one month +daterangepicker_custom_name=custom +daterangepicker_custom_starttime=start time +daterangepicker_custom_endtime=end time +daterangepicker_custom_daysofweek=Sun,Mon,Tue,Wed,Thu,Fri,Sat +daterangepicker_custom_monthnames=Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec + +## dataTable +dataTable_sProcessing=processing... +dataTable_sLengthMenu= _MENU_ records per page +dataTable_sZeroRecords=No matching results +dataTable_sInfo=page _PAGE_ ( Total _PAGES_ pages,_TOTAL_ records ) +dataTable_sInfoEmpty=No Record +dataTable_sInfoFiltered=(Filtered by _MAX_ results) +dataTable_sSearch=Search +dataTable_sEmptyTable=Table data is empty +dataTable_sLoadingRecords=Loading... +dataTable_sFirst=FIRST PAGE +dataTable_sPrevious=Previous Page +dataTable_sNext=Next Page +dataTable_sLast=LAST PAGE +dataTable_sSortAscending=: Rank this column in ascending order +dataTable_sSortDescending=: Rank this column in descending order + +## login +login_btn=Login +login_remember_me=Remember Me +login_username_placeholder=Please enter username +login_password_placeholder=Please enter password +login_username_empty=Please enter username +login_username_lt_4=Username length should not be less than 4 +login_password_empty=Please enter password +login_password_lt_4=Password length should not be less than 4 +login_success=Login success +login_fail=Login fail +login_param_empty=Username or password is empty +login_param_unvalid=Username or password error + +## logout +logout_btn=Logout +logout_confirm=Confirm logout? +logout_success=Logout success +logout_fail=Logout fail + +## change pwd +change_pwd=Change password +change_pwd_suc_to_logout=Change password successful, about to log out login +change_pwd_field_newpwd=new password + +## dashboard +job_dashboard_name=Run report +job_dashboard_job_num=Job number +job_dashboard_job_num_tip=The number of tasks running in the scheduling center +job_dashboard_trigger_num=trigger number +job_dashboard_trigger_num_tip=The number of trigger record scheduled by the scheduling center +job_dashboard_jobgroup_num=Executor number +job_dashboard_jobgroup_num_tip=The number of online executor machines perceived by the scheduling center +job_dashboard_report=Scheduling report +job_dashboard_report_loaddata_fail=Scheduling report load data error +job_dashboard_date_report=Date distribution +job_dashboard_rate_report=Percentage distribution + +## job info +jobinfo_name=Job Manage +jobinfo_job=Job +jobinfo_field_add=Add Job +jobinfo_field_update=Edit Job +jobinfo_field_id=Job ID +jobinfo_field_jobgroup=Executor +jobinfo_field_jobdesc=Job description +jobinfo_field_timeout=Job timeout period +jobinfo_field_gluetype=GLUE Type +jobinfo_field_executorparam=Param +jobinfo_field_author=Author +jobinfo_field_alarmemail=Alarm email +jobinfo_field_alarmemail_placeholder=Please enter alarm mail, if there are more than one comma separated +jobinfo_field_executorRouteStrategy=Route Strategy +jobinfo_field_childJobId=Child Job ID +jobinfo_field_childJobId_placeholder=Please enter the Child job ID, if there are more than one comma separated +jobinfo_field_executorBlockStrategy=Block Strategy +jobinfo_field_executorFailRetryCount=Fail Retry Count +jobinfo_field_executorFailRetryCount_placeholder=Fail Retry Count. effect if greater than zero +jobinfo_script_location=Script location +jobinfo_shard_index=Shard index +jobinfo_shard_total=Shard total +jobinfo_opt_stop=Stop +jobinfo_opt_start=Start +jobinfo_opt_log=Query Log +jobinfo_opt_run=Run Once +jobinfo_opt_run_tips=Please input the address for this trigger. Null will be obtained from the executor +jobinfo_opt_registryinfo=Registry Info +jobinfo_opt_next_time=Next trigger time +jobinfo_glue_remark=Resource Remark +jobinfo_glue_remark_limit=Resource Remark length is limited to 4~100 +jobinfo_glue_rollback=Version Backtrack +jobinfo_glue_jobid_unvalid=Job ID is illegal +jobinfo_glue_gluetype_unvalid=The job is not GLUE Type +jobinfo_field_executorTimeout_placeholder=Job Timeout period,in seconds. effect if greater than zero +schedule_type=Schedule Type +schedule_type_none=None +schedule_type_cron=Cron +schedule_type_fix_rate=Fix rate +schedule_type_fix_delay=Fix delay +schedule_type_none_limit_start=The current schedule type disables startup +misfire_strategy=Misfire strategy +misfire_strategy_do_nothing=Do nothing +misfire_strategy_fire_once_now=Fire once now +jobinfo_conf_base=Base configuration +jobinfo_conf_schedule=Schedule configuration +jobinfo_conf_job=Job configuration +jobinfo_conf_advanced=Advanced configuration + +## job log +joblog_name=Trigger Log +joblog_status=Status +joblog_status_all=All +joblog_status_suc=Success +joblog_status_fail=Fail +joblog_status_running=Running +joblog_field_triggerTime=Trigger Time +joblog_field_triggerCode=Trigger Result +joblog_field_triggerMsg=Trigger Msg +joblog_field_handleTime=Handle Time +joblog_field_handleCode=Handle Result +joblog_field_handleMsg=Trigger Msg +joblog_field_executorAddress=Executor Address +joblog_clean=Clean +joblog_clean_log=Clean Log +joblog_clean_type=Clean Type +joblog_clean_type_1=Clean up log data a month ago +joblog_clean_type_2=Clean up log data three month ago +joblog_clean_type_3=Clean up log data six month ago +joblog_clean_type_4=Clean up log data a year ago +joblog_clean_type_5=Clean up log data a thousand record ago +joblog_clean_type_6=Clean up log data ten thousand record ago +joblog_clean_type_7=Clean up log data thirty thousand record ago +joblog_clean_type_8=Clean up log data hundred thousand record ago +joblog_clean_type_9=Clean up all log data +joblog_clean_type_unvalid=Clean type is illegal +joblog_handleCode_200=Success +joblog_handleCode_500=Fail +joblog_handleCode_502=Timeout +joblog_kill_log=Kill Job +joblog_kill_log_limit=Trigger Fail, can not kill job +joblog_kill_log_byman=Manual operation, kill job +joblog_lost_fail=Job result lost, marked as failure +joblog_rolling_log=Rolling log +joblog_rolling_log_refresh=Refresh +joblog_rolling_log_triggerfail=The job trigger fail, can not view the rolling log +joblog_rolling_log_failoften=The request for the Rolling log is terminated, the number of failed requests exceeds the limit, Reload the log on the refresh page +joblog_logid_unvalid=Log ID is illegal + +## job group +jobgroup_name=Executor Manage +jobgroup_list=Executor List +jobgroup_add=Add Executor +jobgroup_edit=Edit Executor +jobgroup_del=Delete Executor +jobgroup_field_title=Title +jobgroup_field_addressType=Registry Type +jobgroup_field_addressType_0=Automatic registration +jobgroup_field_addressType_1=Manual registration +jobgroup_field_addressType_limit=Manually registration type, the machine address must not be empty +jobgroup_field_registryList=machine address +jobgroup_field_registryList_unvalid=registry machine address is illegal +jobgroup_field_registryList_placeholder=Please enter the machine address, if there are more than one comma separated +jobgroup_field_appname_limit=Limit the beginning of a lowercase letter, consists of lowercase letters、number and hyphen. +jobgroup_field_appname_length=AppName length is limited to 4~64 +jobgroup_field_title_length=Title length is limited to 4~12 +jobgroup_field_order_digits=Please enter a positive integer +jobgroup_field_orderrange=Order is limited to 1~1000 +jobgroup_del_limit_0=Refuse to delete, the executor is being used +jobgroup_del_limit_1=Refuses to delete, the system retains at least one executor +jobgroup_empty=There is no valid executor. Please contact the administrator + +## job conf +jobconf_block_SERIAL_EXECUTION=Serial execution +jobconf_block_DISCARD_LATER=Discard Later +jobconf_block_COVER_EARLY=Cover Early +jobconf_route_first=First +jobconf_route_last=Last +jobconf_route_round=Round +jobconf_route_random=Random +jobconf_route_consistenthash=Consistent Hash +jobconf_route_lfu=Least Frequently Used +jobconf_route_lru=Least Recently Used +jobconf_route_failover=Failover +jobconf_route_busyover=Busyover +jobconf_route_shard=Sharding Broadcast +jobconf_idleBeat=Idle check +jobconf_beat=Heartbeats +jobconf_monitor=Task Scheduling Center monitor alarm +jobconf_monitor_detail=monitor alarm details +jobconf_monitor_alarm_title=Alarm Type +jobconf_monitor_alarm_type=Trigger Fail +jobconf_monitor_alarm_content=Alarm Content +jobconf_trigger_admin_adress=Trigger machine address +jobconf_trigger_exe_regtype=Execotor-Registry Type +jobconf_trigger_exe_regaddress=Execotor-Registry Address +jobconf_trigger_address_empty=Trigger Fail:registry address is empty +jobconf_trigger_run=Trigger Job +jobconf_trigger_child_run=Trigger child job +jobconf_callback_child_msg1={0}/{1} [Job ID={2}], Trigger {3}, Trigger msg: {4}
+jobconf_callback_child_msg2={0}/{1} [Job ID={2}], Trigger Fail, Trigger msg: Job ID is illegal
+jobconf_trigger_type=Job trigger type +jobconf_trigger_type_cron=Cron trigger +jobconf_trigger_type_manual=Manual trigger +jobconf_trigger_type_parent=Parent job trigger +jobconf_trigger_type_api=Api trigger +jobconf_trigger_type_retry=Fail retry trigger +jobconf_trigger_type_misfire=Misfire compensation trigger + +## user +user_manage=User Manage +user_username=Username +user_password=Password +user_role=Role +user_role_admin=Admin User +user_role_normal=Normal User +user_permission=Permission +user_add=Add User +user_update=Edit User +user_username_repeat=Username Repeat +user_username_valid=Restrictions start with a lowercase letter and consist of lowercase letters and Numbers +user_password_update_placeholder=Please input password, empty means not update +user_update_loginuser_limit=Operation of current login account is not allowed + +## help +job_help=Tutorial +job_help_document=Official Document diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties new file mode 100644 index 00000000..fc1d2764 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/i18n/message_zh_CN.properties @@ -0,0 +1,276 @@ +admin_name=任务调度中心 +admin_name_full=分布式任务调度平台XXL-JOB +admin_version=2.4.0 +admin_i18n= + +## system +system_tips=系统提示 +system_ok=确定 +system_close=关闭 +system_save=保存 +system_cancel=取消 +system_search=搜索 +system_status=状态 +system_opt=操作 +system_please_input=请输入 +system_please_choose=请选择 +system_success=成功 +system_fail=失败 +system_add_suc=新增成功 +system_add_fail=新增失败 +system_update_suc=更新成功 +system_update_fail=更新失败 +system_all=全部 +system_api_error=接口异常 +system_show=查看 +system_empty=无 +system_opt_suc=操作成功 +system_opt_fail=操作失败 +system_opt_edit=编辑 +system_opt_del=删除 +system_opt_copy=复制 +system_unvalid=非法 +system_not_found=不存在 +system_nav=导航 +system_digits=整数 +system_lengh_limit=长度限制 +system_permission_limit=权限拦截 +system_welcome=欢迎 + +## daterangepicker +daterangepicker_ranges_recent_hour=最近一小时 +daterangepicker_ranges_today=今日 +daterangepicker_ranges_yesterday=昨日 +daterangepicker_ranges_this_month=本月 +daterangepicker_ranges_last_month=上个月 +daterangepicker_ranges_recent_week=最近一周 +daterangepicker_ranges_recent_month=最近一月 +daterangepicker_custom_name=自定义 +daterangepicker_custom_starttime=起始时间 +daterangepicker_custom_endtime=结束时间 +daterangepicker_custom_daysofweek=日,一,二,三,四,五,六 +daterangepicker_custom_monthnames=一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 + +## dataTable +dataTable_sProcessing=处理中... +dataTable_sLengthMenu=每页 _MENU_ 条记录 +dataTable_sZeroRecords=没有匹配结果 +dataTable_sInfo=第 _PAGE_ 页 ( 总共 _PAGES_ 页,_TOTAL_ 条记录 ) +dataTable_sInfoEmpty=无记录 +dataTable_sInfoFiltered=(由 _MAX_ 项结果过滤) +dataTable_sSearch=搜索 +dataTable_sEmptyTable=表中数据为空 +dataTable_sLoadingRecords=载入中... +dataTable_sFirst=首页 +dataTable_sPrevious=上页 +dataTable_sNext=下页 +dataTable_sLast=末页 +dataTable_sSortAscending=: 以升序排列此列 +dataTable_sSortDescending=: 以降序排列此列 + +## login +login_btn=登录 +login_remember_me=记住密码 +login_username_placeholder=请输入登录账号 +login_password_placeholder=请输入登录密码 +login_username_empty=请输入登录账号 +login_username_lt_4=登录账号不应低于4位 +login_password_empty=请输入登录密码 +login_password_lt_4=登录密码不应低于4位 +login_success=登录成功 +login_fail=登录失败 +login_param_empty=账号或密码为空 +login_param_unvalid=账号或密码错误 + +## logout +logout_btn=注销 +logout_confirm=确认注销登录? +logout_success=注销成功 +logout_fail=注销失败 + +## change pwd +change_pwd=修改密码 +change_pwd_suc_to_logout=修改密码成功,即将注销登陆 +change_pwd_field_newpwd=新密码 + +## dashboard +job_dashboard_name=运行报表 +job_dashboard_job_num=任务数量 +job_dashboard_job_num_tip=调度中心运行的任务数量 +job_dashboard_trigger_num=调度次数 +job_dashboard_trigger_num_tip=调度中心触发的调度次数 +job_dashboard_jobgroup_num=执行器数量 +job_dashboard_jobgroup_num_tip=调度中心在线的执行器机器数量 +job_dashboard_report=调度报表 +job_dashboard_report_loaddata_fail=调度报表数据加载异常 +job_dashboard_date_report=日期分布图 +job_dashboard_rate_report=成功比例图 + +## job info +jobinfo_name=任务管理 +jobinfo_job=任务 +jobinfo_field_add=新增 +jobinfo_field_update=更新任务 +jobinfo_field_id=任务ID +jobinfo_field_jobgroup=执行器 +jobinfo_field_jobdesc=任务描述 +jobinfo_field_gluetype=运行模式 +jobinfo_field_executorparam=任务参数 +jobinfo_field_author=负责人 +jobinfo_field_timeout=任务超时时间 +jobinfo_field_alarmemail=报警邮件 +jobinfo_field_alarmemail_placeholder=请输入报警邮件,多个邮件地址则逗号分隔 +jobinfo_field_executorRouteStrategy=路由策略 +jobinfo_field_childJobId=子任务ID +jobinfo_field_childJobId_placeholder=请输入子任务的任务ID,如存在多个则逗号分隔 +jobinfo_field_executorBlockStrategy=阻塞处理策略 +jobinfo_field_executorFailRetryCount=失败重试次数 +jobinfo_field_executorFailRetryCount_placeholder=失败重试次数,大于零时生效 +jobinfo_script_location=脚本位置 +jobinfo_shard_index=分片序号 +jobinfo_shard_total=分片总数 +jobinfo_opt_stop=停止 +jobinfo_opt_start=启动 +jobinfo_opt_log=查询日志 +jobinfo_opt_run=执行一次 +jobinfo_opt_run_tips=请输入本次执行的机器地址,为空则从执行器获取 +jobinfo_opt_registryinfo=注册节点 +jobinfo_opt_next_time=下次执行时间 +jobinfo_glue_remark=源码备注 +jobinfo_glue_remark_limit=源码备注长度限制为4~100 +jobinfo_glue_rollback=版本回溯 +jobinfo_glue_jobid_unvalid=任务ID非法 +jobinfo_glue_gluetype_unvalid=该任务非GLUE模式 +jobinfo_field_executorTimeout_placeholder=任务超时时间,单位秒,大于零时生效 +schedule_type=调度类型 +schedule_type_none=无 +schedule_type_cron=CRON +schedule_type_fix_rate=固定速度 +schedule_type_fix_delay=固定延迟 +schedule_type_none_limit_start=当前调度类型禁止启动 +misfire_strategy=调度过期策略 +misfire_strategy_do_nothing=忽略 +misfire_strategy_fire_once_now=立即执行一次 +jobinfo_conf_base=基础配置 +jobinfo_conf_schedule=调度配置 +jobinfo_conf_job=任务配置 +jobinfo_conf_advanced=高级配置 + +## job log +joblog_name=调度日志 +joblog_status=状态 +joblog_status_all=全部 +joblog_status_suc=成功 +joblog_status_fail=失败 +joblog_status_running=进行中 +joblog_field_triggerTime=调度时间 +joblog_field_triggerCode=调度结果 +joblog_field_triggerMsg=调度备注 +joblog_field_handleTime=执行时间 +joblog_field_handleCode=执行结果 +joblog_field_handleMsg=执行备注 +joblog_field_executorAddress=执行器地址 +joblog_clean=清理 +joblog_clean_log=日志清理 +joblog_clean_type=清理方式 +joblog_clean_type_1=清理一个月之前日志数据 +joblog_clean_type_2=清理三个月之前日志数据 +joblog_clean_type_3=清理六个月之前日志数据 +joblog_clean_type_4=清理一年之前日志数据 +joblog_clean_type_5=清理一千条以前日志数据 +joblog_clean_type_6=清理一万条以前日志数据 +joblog_clean_type_7=清理三万条以前日志数据 +joblog_clean_type_8=清理十万条以前日志数据 +joblog_clean_type_9=清理所有日志数据 +joblog_clean_type_unvalid=清理类型参数异常 +joblog_handleCode_200=成功 +joblog_handleCode_500=失败 +joblog_handleCode_502=失败(超时) +joblog_kill_log=终止任务 +joblog_kill_log_limit=调度失败,无法终止日志 +joblog_kill_log_byman=人为操作,主动终止 +joblog_lost_fail=任务结果丢失,标记失败 +joblog_rolling_log=执行日志 +joblog_rolling_log_refresh=刷新 +joblog_rolling_log_triggerfail=任务发起调度失败,无法查看执行日志 +joblog_rolling_log_failoften=终止请求Rolling日志,请求失败次数超上限,可刷新页面重新加载日志 +joblog_logid_unvalid=日志ID非法 + +## job group +jobgroup_name=执行器管理 +jobgroup_list=执行器列表 +jobgroup_add=新增执行器 +jobgroup_edit=编辑执行器 +jobgroup_del=删除执行器 +jobgroup_field_title=名称 +jobgroup_field_addressType=注册方式 +jobgroup_field_addressType_0=自动注册 +jobgroup_field_addressType_1=手动录入 +jobgroup_field_addressType_limit=手动录入注册方式,机器地址不可为空 +jobgroup_field_registryList=机器地址 +jobgroup_field_registryList_unvalid=机器地址格式非法 +jobgroup_field_registryList_placeholder=请输入执行器地址列表,多地址逗号分隔 +jobgroup_field_appname_limit=限制以小写字母开头,由小写字母、数字和中划线组成 +jobgroup_field_appname_length=AppName长度限制为4~64 +jobgroup_field_title_length=名称长度限制为4~12 +jobgroup_field_order_digits=请输入整数 +jobgroup_field_orderrange=取值范围为1~1000 +jobgroup_del_limit_0=拒绝删除,该执行器使用中 +jobgroup_del_limit_1=拒绝删除, 系统至少保留一个执行器 +jobgroup_empty=不存在有效执行器,请联系管理员 + +## job conf +jobconf_block_SERIAL_EXECUTION=单机串行 +jobconf_block_DISCARD_LATER=丢弃后续调度 +jobconf_block_COVER_EARLY=覆盖之前调度 +jobconf_route_first=第一个 +jobconf_route_last=最后一个 +jobconf_route_round=轮询 +jobconf_route_random=随机 +jobconf_route_consistenthash=一致性HASH +jobconf_route_lfu=最不经常使用 +jobconf_route_lru=最近最久未使用 +jobconf_route_failover=故障转移 +jobconf_route_busyover=忙碌转移 +jobconf_route_shard=分片广播 +jobconf_idleBeat=空闲检测 +jobconf_beat=心跳检测 +jobconf_monitor=任务调度中心监控报警 +jobconf_monitor_detail=监控告警明细 +jobconf_monitor_alarm_title=告警类型 +jobconf_monitor_alarm_type=调度失败 +jobconf_monitor_alarm_content=告警内容 +jobconf_trigger_admin_adress=调度机器 +jobconf_trigger_exe_regtype=执行器-注册方式 +jobconf_trigger_exe_regaddress=执行器-地址列表 +jobconf_trigger_address_empty=调度失败:执行器地址为空 +jobconf_trigger_run=触发调度 +jobconf_trigger_child_run=触发子任务 +jobconf_callback_child_msg1={0}/{1} [任务ID={2}], 触发{3}, 触发备注: {4}
+jobconf_callback_child_msg2={0}/{1} [任务ID={2}], 触发失败, 触发备注: 任务ID格式错误
+jobconf_trigger_type=任务触发类型 +jobconf_trigger_type_cron=Cron触发 +jobconf_trigger_type_manual=手动触发 +jobconf_trigger_type_parent=父任务触发 +jobconf_trigger_type_api=API触发 +jobconf_trigger_type_retry=失败重试触发 +jobconf_trigger_type_misfire=调度过期补偿 + +## user +user_manage=用户管理 +user_username=账号 +user_password=密码 +user_role=角色 +user_role_admin=管理员 +user_role_normal=普通用户 +user_permission=权限 +user_add=新增用户 +user_update=更新用户 +user_username_repeat=账号重复 +user_username_valid=限制以小写字母开头,由小写字母、数字组成 +user_password_update_placeholder=请输入新密码,为空则不更新密码 +user_update_loginuser_limit=禁止操作当前登录账号 + +## help +job_help=使用教程 +job_help_document=官方文档 \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties new file mode 100644 index 00000000..49d39fd2 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/i18n/message_zh_TC.properties @@ -0,0 +1,276 @@ +admin_name=任務調度中心 +admin_name_full=分布式任務調度平臺XXL-JOB +admin_version=2.4.0 +admin_i18n= + +## system +system_tips=系統提示 +system_ok=確定 +system_close=關閉 +system_save=儲存 +system_cancel=取消 +system_search=搜尋 +system_status=狀態 +system_opt=操作 +system_please_input=請輸入 +system_please_choose=请選擇 +system_success=成功 +system_fail=失敗 +system_add_suc=新增成功 +system_add_fail=新增失敗 +system_update_suc=更新成功 +system_update_fail=更新失敗 +system_all=全部 +system_api_error=API錯誤 +system_show=查看 +system_empty=無 +system_opt_suc=操作成功 +system_opt_fail=操作失敗 +system_opt_edit=編輯 +system_opt_del=刪除 +system_opt_copy=復制 +system_unvalid=非法 +system_not_found=不存在 +system_nav=導航 +system_digits=整數 +system_lengh_limit=長度限制 +system_permission_limit=權限控管 +system_welcome=歡迎 + +## daterangepicker +daterangepicker_ranges_recent_hour=最近一小時 +daterangepicker_ranges_today=今日 +daterangepicker_ranges_yesterday=昨日 +daterangepicker_ranges_this_month=本月 +daterangepicker_ranges_last_month=上個月 +daterangepicker_ranges_recent_week=最近一周 +daterangepicker_ranges_recent_month=最近一月 +daterangepicker_custom_name=自定義 +daterangepicker_custom_starttime=起始時間 +daterangepicker_custom_endtime=結束時間 +daterangepicker_custom_daysofweek=日,一,二,三,四,五,六 +daterangepicker_custom_monthnames=一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 + +## dataTable +dataTable_sProcessing=處理中... +dataTable_sLengthMenu=每頁 _MENU_ 條記錄 +dataTable_sZeroRecords=沒有相符合記錄 +dataTable_sInfo=第 _PAGE_ 頁 ( 總共 _PAGES_ 頁,_TOTAL_ 條記錄 ) +dataTable_sInfoEmpty=無記錄 +dataTable_sInfoFiltered=(由 _MAX_ 項結果過濾) +dataTable_sSearch=搜尋 +dataTable_sEmptyTable=表中資料為空 +dataTable_sLoadingRecords=載入中... +dataTable_sFirst=首頁 +dataTable_sPrevious=上頁 +dataTable_sNext=下頁 +dataTable_sLast=末頁 +dataTable_sSortAscending=: 以升幂排序此列 +dataTable_sSortDescending=: 以降幂排序此列 + +## login +login_btn=登入 +login_remember_me=記住密碼 +login_username_placeholder=請輸入登入帳號 +login_password_placeholder=請輸入登入密碼 +login_username_empty=請輸入登入帳號 +login_username_lt_4=登入帳號不應低於4位數 +login_password_empty=請輸入登入密碼 +login_password_lt_4=登入密碼不應低於4位數 +login_success=登入成功 +login_fail=登入失敗 +login_param_empty=帳號或密碼為空值 +login_param_unvalid=帳號或密碼錯誤 + +## logout +logout_btn=登出 +logout_confirm=確認登出? +logout_success=登出成功 +logout_fail=登出失敗 + +## change pwd +change_pwd=修改密碼 +change_pwd_suc_to_logout=修改密碼成功,即將登出 +change_pwd_field_newpwd=新密碼 + +## dashboard +job_dashboard_name=運行報表 +job_dashboard_job_num=任務數量 +job_dashboard_job_num_tip=調度中心運行的任務數量 +job_dashboard_trigger_num=調度次數 +job_dashboard_trigger_num_tip=調度中心觸發的調度次數 +job_dashboard_jobgroup_num=執行器數量 +job_dashboard_jobgroup_num_tip=調度中心在線的執行器機器數量 +job_dashboard_report=調度報表 +job_dashboard_report_loaddata_fail=調度報表資料加載異常 +job_dashboard_date_report=日期分布圖 +job_dashboard_rate_report=成功比例圖 + +## job info +jobinfo_name=任務管理 +jobinfo_job=任務 +jobinfo_field_add=新增 +jobinfo_field_update=更新任務 +jobinfo_field_id=任務ID +jobinfo_field_jobgroup=執行器 +jobinfo_field_jobdesc=任務描述 +jobinfo_field_gluetype=運行模式 +jobinfo_field_executorparam=任務參數 +jobinfo_field_author=負責人 +jobinfo_field_timeout=任務超時秒數 +jobinfo_field_alarmemail=告警郵件 +jobinfo_field_alarmemail_placeholder=輸入多個告警郵件地址,請以逗號分隔 +jobinfo_field_executorRouteStrategy=路由策略 +jobinfo_field_childJobId=子任務ID +jobinfo_field_childJobId_placeholder=輸入子任務ID,如有多個請以逗號分隔 +jobinfo_field_executorBlockStrategy=阻塞處理策略 +jobinfo_field_executorFailRetryCount=失敗重試次數 +jobinfo_field_executorFailRetryCount_placeholder=失敗重試次數,大於零時生效 +jobinfo_script_location=腳本位置 +jobinfo_shard_index=分片序號 +jobinfo_shard_total=分片總數 +jobinfo_opt_stop=停止 +jobinfo_opt_start=啟動 +jobinfo_opt_log=查詢日誌 +jobinfo_opt_run=執行一次 +jobinfo_opt_run_tips=請輸入本次執行的機器地址,為空則從執行器獲取 +jobinfo_opt_registryinfo=注冊節點 +jobinfo_opt_next_time=下次執行時間 +jobinfo_glue_remark=源碼備註 +jobinfo_glue_remark_limit=源碼備註長度限制為4~100 +jobinfo_glue_rollback=版本回復 +jobinfo_glue_jobid_unvalid=任務ID非法 +jobinfo_glue_gluetype_unvalid=該任務非GLUE模式 +jobinfo_field_executorTimeout_placeholder=任務超時時間,單位秒,大於零時生效 +schedule_type=調度類型 +schedule_type_none=無 +schedule_type_cron=CRON +schedule_type_fix_rate=固定速度 +schedule_type_fix_delay=固定延遲 +schedule_type_none_limit_start=當前調度類型禁止啟動 +misfire_strategy=調度過期策略 +misfire_strategy_do_nothing=忽略 +misfire_strategy_fire_once_now=立即執行壹次 +jobinfo_conf_base=基礎配置 +jobinfo_conf_schedule=調度配置 +jobinfo_conf_job=任務配置 +jobinfo_conf_advanced=高級配置 + +## job log +joblog_name=調度日誌 +joblog_status=狀態 +joblog_status_all=全部 +joblog_status_suc=成功 +joblog_status_fail=失敗 +joblog_status_running=進行中 +joblog_field_triggerTime=調度時間 +joblog_field_triggerCode=調度結果 +joblog_field_triggerMsg=調度備註 +joblog_field_handleTime=執行時間 +joblog_field_handleCode=執行结果 +joblog_field_handleMsg=執行備註 +joblog_field_executorAddress=執行器地址 +joblog_clean=清理 +joblog_clean_log=日誌清理 +joblog_clean_type=清理方式 +joblog_clean_type_1=清理一個月之前日誌資料 +joblog_clean_type_2=清理三個月之前日誌資料 +joblog_clean_type_3=清理六個月之前日誌資料 +joblog_clean_type_4=清理一年之前日誌資料 +joblog_clean_type_5=清理一千條以前日誌資料 +joblog_clean_type_6=清理一萬條以前日誌資料 +joblog_clean_type_7=清理三萬條以前日誌資料 +joblog_clean_type_8=清理十萬條以前日誌資料 +joblog_clean_type_9=清理所有日誌資料 +joblog_clean_type_unvalid=清理類型參数異常 +joblog_handleCode_200=成功 +joblog_handleCode_500=失敗 +joblog_handleCode_502=失敗(超時) +joblog_kill_log=终止任務 +joblog_kill_log_limit=調度失敗,無法终止日誌 +joblog_kill_log_byman=人為操作,主動終止 +joblog_lost_fail=任務結果丟失,標記失敗 +joblog_rolling_log=執行日誌 +joblog_rolling_log_refresh=更新 +joblog_rolling_log_triggerfail=任務發起調度失敗,無法查看執行日誌 +joblog_rolling_log_failoften=終止請求Rolling日誌,請求失敗次數超上限,可刷新頁面重新加載日誌 +joblog_logid_unvalid=日誌ID非法 + +## job group +jobgroup_name=執行器管理 +jobgroup_list=執行器列表 +jobgroup_add=新增執行器 +jobgroup_edit=編輯執行器 +jobgroup_del=刪除執行器 +jobgroup_field_title=名稱 +jobgroup_field_addressType=注冊方式 +jobgroup_field_addressType_0=自動注冊 +jobgroup_field_addressType_1=手動登錄 +jobgroup_field_addressType_limit=手動登錄注冊方式,機器地址不可為空 +jobgroup_field_registryList=機器地址 +jobgroup_field_registryList_unvalid=機器地址格式非法 +jobgroup_field_registryList_placeholder=請輸入執行器地址列表,多個地址請以逗號分隔 +jobgroup_field_appname_limit=限制以小寫字母開頭,由小寫字母、數字和中划線組成 +jobgroup_field_appname_length=AppName長度限制為4~64 +jobgroup_field_title_length=名稱長度限制為4~12 +jobgroup_field_order_digits=請輸入整數 +jobgroup_field_orderrange=取值範圍為1~1000 +jobgroup_del_limit_0=拒絕刪除,該執行器使用中 +jobgroup_del_limit_1=拒絕删除,系统至少保留一個執行器 +jobgroup_empty=不存在有效執行器,請聯絡系統管理員 + +## job conf +jobconf_block_SERIAL_EXECUTION=單機串行 +jobconf_block_DISCARD_LATER=丢棄后續調度 +jobconf_block_COVER_EARLY=覆蓋之前調度 +jobconf_route_first=第一個 +jobconf_route_last=最後一個 +jobconf_route_round=輪詢 +jobconf_route_random=隨機 +jobconf_route_consistenthash=一致性HASH +jobconf_route_lfu=最不經常使用 +jobconf_route_lru=最近最久未使用 +jobconf_route_failover=故障轉移 +jobconf_route_busyover=忙碌轉移 +jobconf_route_shard=分片廣播 +jobconf_idleBeat=空閒檢測 +jobconf_beat=心跳檢測 +jobconf_monitor=任務調度中心監控告警 +jobconf_monitor_detail=監控告警明细 +jobconf_monitor_alarm_title=告警類型 +jobconf_monitor_alarm_type=調度失敗 +jobconf_monitor_alarm_content=告警内容 +jobconf_trigger_admin_adress=調度機器 +jobconf_trigger_exe_regtype=執行器-注冊方式 +jobconf_trigger_exe_regaddress=執行器-地址列表 +jobconf_trigger_address_empty=調度失敗:執行器地址為空 +jobconf_trigger_run=觸發調度 +jobconf_trigger_child_run=觸發子任務 +jobconf_callback_child_msg1={0}/{1} [任務ID={2}], 觸發{3}, 觸發備註: {4}
+jobconf_callback_child_msg2={0}/{1} [任務ID={2}], 觸發失败, 觸發備註: 任務ID格式錯誤
+jobconf_trigger_type=任務觸發類型 +jobconf_trigger_type_cron=Cron觸發 +jobconf_trigger_type_manual=手動觸發 +jobconf_trigger_type_parent=父任務觸發 +jobconf_trigger_type_api=API觸發 +jobconf_trigger_type_retry=失敗重試觸發 +jobconf_trigger_type_misfire=調度過期補償 + +## user +user_manage=用户管理 +user_username=帳號 +user_password=密碼 +user_role=角色 +user_role_admin=管理員 +user_role_normal=普通用戶 +user_permission=權限 +user_add=新增用戶 +user_update=更新用戶 +user_username_repeat=帳號重複 +user_username_valid=限制以小寫字母開頭,由小寫字母、數字組成 +user_password_update_placeholder=請輸入新密碼,為空則不更新密碼 +user_update_loginuser_limit=禁止操作當前登入帳號 + +## help +job_help=使用教程 +job_help_document=官方文件 \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/logback.xml b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/logback.xml new file mode 100644 index 00000000..d4b08c24 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/logback.xml @@ -0,0 +1,29 @@ + + + + logback + + + + + %d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n + + + + + ${log.path} + + ${log.path}.%d{yyyy-MM-dd}.zip + + + %date %level [%thread] %logger{36} [%file : %line] %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml new file mode 100644 index 00000000..87299f88 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobGroupMapper.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + t.id, + t.app_name, + t.title, + t.address_type, + t.address_list, + t.update_time + + + + + + + + INSERT INTO xxl_job_group ( `app_name`, `title`, `address_type`, `address_list`, `update_time`) + values ( #{appname}, #{title}, #{addressType}, #{addressList}, #{updateTime} ); + + + + UPDATE xxl_job_group + SET `app_name` = #{appname}, + `title` = #{title}, + `address_type` = #{addressType}, + `address_list` = #{addressList}, + `update_time` = #{updateTime} + WHERE id = #{id} + + + + DELETE FROM xxl_job_group + WHERE id = #{id} + + + + + + + + + \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml new file mode 100644 index 00000000..7b3c3a3e --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobInfoMapper.xml @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + t.id, + t.job_group, + t.job_desc, + t.add_time, + t.update_time, + t.author, + t.alarm_email, + t.schedule_type, + t.schedule_conf, + t.misfire_strategy, + t.executor_route_strategy, + t.executor_handler, + t.executor_param, + t.executor_block_strategy, + t.executor_timeout, + t.executor_fail_retry_count, + t.glue_type, + t.glue_source, + t.glue_remark, + t.glue_updatetime, + t.child_jobid, + t.trigger_status, + t.trigger_last_time, + t.trigger_next_time + + + + + + + + INSERT INTO xxl_job_info ( + job_group, + job_desc, + add_time, + update_time, + author, + alarm_email, + schedule_type, + schedule_conf, + misfire_strategy, + executor_route_strategy, + executor_handler, + executor_param, + executor_block_strategy, + executor_timeout, + executor_fail_retry_count, + glue_type, + glue_source, + glue_remark, + glue_updatetime, + child_jobid, + trigger_status, + trigger_last_time, + trigger_next_time + ) VALUES ( + #{jobGroup}, + #{jobDesc}, + #{addTime}, + #{updateTime}, + #{author}, + #{alarmEmail}, + #{scheduleType}, + #{scheduleConf}, + #{misfireStrategy}, + #{executorRouteStrategy}, + #{executorHandler}, + #{executorParam}, + #{executorBlockStrategy}, + #{executorTimeout}, + #{executorFailRetryCount}, + #{glueType}, + #{glueSource}, + #{glueRemark}, + #{glueUpdatetime}, + #{childJobId}, + #{triggerStatus}, + #{triggerLastTime}, + #{triggerNextTime} + ); + + + + + + + UPDATE xxl_job_info + SET + job_group = #{jobGroup}, + job_desc = #{jobDesc}, + update_time = #{updateTime}, + author = #{author}, + alarm_email = #{alarmEmail}, + schedule_type = #{scheduleType}, + schedule_conf = #{scheduleConf}, + misfire_strategy = #{misfireStrategy}, + executor_route_strategy = #{executorRouteStrategy}, + executor_handler = #{executorHandler}, + executor_param = #{executorParam}, + executor_block_strategy = #{executorBlockStrategy}, + executor_timeout = ${executorTimeout}, + executor_fail_retry_count = ${executorFailRetryCount}, + glue_type = #{glueType}, + glue_source = #{glueSource}, + glue_remark = #{glueRemark}, + glue_updatetime = #{glueUpdatetime}, + child_jobid = #{childJobId}, + trigger_status = #{triggerStatus}, + trigger_last_time = #{triggerLastTime}, + trigger_next_time = #{triggerNextTime} + WHERE id = #{id} + + + + DELETE + FROM xxl_job_info + WHERE id = #{id} + + + + + + + + + + + UPDATE xxl_job_info + SET + trigger_last_time = #{triggerLastTime}, + trigger_next_time = #{triggerNextTime}, + trigger_status = #{triggerStatus} + WHERE id = #{id} + + + \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml new file mode 100644 index 00000000..699277c5 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogGlueMapper.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + t.id, + t.job_id, + t.glue_type, + t.glue_source, + t.glue_remark, + t.add_time, + t.update_time + + + + INSERT INTO xxl_job_logglue ( + `job_id`, + `glue_type`, + `glue_source`, + `glue_remark`, + `add_time`, + `update_time` + ) VALUES ( + #{jobId}, + #{glueType}, + #{glueSource}, + #{glueRemark}, + #{addTime}, + #{updateTime} + ); + + + + + + + DELETE FROM xxl_job_logglue + WHERE id NOT in( + SELECT id FROM( + SELECT id FROM xxl_job_logglue + WHERE `job_id` = #{jobId} + ORDER BY update_time desc + LIMIT 0, #{limit} + ) t1 + ) AND `job_id` = #{jobId} + + + + DELETE FROM xxl_job_logglue + WHERE `job_id` = #{jobId} + + + \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml new file mode 100644 index 00000000..4155f179 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogMapper.xml @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + t.id, + t.job_group, + t.job_id, + t.executor_address, + t.executor_handler, + t.executor_param, + t.executor_sharding_param, + t.executor_fail_retry_count, + t.trigger_time, + t.trigger_code, + t.trigger_msg, + t.handle_time, + t.handle_code, + t.handle_msg, + t.alarm_status + + + + + + + + + + + INSERT INTO xxl_job_log ( + `job_group`, + `job_id`, + `trigger_time`, + `trigger_code`, + `handle_code` + ) VALUES ( + #{jobGroup}, + #{jobId}, + #{triggerTime}, + #{triggerCode}, + #{handleCode} + ); + + + + + UPDATE xxl_job_log + SET + `trigger_time`= #{triggerTime}, + `trigger_code`= #{triggerCode}, + `trigger_msg`= #{triggerMsg}, + `executor_address`= #{executorAddress}, + `executor_handler`=#{executorHandler}, + `executor_param`= #{executorParam}, + `executor_sharding_param`= #{executorShardingParam}, + `executor_fail_retry_count`= #{executorFailRetryCount} + WHERE `id`= #{id} + + + + UPDATE xxl_job_log + SET + `handle_time`= #{handleTime}, + `handle_code`= #{handleCode}, + `handle_msg`= #{handleMsg} + WHERE `id`= #{id} + + + + delete from xxl_job_log + WHERE job_id = #{jobId} + + + + + + + + + + delete from xxl_job_log + WHERE id in + + #{item} + + + + + + + UPDATE xxl_job_log + SET + `alarm_status` = #{newAlarmStatus} + WHERE `id`= #{logId} AND `alarm_status` = #{oldAlarmStatus} + + + + + + \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml new file mode 100644 index 00000000..579d5f39 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobLogReportMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + t.id, + t.trigger_day, + t.running_count, + t.suc_count, + t.fail_count + + + + INSERT INTO xxl_job_log_report ( + `trigger_day`, + `running_count`, + `suc_count`, + `fail_count` + ) VALUES ( + #{triggerDay}, + #{runningCount}, + #{sucCount}, + #{failCount} + ); + + + + + UPDATE xxl_job_log_report + SET `running_count` = #{runningCount}, + `suc_count` = #{sucCount}, + `fail_count` = #{failCount} + WHERE `trigger_day` = #{triggerDay} + + + + + + + \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml new file mode 100644 index 00000000..4cae667a --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobRegistryMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + t.id, + t.registry_group, + t.registry_key, + t.registry_value, + t.update_time + + + + + + DELETE FROM xxl_job_registry + WHERE id in + + #{item} + + + + + + + UPDATE xxl_job_registry + SET `update_time` = #{updateTime} + WHERE `registry_group` = #{registryGroup} + AND `registry_key` = #{registryKey} + AND `registry_value` = #{registryValue} + + + + INSERT INTO xxl_job_registry( `registry_group` , `registry_key` , `registry_value`, `update_time`) + VALUES( #{registryGroup} , #{registryKey} , #{registryValue}, #{updateTime}) + + + + DELETE FROM xxl_job_registry + WHERE registry_group = #{registryGroup} + AND registry_key = #{registryKey} + AND registry_value = #{registryValue} + + + \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml new file mode 100644 index 00000000..9e09b4aa --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/mybatis-mapper/XxlJobUserMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + t.id, + t.username, + t.password, + t.role, + t.permission + + + + + + + + + + INSERT INTO xxl_job_user ( + username, + password, + role, + permission + ) VALUES ( + #{username}, + #{password}, + #{role}, + #{permission} + ); + + + + UPDATE xxl_job_user + SET + + password = #{password}, + + role = #{role}, + permission = #{permission} + WHERE id = #{id} + + + + DELETE + FROM xxl_job_user + WHERE id = #{id} + + + \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/css/ionicons.min.css b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/css/ionicons.min.css new file mode 100644 index 00000000..baba9e93 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/css/ionicons.min.css @@ -0,0 +1,11 @@ +@charset "UTF-8";/*! + Ionicons, v2.0.0 + Created by Ben Sperry for the Ionic Framework, http://ionicons.com/ + https://twitter.com/benjsperry https://twitter.com/ionicframework + MIT License: https://github.com/driftyco/ionicons + + Android-style icons originally built by Google’s + Material Design Icons: https://github.com/google/material-design-icons + used under CC BY http://creativecommons.org/licenses/by/4.0/ + Modified icons to fit ionicon’s grid from original. +*/@font-face{font-family:"Ionicons";src:url("../fonts/ionicons.eot?v=2.0.0");src:url("../fonts/ionicons.eot?v=2.0.0#iefix") format("embedded-opentype"),url("../fonts/ionicons.ttf?v=2.0.0") format("truetype"),url("../fonts/ionicons.woff?v=2.0.0") format("woff"),url("../fonts/ionicons.svg?v=2.0.0#Ionicons") format("svg");font-weight:normal;font-style:normal}.ion,.ionicons,.ion-alert:before,.ion-alert-circled:before,.ion-android-add:before,.ion-android-add-circle:before,.ion-android-alarm-clock:before,.ion-android-alert:before,.ion-android-apps:before,.ion-android-archive:before,.ion-android-arrow-back:before,.ion-android-arrow-down:before,.ion-android-arrow-dropdown:before,.ion-android-arrow-dropdown-circle:before,.ion-android-arrow-dropleft:before,.ion-android-arrow-dropleft-circle:before,.ion-android-arrow-dropright:before,.ion-android-arrow-dropright-circle:before,.ion-android-arrow-dropup:before,.ion-android-arrow-dropup-circle:before,.ion-android-arrow-forward:before,.ion-android-arrow-up:before,.ion-android-attach:before,.ion-android-bar:before,.ion-android-bicycle:before,.ion-android-boat:before,.ion-android-bookmark:before,.ion-android-bulb:before,.ion-android-bus:before,.ion-android-calendar:before,.ion-android-call:before,.ion-android-camera:before,.ion-android-cancel:before,.ion-android-car:before,.ion-android-cart:before,.ion-android-chat:before,.ion-android-checkbox:before,.ion-android-checkbox-blank:before,.ion-android-checkbox-outline:before,.ion-android-checkbox-outline-blank:before,.ion-android-checkmark-circle:before,.ion-android-clipboard:before,.ion-android-close:before,.ion-android-cloud:before,.ion-android-cloud-circle:before,.ion-android-cloud-done:before,.ion-android-cloud-outline:before,.ion-android-color-palette:before,.ion-android-compass:before,.ion-android-contact:before,.ion-android-contacts:before,.ion-android-contract:before,.ion-android-create:before,.ion-android-delete:before,.ion-android-desktop:before,.ion-android-document:before,.ion-android-done:before,.ion-android-done-all:before,.ion-android-download:before,.ion-android-drafts:before,.ion-android-exit:before,.ion-android-expand:before,.ion-android-favorite:before,.ion-android-favorite-outline:before,.ion-android-film:before,.ion-android-folder:before,.ion-android-folder-open:before,.ion-android-funnel:before,.ion-android-globe:before,.ion-android-hand:before,.ion-android-hangout:before,.ion-android-happy:before,.ion-android-home:before,.ion-android-image:before,.ion-android-laptop:before,.ion-android-list:before,.ion-android-locate:before,.ion-android-lock:before,.ion-android-mail:before,.ion-android-map:before,.ion-android-menu:before,.ion-android-microphone:before,.ion-android-microphone-off:before,.ion-android-more-horizontal:before,.ion-android-more-vertical:before,.ion-android-navigate:before,.ion-android-notifications:before,.ion-android-notifications-none:before,.ion-android-notifications-off:before,.ion-android-open:before,.ion-android-options:before,.ion-android-people:before,.ion-android-person:before,.ion-android-person-add:before,.ion-android-phone-landscape:before,.ion-android-phone-portrait:before,.ion-android-pin:before,.ion-android-plane:before,.ion-android-playstore:before,.ion-android-print:before,.ion-android-radio-button-off:before,.ion-android-radio-button-on:before,.ion-android-refresh:before,.ion-android-remove:before,.ion-android-remove-circle:before,.ion-android-restaurant:before,.ion-android-sad:before,.ion-android-search:before,.ion-android-send:before,.ion-android-settings:before,.ion-android-share:before,.ion-android-share-alt:before,.ion-android-star:before,.ion-android-star-half:before,.ion-android-star-outline:before,.ion-android-stopwatch:before,.ion-android-subway:before,.ion-android-sunny:before,.ion-android-sync:before,.ion-android-textsms:before,.ion-android-time:before,.ion-android-train:before,.ion-android-unlock:before,.ion-android-upload:before,.ion-android-volume-down:before,.ion-android-volume-mute:before,.ion-android-volume-off:before,.ion-android-volume-up:before,.ion-android-walk:before,.ion-android-warning:before,.ion-android-watch:before,.ion-android-wifi:before,.ion-aperture:before,.ion-archive:before,.ion-arrow-down-a:before,.ion-arrow-down-b:before,.ion-arrow-down-c:before,.ion-arrow-expand:before,.ion-arrow-graph-down-left:before,.ion-arrow-graph-down-right:before,.ion-arrow-graph-up-left:before,.ion-arrow-graph-up-right:before,.ion-arrow-left-a:before,.ion-arrow-left-b:before,.ion-arrow-left-c:before,.ion-arrow-move:before,.ion-arrow-resize:before,.ion-arrow-return-left:before,.ion-arrow-return-right:before,.ion-arrow-right-a:before,.ion-arrow-right-b:before,.ion-arrow-right-c:before,.ion-arrow-shrink:before,.ion-arrow-swap:before,.ion-arrow-up-a:before,.ion-arrow-up-b:before,.ion-arrow-up-c:before,.ion-asterisk:before,.ion-at:before,.ion-backspace:before,.ion-backspace-outline:before,.ion-bag:before,.ion-battery-charging:before,.ion-battery-empty:before,.ion-battery-full:before,.ion-battery-half:before,.ion-battery-low:before,.ion-beaker:before,.ion-beer:before,.ion-bluetooth:before,.ion-bonfire:before,.ion-bookmark:before,.ion-bowtie:before,.ion-briefcase:before,.ion-bug:before,.ion-calculator:before,.ion-calendar:before,.ion-camera:before,.ion-card:before,.ion-cash:before,.ion-chatbox:before,.ion-chatbox-working:before,.ion-chatboxes:before,.ion-chatbubble:before,.ion-chatbubble-working:before,.ion-chatbubbles:before,.ion-checkmark:before,.ion-checkmark-circled:before,.ion-checkmark-round:before,.ion-chevron-down:before,.ion-chevron-left:before,.ion-chevron-right:before,.ion-chevron-up:before,.ion-clipboard:before,.ion-clock:before,.ion-close:before,.ion-close-circled:before,.ion-close-round:before,.ion-closed-captioning:before,.ion-cloud:before,.ion-code:before,.ion-code-download:before,.ion-code-working:before,.ion-coffee:before,.ion-compass:before,.ion-compose:before,.ion-connection-bars:before,.ion-contrast:before,.ion-crop:before,.ion-cube:before,.ion-disc:before,.ion-document:before,.ion-document-text:before,.ion-drag:before,.ion-earth:before,.ion-easel:before,.ion-edit:before,.ion-egg:before,.ion-eject:before,.ion-email:before,.ion-email-unread:before,.ion-erlenmeyer-flask:before,.ion-erlenmeyer-flask-bubbles:before,.ion-eye:before,.ion-eye-disabled:before,.ion-female:before,.ion-filing:before,.ion-film-marker:before,.ion-fireball:before,.ion-flag:before,.ion-flame:before,.ion-flash:before,.ion-flash-off:before,.ion-folder:before,.ion-fork:before,.ion-fork-repo:before,.ion-forward:before,.ion-funnel:before,.ion-gear-a:before,.ion-gear-b:before,.ion-grid:before,.ion-hammer:before,.ion-happy:before,.ion-happy-outline:before,.ion-headphone:before,.ion-heart:before,.ion-heart-broken:before,.ion-help:before,.ion-help-buoy:before,.ion-help-circled:before,.ion-home:before,.ion-icecream:before,.ion-image:before,.ion-images:before,.ion-information:before,.ion-information-circled:before,.ion-ionic:before,.ion-ios-alarm:before,.ion-ios-alarm-outline:before,.ion-ios-albums:before,.ion-ios-albums-outline:before,.ion-ios-americanfootball:before,.ion-ios-americanfootball-outline:before,.ion-ios-analytics:before,.ion-ios-analytics-outline:before,.ion-ios-arrow-back:before,.ion-ios-arrow-down:before,.ion-ios-arrow-forward:before,.ion-ios-arrow-left:before,.ion-ios-arrow-right:before,.ion-ios-arrow-thin-down:before,.ion-ios-arrow-thin-left:before,.ion-ios-arrow-thin-right:before,.ion-ios-arrow-thin-up:before,.ion-ios-arrow-up:before,.ion-ios-at:before,.ion-ios-at-outline:before,.ion-ios-barcode:before,.ion-ios-barcode-outline:before,.ion-ios-baseball:before,.ion-ios-baseball-outline:before,.ion-ios-basketball:before,.ion-ios-basketball-outline:before,.ion-ios-bell:before,.ion-ios-bell-outline:before,.ion-ios-body:before,.ion-ios-body-outline:before,.ion-ios-bolt:before,.ion-ios-bolt-outline:before,.ion-ios-book:before,.ion-ios-book-outline:before,.ion-ios-bookmarks:before,.ion-ios-bookmarks-outline:before,.ion-ios-box:before,.ion-ios-box-outline:before,.ion-ios-briefcase:before,.ion-ios-briefcase-outline:before,.ion-ios-browsers:before,.ion-ios-browsers-outline:before,.ion-ios-calculator:before,.ion-ios-calculator-outline:before,.ion-ios-calendar:before,.ion-ios-calendar-outline:before,.ion-ios-camera:before,.ion-ios-camera-outline:before,.ion-ios-cart:before,.ion-ios-cart-outline:before,.ion-ios-chatboxes:before,.ion-ios-chatboxes-outline:before,.ion-ios-chatbubble:before,.ion-ios-chatbubble-outline:before,.ion-ios-checkmark:before,.ion-ios-checkmark-empty:before,.ion-ios-checkmark-outline:before,.ion-ios-circle-filled:before,.ion-ios-circle-outline:before,.ion-ios-clock:before,.ion-ios-clock-outline:before,.ion-ios-close:before,.ion-ios-close-empty:before,.ion-ios-close-outline:before,.ion-ios-cloud:before,.ion-ios-cloud-download:before,.ion-ios-cloud-download-outline:before,.ion-ios-cloud-outline:before,.ion-ios-cloud-upload:before,.ion-ios-cloud-upload-outline:before,.ion-ios-cloudy:before,.ion-ios-cloudy-night:before,.ion-ios-cloudy-night-outline:before,.ion-ios-cloudy-outline:before,.ion-ios-cog:before,.ion-ios-cog-outline:before,.ion-ios-color-filter:before,.ion-ios-color-filter-outline:before,.ion-ios-color-wand:before,.ion-ios-color-wand-outline:before,.ion-ios-compose:before,.ion-ios-compose-outline:before,.ion-ios-contact:before,.ion-ios-contact-outline:before,.ion-ios-copy:before,.ion-ios-copy-outline:before,.ion-ios-crop:before,.ion-ios-crop-strong:before,.ion-ios-download:before,.ion-ios-download-outline:before,.ion-ios-drag:before,.ion-ios-email:before,.ion-ios-email-outline:before,.ion-ios-eye:before,.ion-ios-eye-outline:before,.ion-ios-fastforward:before,.ion-ios-fastforward-outline:before,.ion-ios-filing:before,.ion-ios-filing-outline:before,.ion-ios-film:before,.ion-ios-film-outline:before,.ion-ios-flag:before,.ion-ios-flag-outline:before,.ion-ios-flame:before,.ion-ios-flame-outline:before,.ion-ios-flask:before,.ion-ios-flask-outline:before,.ion-ios-flower:before,.ion-ios-flower-outline:before,.ion-ios-folder:before,.ion-ios-folder-outline:before,.ion-ios-football:before,.ion-ios-football-outline:before,.ion-ios-game-controller-a:before,.ion-ios-game-controller-a-outline:before,.ion-ios-game-controller-b:before,.ion-ios-game-controller-b-outline:before,.ion-ios-gear:before,.ion-ios-gear-outline:before,.ion-ios-glasses:before,.ion-ios-glasses-outline:before,.ion-ios-grid-view:before,.ion-ios-grid-view-outline:before,.ion-ios-heart:before,.ion-ios-heart-outline:before,.ion-ios-help:before,.ion-ios-help-empty:before,.ion-ios-help-outline:before,.ion-ios-home:before,.ion-ios-home-outline:before,.ion-ios-infinite:before,.ion-ios-infinite-outline:before,.ion-ios-information:before,.ion-ios-information-empty:before,.ion-ios-information-outline:before,.ion-ios-ionic-outline:before,.ion-ios-keypad:before,.ion-ios-keypad-outline:before,.ion-ios-lightbulb:before,.ion-ios-lightbulb-outline:before,.ion-ios-list:before,.ion-ios-list-outline:before,.ion-ios-location:before,.ion-ios-location-outline:before,.ion-ios-locked:before,.ion-ios-locked-outline:before,.ion-ios-loop:before,.ion-ios-loop-strong:before,.ion-ios-medical:before,.ion-ios-medical-outline:before,.ion-ios-medkit:before,.ion-ios-medkit-outline:before,.ion-ios-mic:before,.ion-ios-mic-off:before,.ion-ios-mic-outline:before,.ion-ios-minus:before,.ion-ios-minus-empty:before,.ion-ios-minus-outline:before,.ion-ios-monitor:before,.ion-ios-monitor-outline:before,.ion-ios-moon:before,.ion-ios-moon-outline:before,.ion-ios-more:before,.ion-ios-more-outline:before,.ion-ios-musical-note:before,.ion-ios-musical-notes:before,.ion-ios-navigate:before,.ion-ios-navigate-outline:before,.ion-ios-nutrition:before,.ion-ios-nutrition-outline:before,.ion-ios-paper:before,.ion-ios-paper-outline:before,.ion-ios-paperplane:before,.ion-ios-paperplane-outline:before,.ion-ios-partlysunny:before,.ion-ios-partlysunny-outline:before,.ion-ios-pause:before,.ion-ios-pause-outline:before,.ion-ios-paw:before,.ion-ios-paw-outline:before,.ion-ios-people:before,.ion-ios-people-outline:before,.ion-ios-person:before,.ion-ios-person-outline:before,.ion-ios-personadd:before,.ion-ios-personadd-outline:before,.ion-ios-photos:before,.ion-ios-photos-outline:before,.ion-ios-pie:before,.ion-ios-pie-outline:before,.ion-ios-pint:before,.ion-ios-pint-outline:before,.ion-ios-play:before,.ion-ios-play-outline:before,.ion-ios-plus:before,.ion-ios-plus-empty:before,.ion-ios-plus-outline:before,.ion-ios-pricetag:before,.ion-ios-pricetag-outline:before,.ion-ios-pricetags:before,.ion-ios-pricetags-outline:before,.ion-ios-printer:before,.ion-ios-printer-outline:before,.ion-ios-pulse:before,.ion-ios-pulse-strong:before,.ion-ios-rainy:before,.ion-ios-rainy-outline:before,.ion-ios-recording:before,.ion-ios-recording-outline:before,.ion-ios-redo:before,.ion-ios-redo-outline:before,.ion-ios-refresh:before,.ion-ios-refresh-empty:before,.ion-ios-refresh-outline:before,.ion-ios-reload:before,.ion-ios-reverse-camera:before,.ion-ios-reverse-camera-outline:before,.ion-ios-rewind:before,.ion-ios-rewind-outline:before,.ion-ios-rose:before,.ion-ios-rose-outline:before,.ion-ios-search:before,.ion-ios-search-strong:before,.ion-ios-settings:before,.ion-ios-settings-strong:before,.ion-ios-shuffle:before,.ion-ios-shuffle-strong:before,.ion-ios-skipbackward:before,.ion-ios-skipbackward-outline:before,.ion-ios-skipforward:before,.ion-ios-skipforward-outline:before,.ion-ios-snowy:before,.ion-ios-speedometer:before,.ion-ios-speedometer-outline:before,.ion-ios-star:before,.ion-ios-star-half:before,.ion-ios-star-outline:before,.ion-ios-stopwatch:before,.ion-ios-stopwatch-outline:before,.ion-ios-sunny:before,.ion-ios-sunny-outline:before,.ion-ios-telephone:before,.ion-ios-telephone-outline:before,.ion-ios-tennisball:before,.ion-ios-tennisball-outline:before,.ion-ios-thunderstorm:before,.ion-ios-thunderstorm-outline:before,.ion-ios-time:before,.ion-ios-time-outline:before,.ion-ios-timer:before,.ion-ios-timer-outline:before,.ion-ios-toggle:before,.ion-ios-toggle-outline:before,.ion-ios-trash:before,.ion-ios-trash-outline:before,.ion-ios-undo:before,.ion-ios-undo-outline:before,.ion-ios-unlocked:before,.ion-ios-unlocked-outline:before,.ion-ios-upload:before,.ion-ios-upload-outline:before,.ion-ios-videocam:before,.ion-ios-videocam-outline:before,.ion-ios-volume-high:before,.ion-ios-volume-low:before,.ion-ios-wineglass:before,.ion-ios-wineglass-outline:before,.ion-ios-world:before,.ion-ios-world-outline:before,.ion-ipad:before,.ion-iphone:before,.ion-ipod:before,.ion-jet:before,.ion-key:before,.ion-knife:before,.ion-laptop:before,.ion-leaf:before,.ion-levels:before,.ion-lightbulb:before,.ion-link:before,.ion-load-a:before,.ion-load-b:before,.ion-load-c:before,.ion-load-d:before,.ion-location:before,.ion-lock-combination:before,.ion-locked:before,.ion-log-in:before,.ion-log-out:before,.ion-loop:before,.ion-magnet:before,.ion-male:before,.ion-man:before,.ion-map:before,.ion-medkit:before,.ion-merge:before,.ion-mic-a:before,.ion-mic-b:before,.ion-mic-c:before,.ion-minus:before,.ion-minus-circled:before,.ion-minus-round:before,.ion-model-s:before,.ion-monitor:before,.ion-more:before,.ion-mouse:before,.ion-music-note:before,.ion-navicon:before,.ion-navicon-round:before,.ion-navigate:before,.ion-network:before,.ion-no-smoking:before,.ion-nuclear:before,.ion-outlet:before,.ion-paintbrush:before,.ion-paintbucket:before,.ion-paper-airplane:before,.ion-paperclip:before,.ion-pause:before,.ion-person:before,.ion-person-add:before,.ion-person-stalker:before,.ion-pie-graph:before,.ion-pin:before,.ion-pinpoint:before,.ion-pizza:before,.ion-plane:before,.ion-planet:before,.ion-play:before,.ion-playstation:before,.ion-plus:before,.ion-plus-circled:before,.ion-plus-round:before,.ion-podium:before,.ion-pound:before,.ion-power:before,.ion-pricetag:before,.ion-pricetags:before,.ion-printer:before,.ion-pull-request:before,.ion-qr-scanner:before,.ion-quote:before,.ion-radio-waves:before,.ion-record:before,.ion-refresh:before,.ion-reply:before,.ion-reply-all:before,.ion-ribbon-a:before,.ion-ribbon-b:before,.ion-sad:before,.ion-sad-outline:before,.ion-scissors:before,.ion-search:before,.ion-settings:before,.ion-share:before,.ion-shuffle:before,.ion-skip-backward:before,.ion-skip-forward:before,.ion-social-android:before,.ion-social-android-outline:before,.ion-social-angular:before,.ion-social-angular-outline:before,.ion-social-apple:before,.ion-social-apple-outline:before,.ion-social-bitcoin:before,.ion-social-bitcoin-outline:before,.ion-social-buffer:before,.ion-social-buffer-outline:before,.ion-social-chrome:before,.ion-social-chrome-outline:before,.ion-social-codepen:before,.ion-social-codepen-outline:before,.ion-social-css3:before,.ion-social-css3-outline:before,.ion-social-designernews:before,.ion-social-designernews-outline:before,.ion-social-dribbble:before,.ion-social-dribbble-outline:before,.ion-social-dropbox:before,.ion-social-dropbox-outline:before,.ion-social-euro:before,.ion-social-euro-outline:before,.ion-social-facebook:before,.ion-social-facebook-outline:before,.ion-social-foursquare:before,.ion-social-foursquare-outline:before,.ion-social-freebsd-devil:before,.ion-social-github:before,.ion-social-github-outline:before,.ion-social-google:before,.ion-social-google-outline:before,.ion-social-googleplus:before,.ion-social-googleplus-outline:before,.ion-social-hackernews:before,.ion-social-hackernews-outline:before,.ion-social-html5:before,.ion-social-html5-outline:before,.ion-social-instagram:before,.ion-social-instagram-outline:before,.ion-social-javascript:before,.ion-social-javascript-outline:before,.ion-social-linkedin:before,.ion-social-linkedin-outline:before,.ion-social-markdown:before,.ion-social-nodejs:before,.ion-social-octocat:before,.ion-social-pinterest:before,.ion-social-pinterest-outline:before,.ion-social-python:before,.ion-social-reddit:before,.ion-social-reddit-outline:before,.ion-social-rss:before,.ion-social-rss-outline:before,.ion-social-sass:before,.ion-social-skype:before,.ion-social-skype-outline:before,.ion-social-snapchat:before,.ion-social-snapchat-outline:before,.ion-social-tumblr:before,.ion-social-tumblr-outline:before,.ion-social-tux:before,.ion-social-twitch:before,.ion-social-twitch-outline:before,.ion-social-twitter:before,.ion-social-twitter-outline:before,.ion-social-usd:before,.ion-social-usd-outline:before,.ion-social-vimeo:before,.ion-social-vimeo-outline:before,.ion-social-whatsapp:before,.ion-social-whatsapp-outline:before,.ion-social-windows:before,.ion-social-windows-outline:before,.ion-social-wordpress:before,.ion-social-wordpress-outline:before,.ion-social-yahoo:before,.ion-social-yahoo-outline:before,.ion-social-yen:before,.ion-social-yen-outline:before,.ion-social-youtube:before,.ion-social-youtube-outline:before,.ion-soup-can:before,.ion-soup-can-outline:before,.ion-speakerphone:before,.ion-speedometer:before,.ion-spoon:before,.ion-star:before,.ion-stats-bars:before,.ion-steam:before,.ion-stop:before,.ion-thermometer:before,.ion-thumbsdown:before,.ion-thumbsup:before,.ion-toggle:before,.ion-toggle-filled:before,.ion-transgender:before,.ion-trash-a:before,.ion-trash-b:before,.ion-trophy:before,.ion-tshirt:before,.ion-tshirt-outline:before,.ion-umbrella:before,.ion-university:before,.ion-unlocked:before,.ion-upload:before,.ion-usb:before,.ion-videocamera:before,.ion-volume-high:before,.ion-volume-low:before,.ion-volume-medium:before,.ion-volume-mute:before,.ion-wand:before,.ion-waterdrop:before,.ion-wifi:before,.ion-wineglass:before,.ion-woman:before,.ion-wrench:before,.ion-xbox:before{display:inline-block;font-family:"Ionicons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;text-rendering:auto;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ion-alert:before{content:"\f101"}.ion-alert-circled:before{content:"\f100"}.ion-android-add:before{content:"\f2c7"}.ion-android-add-circle:before{content:"\f359"}.ion-android-alarm-clock:before{content:"\f35a"}.ion-android-alert:before{content:"\f35b"}.ion-android-apps:before{content:"\f35c"}.ion-android-archive:before{content:"\f2c9"}.ion-android-arrow-back:before{content:"\f2ca"}.ion-android-arrow-down:before{content:"\f35d"}.ion-android-arrow-dropdown:before{content:"\f35f"}.ion-android-arrow-dropdown-circle:before{content:"\f35e"}.ion-android-arrow-dropleft:before{content:"\f361"}.ion-android-arrow-dropleft-circle:before{content:"\f360"}.ion-android-arrow-dropright:before{content:"\f363"}.ion-android-arrow-dropright-circle:before{content:"\f362"}.ion-android-arrow-dropup:before{content:"\f365"}.ion-android-arrow-dropup-circle:before{content:"\f364"}.ion-android-arrow-forward:before{content:"\f30f"}.ion-android-arrow-up:before{content:"\f366"}.ion-android-attach:before{content:"\f367"}.ion-android-bar:before{content:"\f368"}.ion-android-bicycle:before{content:"\f369"}.ion-android-boat:before{content:"\f36a"}.ion-android-bookmark:before{content:"\f36b"}.ion-android-bulb:before{content:"\f36c"}.ion-android-bus:before{content:"\f36d"}.ion-android-calendar:before{content:"\f2d1"}.ion-android-call:before{content:"\f2d2"}.ion-android-camera:before{content:"\f2d3"}.ion-android-cancel:before{content:"\f36e"}.ion-android-car:before{content:"\f36f"}.ion-android-cart:before{content:"\f370"}.ion-android-chat:before{content:"\f2d4"}.ion-android-checkbox:before{content:"\f374"}.ion-android-checkbox-blank:before{content:"\f371"}.ion-android-checkbox-outline:before{content:"\f373"}.ion-android-checkbox-outline-blank:before{content:"\f372"}.ion-android-checkmark-circle:before{content:"\f375"}.ion-android-clipboard:before{content:"\f376"}.ion-android-close:before{content:"\f2d7"}.ion-android-cloud:before{content:"\f37a"}.ion-android-cloud-circle:before{content:"\f377"}.ion-android-cloud-done:before{content:"\f378"}.ion-android-cloud-outline:before{content:"\f379"}.ion-android-color-palette:before{content:"\f37b"}.ion-android-compass:before{content:"\f37c"}.ion-android-contact:before{content:"\f2d8"}.ion-android-contacts:before{content:"\f2d9"}.ion-android-contract:before{content:"\f37d"}.ion-android-create:before{content:"\f37e"}.ion-android-delete:before{content:"\f37f"}.ion-android-desktop:before{content:"\f380"}.ion-android-document:before{content:"\f381"}.ion-android-done:before{content:"\f383"}.ion-android-done-all:before{content:"\f382"}.ion-android-download:before{content:"\f2dd"}.ion-android-drafts:before{content:"\f384"}.ion-android-exit:before{content:"\f385"}.ion-android-expand:before{content:"\f386"}.ion-android-favorite:before{content:"\f388"}.ion-android-favorite-outline:before{content:"\f387"}.ion-android-film:before{content:"\f389"}.ion-android-folder:before{content:"\f2e0"}.ion-android-folder-open:before{content:"\f38a"}.ion-android-funnel:before{content:"\f38b"}.ion-android-globe:before{content:"\f38c"}.ion-android-hand:before{content:"\f2e3"}.ion-android-hangout:before{content:"\f38d"}.ion-android-happy:before{content:"\f38e"}.ion-android-home:before{content:"\f38f"}.ion-android-image:before{content:"\f2e4"}.ion-android-laptop:before{content:"\f390"}.ion-android-list:before{content:"\f391"}.ion-android-locate:before{content:"\f2e9"}.ion-android-lock:before{content:"\f392"}.ion-android-mail:before{content:"\f2eb"}.ion-android-map:before{content:"\f393"}.ion-android-menu:before{content:"\f394"}.ion-android-microphone:before{content:"\f2ec"}.ion-android-microphone-off:before{content:"\f395"}.ion-android-more-horizontal:before{content:"\f396"}.ion-android-more-vertical:before{content:"\f397"}.ion-android-navigate:before{content:"\f398"}.ion-android-notifications:before{content:"\f39b"}.ion-android-notifications-none:before{content:"\f399"}.ion-android-notifications-off:before{content:"\f39a"}.ion-android-open:before{content:"\f39c"}.ion-android-options:before{content:"\f39d"}.ion-android-people:before{content:"\f39e"}.ion-android-person:before{content:"\f3a0"}.ion-android-person-add:before{content:"\f39f"}.ion-android-phone-landscape:before{content:"\f3a1"}.ion-android-phone-portrait:before{content:"\f3a2"}.ion-android-pin:before{content:"\f3a3"}.ion-android-plane:before{content:"\f3a4"}.ion-android-playstore:before{content:"\f2f0"}.ion-android-print:before{content:"\f3a5"}.ion-android-radio-button-off:before{content:"\f3a6"}.ion-android-radio-button-on:before{content:"\f3a7"}.ion-android-refresh:before{content:"\f3a8"}.ion-android-remove:before{content:"\f2f4"}.ion-android-remove-circle:before{content:"\f3a9"}.ion-android-restaurant:before{content:"\f3aa"}.ion-android-sad:before{content:"\f3ab"}.ion-android-search:before{content:"\f2f5"}.ion-android-send:before{content:"\f2f6"}.ion-android-settings:before{content:"\f2f7"}.ion-android-share:before{content:"\f2f8"}.ion-android-share-alt:before{content:"\f3ac"}.ion-android-star:before{content:"\f2fc"}.ion-android-star-half:before{content:"\f3ad"}.ion-android-star-outline:before{content:"\f3ae"}.ion-android-stopwatch:before{content:"\f2fd"}.ion-android-subway:before{content:"\f3af"}.ion-android-sunny:before{content:"\f3b0"}.ion-android-sync:before{content:"\f3b1"}.ion-android-textsms:before{content:"\f3b2"}.ion-android-time:before{content:"\f3b3"}.ion-android-train:before{content:"\f3b4"}.ion-android-unlock:before{content:"\f3b5"}.ion-android-upload:before{content:"\f3b6"}.ion-android-volume-down:before{content:"\f3b7"}.ion-android-volume-mute:before{content:"\f3b8"}.ion-android-volume-off:before{content:"\f3b9"}.ion-android-volume-up:before{content:"\f3ba"}.ion-android-walk:before{content:"\f3bb"}.ion-android-warning:before{content:"\f3bc"}.ion-android-watch:before{content:"\f3bd"}.ion-android-wifi:before{content:"\f305"}.ion-aperture:before{content:"\f313"}.ion-archive:before{content:"\f102"}.ion-arrow-down-a:before{content:"\f103"}.ion-arrow-down-b:before{content:"\f104"}.ion-arrow-down-c:before{content:"\f105"}.ion-arrow-expand:before{content:"\f25e"}.ion-arrow-graph-down-left:before{content:"\f25f"}.ion-arrow-graph-down-right:before{content:"\f260"}.ion-arrow-graph-up-left:before{content:"\f261"}.ion-arrow-graph-up-right:before{content:"\f262"}.ion-arrow-left-a:before{content:"\f106"}.ion-arrow-left-b:before{content:"\f107"}.ion-arrow-left-c:before{content:"\f108"}.ion-arrow-move:before{content:"\f263"}.ion-arrow-resize:before{content:"\f264"}.ion-arrow-return-left:before{content:"\f265"}.ion-arrow-return-right:before{content:"\f266"}.ion-arrow-right-a:before{content:"\f109"}.ion-arrow-right-b:before{content:"\f10a"}.ion-arrow-right-c:before{content:"\f10b"}.ion-arrow-shrink:before{content:"\f267"}.ion-arrow-swap:before{content:"\f268"}.ion-arrow-up-a:before{content:"\f10c"}.ion-arrow-up-b:before{content:"\f10d"}.ion-arrow-up-c:before{content:"\f10e"}.ion-asterisk:before{content:"\f314"}.ion-at:before{content:"\f10f"}.ion-backspace:before{content:"\f3bf"}.ion-backspace-outline:before{content:"\f3be"}.ion-bag:before{content:"\f110"}.ion-battery-charging:before{content:"\f111"}.ion-battery-empty:before{content:"\f112"}.ion-battery-full:before{content:"\f113"}.ion-battery-half:before{content:"\f114"}.ion-battery-low:before{content:"\f115"}.ion-beaker:before{content:"\f269"}.ion-beer:before{content:"\f26a"}.ion-bluetooth:before{content:"\f116"}.ion-bonfire:before{content:"\f315"}.ion-bookmark:before{content:"\f26b"}.ion-bowtie:before{content:"\f3c0"}.ion-briefcase:before{content:"\f26c"}.ion-bug:before{content:"\f2be"}.ion-calculator:before{content:"\f26d"}.ion-calendar:before{content:"\f117"}.ion-camera:before{content:"\f118"}.ion-card:before{content:"\f119"}.ion-cash:before{content:"\f316"}.ion-chatbox:before{content:"\f11b"}.ion-chatbox-working:before{content:"\f11a"}.ion-chatboxes:before{content:"\f11c"}.ion-chatbubble:before{content:"\f11e"}.ion-chatbubble-working:before{content:"\f11d"}.ion-chatbubbles:before{content:"\f11f"}.ion-checkmark:before{content:"\f122"}.ion-checkmark-circled:before{content:"\f120"}.ion-checkmark-round:before{content:"\f121"}.ion-chevron-down:before{content:"\f123"}.ion-chevron-left:before{content:"\f124"}.ion-chevron-right:before{content:"\f125"}.ion-chevron-up:before{content:"\f126"}.ion-clipboard:before{content:"\f127"}.ion-clock:before{content:"\f26e"}.ion-close:before{content:"\f12a"}.ion-close-circled:before{content:"\f128"}.ion-close-round:before{content:"\f129"}.ion-closed-captioning:before{content:"\f317"}.ion-cloud:before{content:"\f12b"}.ion-code:before{content:"\f271"}.ion-code-download:before{content:"\f26f"}.ion-code-working:before{content:"\f270"}.ion-coffee:before{content:"\f272"}.ion-compass:before{content:"\f273"}.ion-compose:before{content:"\f12c"}.ion-connection-bars:before{content:"\f274"}.ion-contrast:before{content:"\f275"}.ion-crop:before{content:"\f3c1"}.ion-cube:before{content:"\f318"}.ion-disc:before{content:"\f12d"}.ion-document:before{content:"\f12f"}.ion-document-text:before{content:"\f12e"}.ion-drag:before{content:"\f130"}.ion-earth:before{content:"\f276"}.ion-easel:before{content:"\f3c2"}.ion-edit:before{content:"\f2bf"}.ion-egg:before{content:"\f277"}.ion-eject:before{content:"\f131"}.ion-email:before{content:"\f132"}.ion-email-unread:before{content:"\f3c3"}.ion-erlenmeyer-flask:before{content:"\f3c5"}.ion-erlenmeyer-flask-bubbles:before{content:"\f3c4"}.ion-eye:before{content:"\f133"}.ion-eye-disabled:before{content:"\f306"}.ion-female:before{content:"\f278"}.ion-filing:before{content:"\f134"}.ion-film-marker:before{content:"\f135"}.ion-fireball:before{content:"\f319"}.ion-flag:before{content:"\f279"}.ion-flame:before{content:"\f31a"}.ion-flash:before{content:"\f137"}.ion-flash-off:before{content:"\f136"}.ion-folder:before{content:"\f139"}.ion-fork:before{content:"\f27a"}.ion-fork-repo:before{content:"\f2c0"}.ion-forward:before{content:"\f13a"}.ion-funnel:before{content:"\f31b"}.ion-gear-a:before{content:"\f13d"}.ion-gear-b:before{content:"\f13e"}.ion-grid:before{content:"\f13f"}.ion-hammer:before{content:"\f27b"}.ion-happy:before{content:"\f31c"}.ion-happy-outline:before{content:"\f3c6"}.ion-headphone:before{content:"\f140"}.ion-heart:before{content:"\f141"}.ion-heart-broken:before{content:"\f31d"}.ion-help:before{content:"\f143"}.ion-help-buoy:before{content:"\f27c"}.ion-help-circled:before{content:"\f142"}.ion-home:before{content:"\f144"}.ion-icecream:before{content:"\f27d"}.ion-image:before{content:"\f147"}.ion-images:before{content:"\f148"}.ion-information:before{content:"\f14a"}.ion-information-circled:before{content:"\f149"}.ion-ionic:before{content:"\f14b"}.ion-ios-alarm:before{content:"\f3c8"}.ion-ios-alarm-outline:before{content:"\f3c7"}.ion-ios-albums:before{content:"\f3ca"}.ion-ios-albums-outline:before{content:"\f3c9"}.ion-ios-americanfootball:before{content:"\f3cc"}.ion-ios-americanfootball-outline:before{content:"\f3cb"}.ion-ios-analytics:before{content:"\f3ce"}.ion-ios-analytics-outline:before{content:"\f3cd"}.ion-ios-arrow-back:before{content:"\f3cf"}.ion-ios-arrow-down:before{content:"\f3d0"}.ion-ios-arrow-forward:before{content:"\f3d1"}.ion-ios-arrow-left:before{content:"\f3d2"}.ion-ios-arrow-right:before{content:"\f3d3"}.ion-ios-arrow-thin-down:before{content:"\f3d4"}.ion-ios-arrow-thin-left:before{content:"\f3d5"}.ion-ios-arrow-thin-right:before{content:"\f3d6"}.ion-ios-arrow-thin-up:before{content:"\f3d7"}.ion-ios-arrow-up:before{content:"\f3d8"}.ion-ios-at:before{content:"\f3da"}.ion-ios-at-outline:before{content:"\f3d9"}.ion-ios-barcode:before{content:"\f3dc"}.ion-ios-barcode-outline:before{content:"\f3db"}.ion-ios-baseball:before{content:"\f3de"}.ion-ios-baseball-outline:before{content:"\f3dd"}.ion-ios-basketball:before{content:"\f3e0"}.ion-ios-basketball-outline:before{content:"\f3df"}.ion-ios-bell:before{content:"\f3e2"}.ion-ios-bell-outline:before{content:"\f3e1"}.ion-ios-body:before{content:"\f3e4"}.ion-ios-body-outline:before{content:"\f3e3"}.ion-ios-bolt:before{content:"\f3e6"}.ion-ios-bolt-outline:before{content:"\f3e5"}.ion-ios-book:before{content:"\f3e8"}.ion-ios-book-outline:before{content:"\f3e7"}.ion-ios-bookmarks:before{content:"\f3ea"}.ion-ios-bookmarks-outline:before{content:"\f3e9"}.ion-ios-box:before{content:"\f3ec"}.ion-ios-box-outline:before{content:"\f3eb"}.ion-ios-briefcase:before{content:"\f3ee"}.ion-ios-briefcase-outline:before{content:"\f3ed"}.ion-ios-browsers:before{content:"\f3f0"}.ion-ios-browsers-outline:before{content:"\f3ef"}.ion-ios-calculator:before{content:"\f3f2"}.ion-ios-calculator-outline:before{content:"\f3f1"}.ion-ios-calendar:before{content:"\f3f4"}.ion-ios-calendar-outline:before{content:"\f3f3"}.ion-ios-camera:before{content:"\f3f6"}.ion-ios-camera-outline:before{content:"\f3f5"}.ion-ios-cart:before{content:"\f3f8"}.ion-ios-cart-outline:before{content:"\f3f7"}.ion-ios-chatboxes:before{content:"\f3fa"}.ion-ios-chatboxes-outline:before{content:"\f3f9"}.ion-ios-chatbubble:before{content:"\f3fc"}.ion-ios-chatbubble-outline:before{content:"\f3fb"}.ion-ios-checkmark:before{content:"\f3ff"}.ion-ios-checkmark-empty:before{content:"\f3fd"}.ion-ios-checkmark-outline:before{content:"\f3fe"}.ion-ios-circle-filled:before{content:"\f400"}.ion-ios-circle-outline:before{content:"\f401"}.ion-ios-clock:before{content:"\f403"}.ion-ios-clock-outline:before{content:"\f402"}.ion-ios-close:before{content:"\f406"}.ion-ios-close-empty:before{content:"\f404"}.ion-ios-close-outline:before{content:"\f405"}.ion-ios-cloud:before{content:"\f40c"}.ion-ios-cloud-download:before{content:"\f408"}.ion-ios-cloud-download-outline:before{content:"\f407"}.ion-ios-cloud-outline:before{content:"\f409"}.ion-ios-cloud-upload:before{content:"\f40b"}.ion-ios-cloud-upload-outline:before{content:"\f40a"}.ion-ios-cloudy:before{content:"\f410"}.ion-ios-cloudy-night:before{content:"\f40e"}.ion-ios-cloudy-night-outline:before{content:"\f40d"}.ion-ios-cloudy-outline:before{content:"\f40f"}.ion-ios-cog:before{content:"\f412"}.ion-ios-cog-outline:before{content:"\f411"}.ion-ios-color-filter:before{content:"\f414"}.ion-ios-color-filter-outline:before{content:"\f413"}.ion-ios-color-wand:before{content:"\f416"}.ion-ios-color-wand-outline:before{content:"\f415"}.ion-ios-compose:before{content:"\f418"}.ion-ios-compose-outline:before{content:"\f417"}.ion-ios-contact:before{content:"\f41a"}.ion-ios-contact-outline:before{content:"\f419"}.ion-ios-copy:before{content:"\f41c"}.ion-ios-copy-outline:before{content:"\f41b"}.ion-ios-crop:before{content:"\f41e"}.ion-ios-crop-strong:before{content:"\f41d"}.ion-ios-download:before{content:"\f420"}.ion-ios-download-outline:before{content:"\f41f"}.ion-ios-drag:before{content:"\f421"}.ion-ios-email:before{content:"\f423"}.ion-ios-email-outline:before{content:"\f422"}.ion-ios-eye:before{content:"\f425"}.ion-ios-eye-outline:before{content:"\f424"}.ion-ios-fastforward:before{content:"\f427"}.ion-ios-fastforward-outline:before{content:"\f426"}.ion-ios-filing:before{content:"\f429"}.ion-ios-filing-outline:before{content:"\f428"}.ion-ios-film:before{content:"\f42b"}.ion-ios-film-outline:before{content:"\f42a"}.ion-ios-flag:before{content:"\f42d"}.ion-ios-flag-outline:before{content:"\f42c"}.ion-ios-flame:before{content:"\f42f"}.ion-ios-flame-outline:before{content:"\f42e"}.ion-ios-flask:before{content:"\f431"}.ion-ios-flask-outline:before{content:"\f430"}.ion-ios-flower:before{content:"\f433"}.ion-ios-flower-outline:before{content:"\f432"}.ion-ios-folder:before{content:"\f435"}.ion-ios-folder-outline:before{content:"\f434"}.ion-ios-football:before{content:"\f437"}.ion-ios-football-outline:before{content:"\f436"}.ion-ios-game-controller-a:before{content:"\f439"}.ion-ios-game-controller-a-outline:before{content:"\f438"}.ion-ios-game-controller-b:before{content:"\f43b"}.ion-ios-game-controller-b-outline:before{content:"\f43a"}.ion-ios-gear:before{content:"\f43d"}.ion-ios-gear-outline:before{content:"\f43c"}.ion-ios-glasses:before{content:"\f43f"}.ion-ios-glasses-outline:before{content:"\f43e"}.ion-ios-grid-view:before{content:"\f441"}.ion-ios-grid-view-outline:before{content:"\f440"}.ion-ios-heart:before{content:"\f443"}.ion-ios-heart-outline:before{content:"\f442"}.ion-ios-help:before{content:"\f446"}.ion-ios-help-empty:before{content:"\f444"}.ion-ios-help-outline:before{content:"\f445"}.ion-ios-home:before{content:"\f448"}.ion-ios-home-outline:before{content:"\f447"}.ion-ios-infinite:before{content:"\f44a"}.ion-ios-infinite-outline:before{content:"\f449"}.ion-ios-information:before{content:"\f44d"}.ion-ios-information-empty:before{content:"\f44b"}.ion-ios-information-outline:before{content:"\f44c"}.ion-ios-ionic-outline:before{content:"\f44e"}.ion-ios-keypad:before{content:"\f450"}.ion-ios-keypad-outline:before{content:"\f44f"}.ion-ios-lightbulb:before{content:"\f452"}.ion-ios-lightbulb-outline:before{content:"\f451"}.ion-ios-list:before{content:"\f454"}.ion-ios-list-outline:before{content:"\f453"}.ion-ios-location:before{content:"\f456"}.ion-ios-location-outline:before{content:"\f455"}.ion-ios-locked:before{content:"\f458"}.ion-ios-locked-outline:before{content:"\f457"}.ion-ios-loop:before{content:"\f45a"}.ion-ios-loop-strong:before{content:"\f459"}.ion-ios-medical:before{content:"\f45c"}.ion-ios-medical-outline:before{content:"\f45b"}.ion-ios-medkit:before{content:"\f45e"}.ion-ios-medkit-outline:before{content:"\f45d"}.ion-ios-mic:before{content:"\f461"}.ion-ios-mic-off:before{content:"\f45f"}.ion-ios-mic-outline:before{content:"\f460"}.ion-ios-minus:before{content:"\f464"}.ion-ios-minus-empty:before{content:"\f462"}.ion-ios-minus-outline:before{content:"\f463"}.ion-ios-monitor:before{content:"\f466"}.ion-ios-monitor-outline:before{content:"\f465"}.ion-ios-moon:before{content:"\f468"}.ion-ios-moon-outline:before{content:"\f467"}.ion-ios-more:before{content:"\f46a"}.ion-ios-more-outline:before{content:"\f469"}.ion-ios-musical-note:before{content:"\f46b"}.ion-ios-musical-notes:before{content:"\f46c"}.ion-ios-navigate:before{content:"\f46e"}.ion-ios-navigate-outline:before{content:"\f46d"}.ion-ios-nutrition:before{content:"\f470"}.ion-ios-nutrition-outline:before{content:"\f46f"}.ion-ios-paper:before{content:"\f472"}.ion-ios-paper-outline:before{content:"\f471"}.ion-ios-paperplane:before{content:"\f474"}.ion-ios-paperplane-outline:before{content:"\f473"}.ion-ios-partlysunny:before{content:"\f476"}.ion-ios-partlysunny-outline:before{content:"\f475"}.ion-ios-pause:before{content:"\f478"}.ion-ios-pause-outline:before{content:"\f477"}.ion-ios-paw:before{content:"\f47a"}.ion-ios-paw-outline:before{content:"\f479"}.ion-ios-people:before{content:"\f47c"}.ion-ios-people-outline:before{content:"\f47b"}.ion-ios-person:before{content:"\f47e"}.ion-ios-person-outline:before{content:"\f47d"}.ion-ios-personadd:before{content:"\f480"}.ion-ios-personadd-outline:before{content:"\f47f"}.ion-ios-photos:before{content:"\f482"}.ion-ios-photos-outline:before{content:"\f481"}.ion-ios-pie:before{content:"\f484"}.ion-ios-pie-outline:before{content:"\f483"}.ion-ios-pint:before{content:"\f486"}.ion-ios-pint-outline:before{content:"\f485"}.ion-ios-play:before{content:"\f488"}.ion-ios-play-outline:before{content:"\f487"}.ion-ios-plus:before{content:"\f48b"}.ion-ios-plus-empty:before{content:"\f489"}.ion-ios-plus-outline:before{content:"\f48a"}.ion-ios-pricetag:before{content:"\f48d"}.ion-ios-pricetag-outline:before{content:"\f48c"}.ion-ios-pricetags:before{content:"\f48f"}.ion-ios-pricetags-outline:before{content:"\f48e"}.ion-ios-printer:before{content:"\f491"}.ion-ios-printer-outline:before{content:"\f490"}.ion-ios-pulse:before{content:"\f493"}.ion-ios-pulse-strong:before{content:"\f492"}.ion-ios-rainy:before{content:"\f495"}.ion-ios-rainy-outline:before{content:"\f494"}.ion-ios-recording:before{content:"\f497"}.ion-ios-recording-outline:before{content:"\f496"}.ion-ios-redo:before{content:"\f499"}.ion-ios-redo-outline:before{content:"\f498"}.ion-ios-refresh:before{content:"\f49c"}.ion-ios-refresh-empty:before{content:"\f49a"}.ion-ios-refresh-outline:before{content:"\f49b"}.ion-ios-reload:before{content:"\f49d"}.ion-ios-reverse-camera:before{content:"\f49f"}.ion-ios-reverse-camera-outline:before{content:"\f49e"}.ion-ios-rewind:before{content:"\f4a1"}.ion-ios-rewind-outline:before{content:"\f4a0"}.ion-ios-rose:before{content:"\f4a3"}.ion-ios-rose-outline:before{content:"\f4a2"}.ion-ios-search:before{content:"\f4a5"}.ion-ios-search-strong:before{content:"\f4a4"}.ion-ios-settings:before{content:"\f4a7"}.ion-ios-settings-strong:before{content:"\f4a6"}.ion-ios-shuffle:before{content:"\f4a9"}.ion-ios-shuffle-strong:before{content:"\f4a8"}.ion-ios-skipbackward:before{content:"\f4ab"}.ion-ios-skipbackward-outline:before{content:"\f4aa"}.ion-ios-skipforward:before{content:"\f4ad"}.ion-ios-skipforward-outline:before{content:"\f4ac"}.ion-ios-snowy:before{content:"\f4ae"}.ion-ios-speedometer:before{content:"\f4b0"}.ion-ios-speedometer-outline:before{content:"\f4af"}.ion-ios-star:before{content:"\f4b3"}.ion-ios-star-half:before{content:"\f4b1"}.ion-ios-star-outline:before{content:"\f4b2"}.ion-ios-stopwatch:before{content:"\f4b5"}.ion-ios-stopwatch-outline:before{content:"\f4b4"}.ion-ios-sunny:before{content:"\f4b7"}.ion-ios-sunny-outline:before{content:"\f4b6"}.ion-ios-telephone:before{content:"\f4b9"}.ion-ios-telephone-outline:before{content:"\f4b8"}.ion-ios-tennisball:before{content:"\f4bb"}.ion-ios-tennisball-outline:before{content:"\f4ba"}.ion-ios-thunderstorm:before{content:"\f4bd"}.ion-ios-thunderstorm-outline:before{content:"\f4bc"}.ion-ios-time:before{content:"\f4bf"}.ion-ios-time-outline:before{content:"\f4be"}.ion-ios-timer:before{content:"\f4c1"}.ion-ios-timer-outline:before{content:"\f4c0"}.ion-ios-toggle:before{content:"\f4c3"}.ion-ios-toggle-outline:before{content:"\f4c2"}.ion-ios-trash:before{content:"\f4c5"}.ion-ios-trash-outline:before{content:"\f4c4"}.ion-ios-undo:before{content:"\f4c7"}.ion-ios-undo-outline:before{content:"\f4c6"}.ion-ios-unlocked:before{content:"\f4c9"}.ion-ios-unlocked-outline:before{content:"\f4c8"}.ion-ios-upload:before{content:"\f4cb"}.ion-ios-upload-outline:before{content:"\f4ca"}.ion-ios-videocam:before{content:"\f4cd"}.ion-ios-videocam-outline:before{content:"\f4cc"}.ion-ios-volume-high:before{content:"\f4ce"}.ion-ios-volume-low:before{content:"\f4cf"}.ion-ios-wineglass:before{content:"\f4d1"}.ion-ios-wineglass-outline:before{content:"\f4d0"}.ion-ios-world:before{content:"\f4d3"}.ion-ios-world-outline:before{content:"\f4d2"}.ion-ipad:before{content:"\f1f9"}.ion-iphone:before{content:"\f1fa"}.ion-ipod:before{content:"\f1fb"}.ion-jet:before{content:"\f295"}.ion-key:before{content:"\f296"}.ion-knife:before{content:"\f297"}.ion-laptop:before{content:"\f1fc"}.ion-leaf:before{content:"\f1fd"}.ion-levels:before{content:"\f298"}.ion-lightbulb:before{content:"\f299"}.ion-link:before{content:"\f1fe"}.ion-load-a:before{content:"\f29a"}.ion-load-b:before{content:"\f29b"}.ion-load-c:before{content:"\f29c"}.ion-load-d:before{content:"\f29d"}.ion-location:before{content:"\f1ff"}.ion-lock-combination:before{content:"\f4d4"}.ion-locked:before{content:"\f200"}.ion-log-in:before{content:"\f29e"}.ion-log-out:before{content:"\f29f"}.ion-loop:before{content:"\f201"}.ion-magnet:before{content:"\f2a0"}.ion-male:before{content:"\f2a1"}.ion-man:before{content:"\f202"}.ion-map:before{content:"\f203"}.ion-medkit:before{content:"\f2a2"}.ion-merge:before{content:"\f33f"}.ion-mic-a:before{content:"\f204"}.ion-mic-b:before{content:"\f205"}.ion-mic-c:before{content:"\f206"}.ion-minus:before{content:"\f209"}.ion-minus-circled:before{content:"\f207"}.ion-minus-round:before{content:"\f208"}.ion-model-s:before{content:"\f2c1"}.ion-monitor:before{content:"\f20a"}.ion-more:before{content:"\f20b"}.ion-mouse:before{content:"\f340"}.ion-music-note:before{content:"\f20c"}.ion-navicon:before{content:"\f20e"}.ion-navicon-round:before{content:"\f20d"}.ion-navigate:before{content:"\f2a3"}.ion-network:before{content:"\f341"}.ion-no-smoking:before{content:"\f2c2"}.ion-nuclear:before{content:"\f2a4"}.ion-outlet:before{content:"\f342"}.ion-paintbrush:before{content:"\f4d5"}.ion-paintbucket:before{content:"\f4d6"}.ion-paper-airplane:before{content:"\f2c3"}.ion-paperclip:before{content:"\f20f"}.ion-pause:before{content:"\f210"}.ion-person:before{content:"\f213"}.ion-person-add:before{content:"\f211"}.ion-person-stalker:before{content:"\f212"}.ion-pie-graph:before{content:"\f2a5"}.ion-pin:before{content:"\f2a6"}.ion-pinpoint:before{content:"\f2a7"}.ion-pizza:before{content:"\f2a8"}.ion-plane:before{content:"\f214"}.ion-planet:before{content:"\f343"}.ion-play:before{content:"\f215"}.ion-playstation:before{content:"\f30a"}.ion-plus:before{content:"\f218"}.ion-plus-circled:before{content:"\f216"}.ion-plus-round:before{content:"\f217"}.ion-podium:before{content:"\f344"}.ion-pound:before{content:"\f219"}.ion-power:before{content:"\f2a9"}.ion-pricetag:before{content:"\f2aa"}.ion-pricetags:before{content:"\f2ab"}.ion-printer:before{content:"\f21a"}.ion-pull-request:before{content:"\f345"}.ion-qr-scanner:before{content:"\f346"}.ion-quote:before{content:"\f347"}.ion-radio-waves:before{content:"\f2ac"}.ion-record:before{content:"\f21b"}.ion-refresh:before{content:"\f21c"}.ion-reply:before{content:"\f21e"}.ion-reply-all:before{content:"\f21d"}.ion-ribbon-a:before{content:"\f348"}.ion-ribbon-b:before{content:"\f349"}.ion-sad:before{content:"\f34a"}.ion-sad-outline:before{content:"\f4d7"}.ion-scissors:before{content:"\f34b"}.ion-search:before{content:"\f21f"}.ion-settings:before{content:"\f2ad"}.ion-share:before{content:"\f220"}.ion-shuffle:before{content:"\f221"}.ion-skip-backward:before{content:"\f222"}.ion-skip-forward:before{content:"\f223"}.ion-social-android:before{content:"\f225"}.ion-social-android-outline:before{content:"\f224"}.ion-social-angular:before{content:"\f4d9"}.ion-social-angular-outline:before{content:"\f4d8"}.ion-social-apple:before{content:"\f227"}.ion-social-apple-outline:before{content:"\f226"}.ion-social-bitcoin:before{content:"\f2af"}.ion-social-bitcoin-outline:before{content:"\f2ae"}.ion-social-buffer:before{content:"\f229"}.ion-social-buffer-outline:before{content:"\f228"}.ion-social-chrome:before{content:"\f4db"}.ion-social-chrome-outline:before{content:"\f4da"}.ion-social-codepen:before{content:"\f4dd"}.ion-social-codepen-outline:before{content:"\f4dc"}.ion-social-css3:before{content:"\f4df"}.ion-social-css3-outline:before{content:"\f4de"}.ion-social-designernews:before{content:"\f22b"}.ion-social-designernews-outline:before{content:"\f22a"}.ion-social-dribbble:before{content:"\f22d"}.ion-social-dribbble-outline:before{content:"\f22c"}.ion-social-dropbox:before{content:"\f22f"}.ion-social-dropbox-outline:before{content:"\f22e"}.ion-social-euro:before{content:"\f4e1"}.ion-social-euro-outline:before{content:"\f4e0"}.ion-social-facebook:before{content:"\f231"}.ion-social-facebook-outline:before{content:"\f230"}.ion-social-foursquare:before{content:"\f34d"}.ion-social-foursquare-outline:before{content:"\f34c"}.ion-social-freebsd-devil:before{content:"\f2c4"}.ion-social-github:before{content:"\f233"}.ion-social-github-outline:before{content:"\f232"}.ion-social-google:before{content:"\f34f"}.ion-social-google-outline:before{content:"\f34e"}.ion-social-googleplus:before{content:"\f235"}.ion-social-googleplus-outline:before{content:"\f234"}.ion-social-hackernews:before{content:"\f237"}.ion-social-hackernews-outline:before{content:"\f236"}.ion-social-html5:before{content:"\f4e3"}.ion-social-html5-outline:before{content:"\f4e2"}.ion-social-instagram:before{content:"\f351"}.ion-social-instagram-outline:before{content:"\f350"}.ion-social-javascript:before{content:"\f4e5"}.ion-social-javascript-outline:before{content:"\f4e4"}.ion-social-linkedin:before{content:"\f239"}.ion-social-linkedin-outline:before{content:"\f238"}.ion-social-markdown:before{content:"\f4e6"}.ion-social-nodejs:before{content:"\f4e7"}.ion-social-octocat:before{content:"\f4e8"}.ion-social-pinterest:before{content:"\f2b1"}.ion-social-pinterest-outline:before{content:"\f2b0"}.ion-social-python:before{content:"\f4e9"}.ion-social-reddit:before{content:"\f23b"}.ion-social-reddit-outline:before{content:"\f23a"}.ion-social-rss:before{content:"\f23d"}.ion-social-rss-outline:before{content:"\f23c"}.ion-social-sass:before{content:"\f4ea"}.ion-social-skype:before{content:"\f23f"}.ion-social-skype-outline:before{content:"\f23e"}.ion-social-snapchat:before{content:"\f4ec"}.ion-social-snapchat-outline:before{content:"\f4eb"}.ion-social-tumblr:before{content:"\f241"}.ion-social-tumblr-outline:before{content:"\f240"}.ion-social-tux:before{content:"\f2c5"}.ion-social-twitch:before{content:"\f4ee"}.ion-social-twitch-outline:before{content:"\f4ed"}.ion-social-twitter:before{content:"\f243"}.ion-social-twitter-outline:before{content:"\f242"}.ion-social-usd:before{content:"\f353"}.ion-social-usd-outline:before{content:"\f352"}.ion-social-vimeo:before{content:"\f245"}.ion-social-vimeo-outline:before{content:"\f244"}.ion-social-whatsapp:before{content:"\f4f0"}.ion-social-whatsapp-outline:before{content:"\f4ef"}.ion-social-windows:before{content:"\f247"}.ion-social-windows-outline:before{content:"\f246"}.ion-social-wordpress:before{content:"\f249"}.ion-social-wordpress-outline:before{content:"\f248"}.ion-social-yahoo:before{content:"\f24b"}.ion-social-yahoo-outline:before{content:"\f24a"}.ion-social-yen:before{content:"\f4f2"}.ion-social-yen-outline:before{content:"\f4f1"}.ion-social-youtube:before{content:"\f24d"}.ion-social-youtube-outline:before{content:"\f24c"}.ion-soup-can:before{content:"\f4f4"}.ion-soup-can-outline:before{content:"\f4f3"}.ion-speakerphone:before{content:"\f2b2"}.ion-speedometer:before{content:"\f2b3"}.ion-spoon:before{content:"\f2b4"}.ion-star:before{content:"\f24e"}.ion-stats-bars:before{content:"\f2b5"}.ion-steam:before{content:"\f30b"}.ion-stop:before{content:"\f24f"}.ion-thermometer:before{content:"\f2b6"}.ion-thumbsdown:before{content:"\f250"}.ion-thumbsup:before{content:"\f251"}.ion-toggle:before{content:"\f355"}.ion-toggle-filled:before{content:"\f354"}.ion-transgender:before{content:"\f4f5"}.ion-trash-a:before{content:"\f252"}.ion-trash-b:before{content:"\f253"}.ion-trophy:before{content:"\f356"}.ion-tshirt:before{content:"\f4f7"}.ion-tshirt-outline:before{content:"\f4f6"}.ion-umbrella:before{content:"\f2b7"}.ion-university:before{content:"\f357"}.ion-unlocked:before{content:"\f254"}.ion-upload:before{content:"\f255"}.ion-usb:before{content:"\f2b8"}.ion-videocamera:before{content:"\f256"}.ion-volume-high:before{content:"\f257"}.ion-volume-low:before{content:"\f258"}.ion-volume-medium:before{content:"\f259"}.ion-volume-mute:before{content:"\f25a"}.ion-wand:before{content:"\f358"}.ion-waterdrop:before{content:"\f25b"}.ion-wifi:before{content:"\f25c"}.ion-wineglass:before{content:"\f2b9"}.ion-woman:before{content:"\f25d"}.ion-wrench:before{content:"\f2ba"}.ion-xbox:before{content:"\f30c"} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.eot b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.eot new file mode 100644 index 00000000..92a3f20a Binary files /dev/null and b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.eot differ diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.svg b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.svg new file mode 100644 index 00000000..49fc8f36 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.svg @@ -0,0 +1,2230 @@ + + + + + +Created by FontForge 20120731 at Thu Dec 4 09:51:48 2014 + By Adam Bradley +Created by Adam Bradley with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.ttf b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.ttf new file mode 100644 index 00000000..c4e46324 Binary files /dev/null and b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.ttf differ diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.woff b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.woff new file mode 100644 index 00000000..5f3a14e0 Binary files /dev/null and b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/Ionicons/fonts/ionicons.woff differ diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/pace.min.js b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/pace.min.js new file mode 100644 index 00000000..234f9b3e --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/pace.min.js @@ -0,0 +1,2 @@ +/*! pace 1.0.2 */ +(function(){var 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=[].slice,Y={}.hasOwnProperty,Z=function(a,b){function c(){this.constructor=a}for(var d in b)Y.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},$=[].indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(b in this&&this[b]===a)return b;return-1};for(u={catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},C=function(){var a;return null!=(a="undefined"!=typeof performance&&null!==performance&&"function"==typeof performance.now?performance.now():void 0)?a:+new Date},E=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,t=window.cancelAnimationFrame||window.mozCancelAnimationFrame,null==E&&(E=function(a){return setTimeout(a,50)},t=function(a){return clearTimeout(a)}),G=function(a){var b,c;return b=C(),(c=function(){var d;return d=C()-b,d>=33?(b=C(),a(d,function(){return E(c)})):setTimeout(c,33-d)})()},F=function(){var a,b,c;return c=arguments[0],b=arguments[1],a=3<=arguments.length?X.call(arguments,2):[],"function"==typeof c[b]?c[b].apply(c,a):c[b]},v=function(){var a,b,c,d,e,f,g;for(b=arguments[0],d=2<=arguments.length?X.call(arguments,1):[],f=0,g=d.length;g>f;f++)if(c=d[f])for(a in c)Y.call(c,a)&&(e=c[a],null!=b[a]&&"object"==typeof b[a]&&null!=e&&"object"==typeof e?v(b[a],e):b[a]=e);return b},q=function(a){var b,c,d,e,f;for(c=b=0,e=0,f=a.length;f>e;e++)d=a[e],c+=Math.abs(d),b++;return c/b},x=function(a,b){var c,d,e;if(null==a&&(a="options"),null==b&&(b=!0),e=document.querySelector("[data-pace-"+a+"]")){if(c=e.getAttribute("data-pace-"+a),!b)return c;try{return JSON.parse(c)}catch(f){return d=f,"undefined"!=typeof console&&null!==console?console.error("Error parsing inline pace options",d):void 0}}},g=function(){function a(){}return a.prototype.on=function(a,b,c,d){var e;return null==d&&(d=!1),null==this.bindings&&(this.bindings={}),null==(e=this.bindings)[a]&&(e[a]=[]),this.bindings[a].push({handler:b,ctx:c,once:d})},a.prototype.once=function(a,b,c){return this.on(a,b,c,!0)},a.prototype.off=function(a,b){var c,d,e;if(null!=(null!=(d=this.bindings)?d[a]:void 0)){if(null==b)return delete this.bindings[a];for(c=0,e=[];cQ;Q++)K=U[Q],D[K]===!0&&(D[K]=u[K]);i=function(a){function b(){return V=b.__super__.constructor.apply(this,arguments)}return Z(b,a),b}(Error),b=function(){function a(){this.progress=0}return a.prototype.getElement=function(){var a;if(null==this.el){if(a=document.querySelector(D.target),!a)throw new i;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/pace-done/g,""),document.body.className+=" pace-running",this.el.innerHTML='
\n
\n
\n
',null!=a.firstChild?a.insertBefore(this.el,a.firstChild):a.appendChild(this.el)}return this.el},a.prototype.finish=function(){var a;return a=this.getElement(),a.className=a.className.replace("pace-active",""),a.className+=" pace-inactive",document.body.className=document.body.className.replace("pace-running",""),document.body.className+=" pace-done"},a.prototype.update=function(a){return this.progress=a,this.render()},a.prototype.destroy=function(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(a){i=a}return this.el=void 0},a.prototype.render=function(){var a,b,c,d,e,f,g;if(null==document.querySelector(D.target))return!1;for(a=this.getElement(),d="translate3d("+this.progress+"%, 0, 0)",g=["webkitTransform","msTransform","transform"],e=0,f=g.length;f>e;e++)b=g[e],a.children[0].style[b]=d;return(!this.lastRenderedProgress||this.lastRenderedProgress|0!==this.progress|0)&&(a.children[0].setAttribute("data-progress-text",""+(0|this.progress)+"%"),this.progress>=100?c="99":(c=this.progress<10?"0":"",c+=0|this.progress),a.children[0].setAttribute("data-progress",""+c)),this.lastRenderedProgress=this.progress},a.prototype.done=function(){return this.progress>=100},a}(),h=function(){function a(){this.bindings={}}return a.prototype.trigger=function(a,b){var c,d,e,f,g;if(null!=this.bindings[a]){for(f=this.bindings[a],g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(c.call(this,b));return g}},a.prototype.on=function(a,b){var c;return null==(c=this.bindings)[a]&&(c[a]=[]),this.bindings[a].push(b)},a}(),P=window.XMLHttpRequest,O=window.XDomainRequest,N=window.WebSocket,w=function(a,b){var c,d,e;e=[];for(d in b.prototype)try{e.push(null==a[d]&&"function"!=typeof b[d]?"function"==typeof Object.defineProperty?Object.defineProperty(a,d,{get:function(){return b.prototype[d]},configurable:!0,enumerable:!0}):a[d]=b.prototype[d]:void 0)}catch(f){c=f}return e},A=[],j.ignore=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("ignore"),c=b.apply(null,a),A.shift(),c},j.track=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("track"),c=b.apply(null,a),A.shift(),c},J=function(a){var b;if(null==a&&(a="GET"),"track"===A[0])return"force";if(!A.length&&D.ajax){if("socket"===a&&D.ajax.trackWebSockets)return!0;if(b=a.toUpperCase(),$.call(D.ajax.trackMethods,b)>=0)return!0}return!1},k=function(a){function b(){var a,c=this;b.__super__.constructor.apply(this,arguments),a=function(a){var b;return b=a.open,a.open=function(d,e){return J(d)&&c.trigger("request",{type:d,url:e,request:a}),b.apply(a,arguments)}},window.XMLHttpRequest=function(b){var c;return c=new P(b),a(c),c};try{w(window.XMLHttpRequest,P)}catch(d){}if(null!=O){window.XDomainRequest=function(){var b;return b=new O,a(b),b};try{w(window.XDomainRequest,O)}catch(d){}}if(null!=N&&D.ajax.trackWebSockets){window.WebSocket=function(a,b){var d;return d=null!=b?new N(a,b):new N(a),J("socket")&&c.trigger("request",{type:"socket",url:a,protocols:b,request:d}),d};try{w(window.WebSocket,N)}catch(d){}}}return Z(b,a),b}(h),R=null,y=function(){return null==R&&(R=new k),R},I=function(a){var b,c,d,e;for(e=D.ajax.ignoreURLs,c=0,d=e.length;d>c;c++)if(b=e[c],"string"==typeof b){if(-1!==a.indexOf(b))return!0}else if(b.test(a))return!0;return!1},y().on("request",function(b){var c,d,e,f,g;return f=b.type,e=b.request,g=b.url,I(g)?void 0:j.running||D.restartOnRequestAfter===!1&&"force"!==J(f)?void 0:(d=arguments,c=D.restartOnRequestAfter||0,"boolean"==typeof c&&(c=0),setTimeout(function(){var b,c,g,h,i,k;if(b="socket"===f?e.readyState<2:0<(h=e.readyState)&&4>h){for(j.restart(),i=j.sources,k=[],c=0,g=i.length;g>c;c++){if(K=i[c],K instanceof a){K.watch.apply(K,d);break}k.push(void 0)}return k}},c))}),a=function(){function a(){var a=this;this.elements=[],y().on("request",function(){return a.watch.apply(a,arguments)})}return a.prototype.watch=function(a){var b,c,d,e;return d=a.type,b=a.request,e=a.url,I(e)?void 0:(c="socket"===d?new n(b):new o(b),this.elements.push(c))},a}(),o=function(){function a(a){var b,c,d,e,f,g,h=this;if(this.progress=0,null!=window.ProgressEvent)for(c=null,a.addEventListener("progress",function(a){return h.progress=a.lengthComputable?100*a.loaded/a.total:h.progress+(100-h.progress)/2},!1),g=["load","abort","timeout","error"],d=0,e=g.length;e>d;d++)b=g[d],a.addEventListener(b,function(){return h.progress=100},!1);else f=a.onreadystatechange,a.onreadystatechange=function(){var b;return 0===(b=a.readyState)||4===b?h.progress=100:3===a.readyState&&(h.progress=50),"function"==typeof f?f.apply(null,arguments):void 0}}return a}(),n=function(){function a(a){var b,c,d,e,f=this;for(this.progress=0,e=["error","open"],c=0,d=e.length;d>c;c++)b=e[c],a.addEventListener(b,function(){return f.progress=100},!1)}return a}(),d=function(){function a(a){var b,c,d,f;for(null==a&&(a={}),this.elements=[],null==a.selectors&&(a.selectors=[]),f=a.selectors,c=0,d=f.length;d>c;c++)b=f[c],this.elements.push(new e(b))}return a}(),e=function(){function a(a){this.selector=a,this.progress=0,this.check()}return a.prototype.check=function(){var a=this;return document.querySelector(this.selector)?this.done():setTimeout(function(){return a.check()},D.elements.checkInterval)},a.prototype.done=function(){return this.progress=100},a}(),c=function(){function a(){var a,b,c=this;this.progress=null!=(b=this.states[document.readyState])?b:100,a=document.onreadystatechange,document.onreadystatechange=function(){return null!=c.states[document.readyState]&&(c.progress=c.states[document.readyState]),"function"==typeof a?a.apply(null,arguments):void 0}}return a.prototype.states={loading:0,interactive:50,complete:100},a}(),f=function(){function a(){var a,b,c,d,e,f=this;this.progress=0,a=0,e=[],d=0,c=C(),b=setInterval(function(){var g;return g=C()-c-50,c=C(),e.push(g),e.length>D.eventLag.sampleCount&&e.shift(),a=q(e),++d>=D.eventLag.minSamples&&a=100&&(this.done=!0),b===this.last?this.sinceLastUpdate+=a:(this.sinceLastUpdate&&(this.rate=(b-this.last)/this.sinceLastUpdate),this.catchup=(b-this.progress)/D.catchupTime,this.sinceLastUpdate=0,this.last=b),b>this.progress&&(this.progress+=this.catchup*a),c=1-Math.pow(this.progress/100,D.easeFactor),this.progress+=c*this.rate*a,this.progress=Math.min(this.lastProgress+D.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress},a}(),L=null,H=null,r=null,M=null,p=null,s=null,j.running=!1,z=function(){return D.restartOnPushState?j.restart():void 0},null!=window.history.pushState&&(T=window.history.pushState,window.history.pushState=function(){return z(),T.apply(window.history,arguments)}),null!=window.history.replaceState&&(W=window.history.replaceState,window.history.replaceState=function(){return z(),W.apply(window.history,arguments)}),l={ajax:a,elements:d,document:c,eventLag:f},(B=function(){var a,c,d,e,f,g,h,i;for(j.sources=L=[],g=["ajax","elements","document","eventLag"],c=0,e=g.length;e>c;c++)a=g[c],D[a]!==!1&&L.push(new l[a](D[a]));for(i=null!=(h=D.extraSources)?h:[],d=0,f=i.length;f>d;d++)K=i[d],L.push(new K(D));return j.bar=r=new b,H=[],M=new m})(),j.stop=function(){return j.trigger("stop"),j.running=!1,r.destroy(),s=!0,null!=p&&("function"==typeof t&&t(p),p=null),B()},j.restart=function(){return j.trigger("restart"),j.stop(),j.start()},j.go=function(){var a;return j.running=!0,r.render(),a=C(),s=!1,p=G(function(b,c){var d,e,f,g,h,i,k,l,n,o,p,q,t,u,v,w;for(l=100-r.progress,e=p=0,f=!0,i=q=0,u=L.length;u>q;i=++q)for(K=L[i],o=null!=H[i]?H[i]:H[i]=[],h=null!=(w=K.elements)?w:[K],k=t=0,v=h.length;v>t;k=++t)g=h[k],n=null!=o[k]?o[k]:o[k]=new m(g),f&=n.done,n.done||(e++,p+=n.tick(b));return d=p/e,r.update(M.tick(b,d)),r.done()||f||s?(r.update(100),j.trigger("done"),setTimeout(function(){return r.finish(),j.running=!1,j.trigger("hide")},Math.max(D.ghostTime,Math.max(D.minTime-(C()-a),0)))):c()})},j.start=function(a){v(D,a),j.running=!0;try{r.render()}catch(b){i=b}return document.querySelector(".pace")?(j.trigger("start"),j.go()):setTimeout(j.start,50)},"function"==typeof define&&define.amd?define(["pace"],function(){return j}):"object"==typeof exports?module.exports=j:D.startOnPageLoad&&j.start()}).call(this); \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/themes/blue/pace-theme-flash.css b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/themes/blue/pace-theme-flash.css new file mode 100644 index 00000000..d9bca466 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/PACE/themes/blue/pace-theme-flash.css @@ -0,0 +1,77 @@ +/* This is a compiled file, you should be editing the file in the templates directory */ +.pace { + -webkit-pointer-events: none; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.pace-inactive { + display: none; +} + +.pace .pace-progress { + background: #2299dd; + position: fixed; + z-index: 2000; + top: 0; + right: 100%; + width: 100%; + height: 2px; +} + +.pace .pace-progress-inner { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px #2299dd, 0 0 5px #2299dd; + opacity: 1.0; + -webkit-transform: rotate(3deg) translate(0px, -4px); + -moz-transform: rotate(3deg) translate(0px, -4px); + -ms-transform: rotate(3deg) translate(0px, -4px); + -o-transform: rotate(3deg) translate(0px, -4px); + transform: rotate(3deg) translate(0px, -4px); +} + +.pace .pace-activity { + display: block; + position: fixed; + z-index: 2000; + top: 15px; + right: 15px; + width: 14px; + height: 14px; + border: solid 2px transparent; + border-top-color: #2299dd; + border-left-color: #2299dd; + border-radius: 10px; + -webkit-animation: pace-spinner 400ms linear infinite; + -moz-animation: pace-spinner 400ms linear infinite; + -ms-animation: pace-spinner 400ms linear infinite; + -o-animation: pace-spinner 400ms linear infinite; + animation: pace-spinner 400ms linear infinite; +} + +@-webkit-keyframes pace-spinner { + 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } +} +@-moz-keyframes pace-spinner { + 0% { -moz-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -moz-transform: rotate(360deg); transform: rotate(360deg); } +} +@-o-keyframes pace-spinner { + 0% { -o-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -o-transform: rotate(360deg); transform: rotate(360deg); } +} +@-ms-keyframes pace-spinner { + 0% { -ms-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -ms-transform: rotate(360deg); transform: rotate(360deg); } +} +@keyframes pace-spinner { + 0% { transform: rotate(0deg); transform: rotate(0deg); } + 100% { transform: rotate(360deg); transform: rotate(360deg); } +} diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css new file mode 100644 index 00000000..86f4b775 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.css @@ -0,0 +1,269 @@ +.daterangepicker { + position: absolute; + color: inherit; + background-color: #fff; + border-radius: 4px; + width: 278px; + padding: 4px; + margin-top: 1px; + top: 100px; + left: 20px; + /* Calendars */ } + .daterangepicker:before, .daterangepicker:after { + position: absolute; + display: inline-block; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; } + .daterangepicker:before { + top: -7px; + border-right: 7px solid transparent; + border-left: 7px solid transparent; + border-bottom: 7px solid #ccc; } + .daterangepicker:after { + top: -6px; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; } + .daterangepicker.opensleft:before { + right: 9px; } + .daterangepicker.opensleft:after { + right: 10px; } + .daterangepicker.openscenter:before { + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; } + .daterangepicker.openscenter:after { + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; } + .daterangepicker.opensright:before { + left: 9px; } + .daterangepicker.opensright:after { + left: 10px; } + .daterangepicker.dropup { + margin-top: -5px; } + .daterangepicker.dropup:before { + top: initial; + bottom: -7px; + border-bottom: initial; + border-top: 7px solid #ccc; } + .daterangepicker.dropup:after { + top: initial; + bottom: -6px; + border-bottom: initial; + border-top: 6px solid #fff; } + .daterangepicker.dropdown-menu { + max-width: none; + z-index: 3001; } + .daterangepicker.single .ranges, .daterangepicker.single .calendar { + float: none; } + .daterangepicker.show-calendar .calendar { + display: block; } + .daterangepicker .calendar { + display: none; + max-width: 270px; + margin: 4px; } + .daterangepicker .calendar.single .calendar-table { + border: none; } + .daterangepicker .calendar th, .daterangepicker .calendar td { + white-space: nowrap; + text-align: center; + min-width: 32px; } + .daterangepicker .calendar-table { + border: 1px solid #fff; + padding: 4px; + border-radius: 4px; + background-color: #fff; } + .daterangepicker table { + width: 100%; + margin: 0; } + .daterangepicker td, .daterangepicker th { + text-align: center; + width: 20px; + height: 20px; + border-radius: 4px; + border: 1px solid transparent; + white-space: nowrap; + cursor: pointer; } + .daterangepicker td.available:hover, .daterangepicker th.available:hover { + background-color: #eee; + border-color: transparent; + color: inherit; } + .daterangepicker td.week, .daterangepicker th.week { + font-size: 80%; + color: #ccc; } + .daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { + background-color: #fff; + border-color: transparent; + color: #999; } + .daterangepicker td.in-range { + background-color: #ebf4f8; + border-color: transparent; + color: #000; + border-radius: 0; } + .daterangepicker td.start-date { + border-radius: 4px 0 0 4px; } + .daterangepicker td.end-date { + border-radius: 0 4px 4px 0; } + .daterangepicker td.start-date.end-date { + border-radius: 4px; } + .daterangepicker td.active, .daterangepicker td.active:hover { + background-color: #357ebd; + border-color: transparent; + color: #fff; } + .daterangepicker th.month { + width: auto; } + .daterangepicker td.disabled, .daterangepicker option.disabled { + color: #999; + cursor: not-allowed; + text-decoration: line-through; } + .daterangepicker select.monthselect, .daterangepicker select.yearselect { + font-size: 12px; + padding: 1px; + height: auto; + margin: 0; + cursor: default; } + .daterangepicker select.monthselect { + margin-right: 2%; + width: 56%; } + .daterangepicker select.yearselect { + width: 40%; } + .daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { + width: 50px; + margin-bottom: 0; } + .daterangepicker .input-mini { + border: 1px solid #ccc; + border-radius: 4px; + color: #555; + height: 30px; + line-height: 30px; + display: block; + vertical-align: middle; + margin: 0 0 5px 0; + padding: 0 6px 0 28px; + width: 100%; } + .daterangepicker .input-mini.active { + border: 1px solid #08c; + border-radius: 4px; } + .daterangepicker .daterangepicker_input { + position: relative; } + .daterangepicker .daterangepicker_input i { + position: absolute; + left: 8px; + top: 8px; } + .daterangepicker.rtl .input-mini { + padding-right: 28px; + padding-left: 6px; } + .daterangepicker.rtl .daterangepicker_input i { + left: auto; + right: 8px; } + .daterangepicker .calendar-time { + text-align: center; + margin: 5px auto; + line-height: 30px; + position: relative; + padding-left: 28px; } + .daterangepicker .calendar-time select.disabled { + color: #ccc; + cursor: not-allowed; } + +.ranges { + font-size: 11px; + float: none; + margin: 4px; + text-align: left; } + .ranges ul { + list-style: none; + margin: 0 auto; + padding: 0; + width: 100%; } + .ranges li { + font-size: 13px; + background-color: #f5f5f5; + border: 1px solid #f5f5f5; + border-radius: 4px; + color: #08c; + padding: 3px 12px; + margin-bottom: 8px; + cursor: pointer; } + .ranges li:hover { + background-color: #08c; + border: 1px solid #08c; + color: #fff; } + .ranges li.active { + background-color: #08c; + border: 1px solid #08c; + color: #fff; } + +/* Larger Screen Styling */ +@media (min-width: 564px) { + .daterangepicker { + width: auto; } + .daterangepicker .ranges ul { + width: 160px; } + .daterangepicker.single .ranges ul { + width: 100%; } + .daterangepicker.single .calendar.left { + clear: none; } + .daterangepicker.single.ltr .ranges, .daterangepicker.single.ltr .calendar { + float: left; } + .daterangepicker.single.rtl .ranges, .daterangepicker.single.rtl .calendar { + float: right; } + .daterangepicker.ltr { + direction: ltr; + text-align: left; } + .daterangepicker.ltr .calendar.left { + clear: left; + margin-right: 0; } + .daterangepicker.ltr .calendar.left .calendar-table { + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } + .daterangepicker.ltr .calendar.right { + margin-left: 0; } + .daterangepicker.ltr .calendar.right .calendar-table { + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } + .daterangepicker.ltr .left .daterangepicker_input { + padding-right: 12px; } + .daterangepicker.ltr .calendar.left .calendar-table { + padding-right: 12px; } + .daterangepicker.ltr .ranges, .daterangepicker.ltr .calendar { + float: left; } + .daterangepicker.rtl { + direction: rtl; + text-align: right; } + .daterangepicker.rtl .calendar.left { + clear: right; + margin-left: 0; } + .daterangepicker.rtl .calendar.left .calendar-table { + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } + .daterangepicker.rtl .calendar.right { + margin-right: 0; } + .daterangepicker.rtl .calendar.right .calendar-table { + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } + .daterangepicker.rtl .left .daterangepicker_input { + padding-left: 12px; } + .daterangepicker.rtl .calendar.left .calendar-table { + padding-left: 12px; } + .daterangepicker.rtl .ranges, .daterangepicker.rtl .calendar { + text-align: right; + float: right; } } +@media (min-width: 730px) { + .daterangepicker .ranges { + width: auto; } + .daterangepicker.ltr .ranges { + float: left; } + .daterangepicker.rtl .ranges { + float: right; } + .daterangepicker .calendar.left { + clear: none !important; } } diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js new file mode 100644 index 00000000..079cde61 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap-daterangepicker/daterangepicker.js @@ -0,0 +1,1653 @@ +/** +* @version: 2.1.27 +* @author: Dan Grossman http://www.dangrossman.info/ +* @copyright: Copyright (c) 2012-2017 Dan Grossman. All rights reserved. +* @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php +* @website: http://www.daterangepicker.com/ +*/ +// Follow the UMD template https://github.com/umdjs/umd/blob/master/templates/returnExportsGlobal.js +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Make globaly available as well + define(['moment', 'jquery'], function (moment, jquery) { + if (!jquery.fn) jquery.fn = {}; // webpack server rendering + return factory(moment, jquery); + }); + } else if (typeof module === 'object' && module.exports) { + // Node / Browserify + //isomorphic issue + var jQuery = (typeof window != 'undefined') ? window.jQuery : undefined; + if (!jQuery) { + jQuery = require('jquery'); + if (!jQuery.fn) jQuery.fn = {}; + } + var moment = (typeof window != 'undefined' && typeof window.moment != 'undefined') ? window.moment : require('moment'); + module.exports = factory(moment, jQuery); + } else { + // Browser globals + root.daterangepicker = factory(root.moment, root.jQuery); + } +}(this, function(moment, $) { + var DateRangePicker = function(element, options, cb) { + + //default settings for options + this.parentEl = 'body'; + this.element = $(element); + this.startDate = moment().startOf('day'); + this.endDate = moment().endOf('day'); + this.minDate = false; + this.maxDate = false; + this.dateLimit = false; + this.autoApply = false; + this.singleDatePicker = false; + this.showDropdowns = false; + this.showWeekNumbers = false; + this.showISOWeekNumbers = false; + this.showCustomRangeLabel = true; + this.timePicker = false; + this.timePicker24Hour = false; + this.timePickerIncrement = 1; + this.timePickerSeconds = false; + this.linkedCalendars = true; + this.autoUpdateInput = true; + this.alwaysShowCalendars = false; + this.ranges = {}; + + this.opens = 'right'; + if (this.element.hasClass('pull-right')) + this.opens = 'left'; + + this.drops = 'down'; + if (this.element.hasClass('dropup')) + this.drops = 'up'; + + this.buttonClasses = 'btn btn-sm'; + this.applyClass = 'btn-success'; + this.cancelClass = 'btn-default'; + + this.locale = { + direction: 'ltr', + format: moment.localeData().longDateFormat('L'), + separator: ' - ', + applyLabel: 'Apply', + cancelLabel: 'Cancel', + weekLabel: 'W', + customRangeLabel: 'Custom Range', + daysOfWeek: moment.weekdaysMin(), + monthNames: moment.monthsShort(), + firstDay: moment.localeData().firstDayOfWeek() + }; + + this.callback = function() { }; + + //some state information + this.isShowing = false; + this.leftCalendar = {}; + this.rightCalendar = {}; + + //custom options from user + if (typeof options !== 'object' || options === null) + options = {}; + + //allow setting options with data attributes + //data-api options will be overwritten with custom javascript options + options = $.extend(this.element.data(), options); + + //html template for the picker UI + if (typeof options.template !== 'string' && !(options.template instanceof $)) + options.template = ''; + + this.parentEl = (options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl); + this.container = $(options.template).appendTo(this.parentEl); + + // + // handle all the possible options overriding defaults + // + + if (typeof options.locale === 'object') { + + if (typeof options.locale.direction === 'string') + this.locale.direction = options.locale.direction; + + if (typeof options.locale.format === 'string') + this.locale.format = options.locale.format; + + if (typeof options.locale.separator === 'string') + this.locale.separator = options.locale.separator; + + if (typeof options.locale.daysOfWeek === 'object') + this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); + + if (typeof options.locale.monthNames === 'object') + this.locale.monthNames = options.locale.monthNames.slice(); + + if (typeof options.locale.firstDay === 'number') + this.locale.firstDay = options.locale.firstDay; + + if (typeof options.locale.applyLabel === 'string') + this.locale.applyLabel = options.locale.applyLabel; + + if (typeof options.locale.cancelLabel === 'string') + this.locale.cancelLabel = options.locale.cancelLabel; + + if (typeof options.locale.weekLabel === 'string') + this.locale.weekLabel = options.locale.weekLabel; + + if (typeof options.locale.customRangeLabel === 'string'){ + //Support unicode chars in the custom range name. + var elem = document.createElement('textarea'); + elem.innerHTML = options.locale.customRangeLabel; + var rangeHtml = elem.value; + this.locale.customRangeLabel = rangeHtml; + } + } + this.container.addClass(this.locale.direction); + + if (typeof options.startDate === 'string') + this.startDate = moment(options.startDate, this.locale.format); + + if (typeof options.endDate === 'string') + this.endDate = moment(options.endDate, this.locale.format); + + if (typeof options.minDate === 'string') + this.minDate = moment(options.minDate, this.locale.format); + + if (typeof options.maxDate === 'string') + this.maxDate = moment(options.maxDate, this.locale.format); + + if (typeof options.startDate === 'object') + this.startDate = moment(options.startDate); + + if (typeof options.endDate === 'object') + this.endDate = moment(options.endDate); + + if (typeof options.minDate === 'object') + this.minDate = moment(options.minDate); + + if (typeof options.maxDate === 'object') + this.maxDate = moment(options.maxDate); + + // sanity check for bad options + if (this.minDate && this.startDate.isBefore(this.minDate)) + this.startDate = this.minDate.clone(); + + // sanity check for bad options + if (this.maxDate && this.endDate.isAfter(this.maxDate)) + this.endDate = this.maxDate.clone(); + + if (typeof options.applyClass === 'string') + this.applyClass = options.applyClass; + + if (typeof options.cancelClass === 'string') + this.cancelClass = options.cancelClass; + + if (typeof options.dateLimit === 'object') + this.dateLimit = options.dateLimit; + + if (typeof options.opens === 'string') + this.opens = options.opens; + + if (typeof options.drops === 'string') + this.drops = options.drops; + + if (typeof options.showWeekNumbers === 'boolean') + this.showWeekNumbers = options.showWeekNumbers; + + if (typeof options.showISOWeekNumbers === 'boolean') + this.showISOWeekNumbers = options.showISOWeekNumbers; + + if (typeof options.buttonClasses === 'string') + this.buttonClasses = options.buttonClasses; + + if (typeof options.buttonClasses === 'object') + this.buttonClasses = options.buttonClasses.join(' '); + + if (typeof options.showDropdowns === 'boolean') + this.showDropdowns = options.showDropdowns; + + if (typeof options.showCustomRangeLabel === 'boolean') + this.showCustomRangeLabel = options.showCustomRangeLabel; + + if (typeof options.singleDatePicker === 'boolean') { + this.singleDatePicker = options.singleDatePicker; + if (this.singleDatePicker) + this.endDate = this.startDate.clone(); + } + + if (typeof options.timePicker === 'boolean') + this.timePicker = options.timePicker; + + if (typeof options.timePickerSeconds === 'boolean') + this.timePickerSeconds = options.timePickerSeconds; + + if (typeof options.timePickerIncrement === 'number') + this.timePickerIncrement = options.timePickerIncrement; + + if (typeof options.timePicker24Hour === 'boolean') + this.timePicker24Hour = options.timePicker24Hour; + + if (typeof options.autoApply === 'boolean') + this.autoApply = options.autoApply; + + if (typeof options.autoUpdateInput === 'boolean') + this.autoUpdateInput = options.autoUpdateInput; + + if (typeof options.linkedCalendars === 'boolean') + this.linkedCalendars = options.linkedCalendars; + + if (typeof options.isInvalidDate === 'function') + this.isInvalidDate = options.isInvalidDate; + + if (typeof options.isCustomDate === 'function') + this.isCustomDate = options.isCustomDate; + + if (typeof options.alwaysShowCalendars === 'boolean') + this.alwaysShowCalendars = options.alwaysShowCalendars; + + // update day names order to firstDay + if (this.locale.firstDay != 0) { + var iterator = this.locale.firstDay; + while (iterator > 0) { + this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); + iterator--; + } + } + + var start, end, range; + + //if no start/end dates set, check if an input element contains initial values + if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') { + if ($(this.element).is('input[type=text]')) { + var val = $(this.element).val(), + split = val.split(this.locale.separator); + + start = end = null; + + if (split.length == 2) { + start = moment(split[0], this.locale.format); + end = moment(split[1], this.locale.format); + } else if (this.singleDatePicker && val !== "") { + start = moment(val, this.locale.format); + end = moment(val, this.locale.format); + } + if (start !== null && end !== null) { + this.setStartDate(start); + this.setEndDate(end); + } + } + } + + if (typeof options.ranges === 'object') { + for (range in options.ranges) { + + if (typeof options.ranges[range][0] === 'string') + start = moment(options.ranges[range][0], this.locale.format); + else + start = moment(options.ranges[range][0]); + + if (typeof options.ranges[range][1] === 'string') + end = moment(options.ranges[range][1], this.locale.format); + else + end = moment(options.ranges[range][1]); + + // If the start or end date exceed those allowed by the minDate or dateLimit + // options, shorten the range to the allowable period. + if (this.minDate && start.isBefore(this.minDate)) + start = this.minDate.clone(); + + var maxDate = this.maxDate; + if (this.dateLimit && maxDate && start.clone().add(this.dateLimit).isAfter(maxDate)) + maxDate = start.clone().add(this.dateLimit); + if (maxDate && end.isAfter(maxDate)) + end = maxDate.clone(); + + // If the end of the range is before the minimum or the start of the range is + // after the maximum, don't display this range option at all. + if ((this.minDate && end.isBefore(this.minDate, this.timepicker ? 'minute' : 'day')) + || (maxDate && start.isAfter(maxDate, this.timepicker ? 'minute' : 'day'))) + continue; + + //Support unicode chars in the range names. + var elem = document.createElement('textarea'); + elem.innerHTML = range; + var rangeHtml = elem.value; + + this.ranges[rangeHtml] = [start, end]; + } + + var list = '
    '; + for (range in this.ranges) { + list += '
  • ' + range + '
  • '; + } + if (this.showCustomRangeLabel) { + list += '
  • ' + this.locale.customRangeLabel + '
  • '; + } + list += '
'; + this.container.find('.ranges').prepend(list); + } + + if (typeof cb === 'function') { + this.callback = cb; + } + + if (!this.timePicker) { + this.startDate = this.startDate.startOf('day'); + this.endDate = this.endDate.endOf('day'); + this.container.find('.calendar-time').hide(); + } + + //can't be used together for now + if (this.timePicker && this.autoApply) + this.autoApply = false; + + if (this.autoApply && typeof options.ranges !== 'object') { + this.container.find('.ranges').hide(); + } else if (this.autoApply) { + this.container.find('.applyBtn, .cancelBtn').addClass('hide'); + } + + if (this.singleDatePicker) { + this.container.addClass('single'); + this.container.find('.calendar.left').addClass('single'); + this.container.find('.calendar.left').show(); + this.container.find('.calendar.right').hide(); + this.container.find('.daterangepicker_input input, .daterangepicker_input > i').hide(); + if (this.timePicker) { + this.container.find('.ranges ul').hide(); + } else { + this.container.find('.ranges').hide(); + } + } + + if ((typeof options.ranges === 'undefined' && !this.singleDatePicker) || this.alwaysShowCalendars) { + this.container.addClass('show-calendar'); + } + + this.container.addClass('opens' + this.opens); + + //swap the position of the predefined ranges if opens right + if (typeof options.ranges !== 'undefined' && this.opens == 'right') { + this.container.find('.ranges').prependTo( this.container.find('.calendar.left').parent() ); + } + + //apply CSS classes and labels to buttons + this.container.find('.applyBtn, .cancelBtn').addClass(this.buttonClasses); + if (this.applyClass.length) + this.container.find('.applyBtn').addClass(this.applyClass); + if (this.cancelClass.length) + this.container.find('.cancelBtn').addClass(this.cancelClass); + this.container.find('.applyBtn').html(this.locale.applyLabel); + this.container.find('.cancelBtn').html(this.locale.cancelLabel); + + // + // event listeners + // + + this.container.find('.calendar') + .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this)) + .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this)) + .on('mousedown.daterangepicker', 'td.available', $.proxy(this.clickDate, this)) + .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this)) + .on('mouseleave.daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this)) + .on('change.daterangepicker', 'select.yearselect', $.proxy(this.monthOrYearChanged, this)) + .on('change.daterangepicker', 'select.monthselect', $.proxy(this.monthOrYearChanged, this)) + .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.timeChanged, this)) + .on('click.daterangepicker', '.daterangepicker_input input', $.proxy(this.showCalendars, this)) + .on('focus.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsFocused, this)) + .on('blur.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsBlurred, this)) + .on('change.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsChanged, this)) + .on('keydown.daterangepicker', '.daterangepicker_input input', $.proxy(this.formInputsKeydown, this)); + + this.container.find('.ranges') + .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this)) + .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this)) + .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this)) + .on('mouseenter.daterangepicker', 'li', $.proxy(this.hoverRange, this)) + .on('mouseleave.daterangepicker', 'li', $.proxy(this.updateFormInputs, this)); + + if (this.element.is('input') || this.element.is('button')) { + this.element.on({ + 'click.daterangepicker': $.proxy(this.show, this), + 'focus.daterangepicker': $.proxy(this.show, this), + 'keyup.daterangepicker': $.proxy(this.elementChanged, this), + 'keydown.daterangepicker': $.proxy(this.keydown, this) //IE 11 compatibility + }); + } else { + this.element.on('click.daterangepicker', $.proxy(this.toggle, this)); + this.element.on('keydown.daterangepicker', $.proxy(this.toggle, this)); + } + + // + // if attached to a text input, set the initial value + // + + if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); + this.element.trigger('change'); + } else if (this.element.is('input') && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format)); + this.element.trigger('change'); + } + + }; + + DateRangePicker.prototype = { + + constructor: DateRangePicker, + + setStartDate: function(startDate) { + if (typeof startDate === 'string') + this.startDate = moment(startDate, this.locale.format); + + if (typeof startDate === 'object') + this.startDate = moment(startDate); + + if (!this.timePicker) + this.startDate = this.startDate.startOf('day'); + + if (this.timePicker && this.timePickerIncrement) + this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); + + if (this.minDate && this.startDate.isBefore(this.minDate)) { + this.startDate = this.minDate.clone(); + if (this.timePicker && this.timePickerIncrement) + this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); + } + + if (this.maxDate && this.startDate.isAfter(this.maxDate)) { + this.startDate = this.maxDate.clone(); + if (this.timePicker && this.timePickerIncrement) + this.startDate.minute(Math.floor(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); + } + + if (!this.isShowing) + this.updateElement(); + + this.updateMonthsInView(); + }, + + setEndDate: function(endDate) { + if (typeof endDate === 'string') + this.endDate = moment(endDate, this.locale.format); + + if (typeof endDate === 'object') + this.endDate = moment(endDate); + + if (!this.timePicker) + this.endDate = this.endDate.add(1,'d').startOf('day').subtract(1,'second'); + + if (this.timePicker && this.timePickerIncrement) + this.endDate.minute(Math.round(this.endDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); + + if (this.endDate.isBefore(this.startDate)) + this.endDate = this.startDate.clone(); + + if (this.maxDate && this.endDate.isAfter(this.maxDate)) + this.endDate = this.maxDate.clone(); + + if (this.dateLimit && this.startDate.clone().add(this.dateLimit).isBefore(this.endDate)) + this.endDate = this.startDate.clone().add(this.dateLimit); + + this.previousRightTime = this.endDate.clone(); + + if (!this.isShowing) + this.updateElement(); + + this.updateMonthsInView(); + }, + + isInvalidDate: function() { + return false; + }, + + isCustomDate: function() { + return false; + }, + + updateView: function() { + if (this.timePicker) { + this.renderTimePicker('left'); + this.renderTimePicker('right'); + if (!this.endDate) { + this.container.find('.right .calendar-time select').attr('disabled', 'disabled').addClass('disabled'); + } else { + this.container.find('.right .calendar-time select').removeAttr('disabled').removeClass('disabled'); + } + } + if (this.endDate) { + this.container.find('input[name="daterangepicker_end"]').removeClass('active'); + this.container.find('input[name="daterangepicker_start"]').addClass('active'); + } else { + this.container.find('input[name="daterangepicker_end"]').addClass('active'); + this.container.find('input[name="daterangepicker_start"]').removeClass('active'); + } + this.updateMonthsInView(); + this.updateCalendars(); + this.updateFormInputs(); + }, + + updateMonthsInView: function() { + if (this.endDate) { + + //if both dates are visible already, do nothing + if (!this.singleDatePicker && this.leftCalendar.month && this.rightCalendar.month && + (this.startDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.startDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) + && + (this.endDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.endDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) + ) { + return; + } + + this.leftCalendar.month = this.startDate.clone().date(2); + if (!this.linkedCalendars && (this.endDate.month() != this.startDate.month() || this.endDate.year() != this.startDate.year())) { + this.rightCalendar.month = this.endDate.clone().date(2); + } else { + this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); + } + + } else { + if (this.leftCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM') && this.rightCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM')) { + this.leftCalendar.month = this.startDate.clone().date(2); + this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); + } + } + if (this.maxDate && this.linkedCalendars && !this.singleDatePicker && this.rightCalendar.month > this.maxDate) { + this.rightCalendar.month = this.maxDate.clone().date(2); + this.leftCalendar.month = this.maxDate.clone().date(2).subtract(1, 'month'); + } + }, + + updateCalendars: function() { + + if (this.timePicker) { + var hour, minute, second; + if (this.endDate) { + hour = parseInt(this.container.find('.left .hourselect').val(), 10); + minute = parseInt(this.container.find('.left .minuteselect').val(), 10); + second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0; + if (!this.timePicker24Hour) { + var ampm = this.container.find('.left .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + } else { + hour = parseInt(this.container.find('.right .hourselect').val(), 10); + minute = parseInt(this.container.find('.right .minuteselect').val(), 10); + second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0; + if (!this.timePicker24Hour) { + var ampm = this.container.find('.right .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + } + this.leftCalendar.month.hour(hour).minute(minute).second(second); + this.rightCalendar.month.hour(hour).minute(minute).second(second); + } + + this.renderCalendar('left'); + this.renderCalendar('right'); + + //highlight any predefined range matching the current start and end dates + this.container.find('.ranges li').removeClass('active'); + if (this.endDate == null) return; + + this.calculateChosenLabel(); + }, + + renderCalendar: function(side) { + + // + // Build the matrix of dates that will populate the calendar + // + + var calendar = side == 'left' ? this.leftCalendar : this.rightCalendar; + var month = calendar.month.month(); + var year = calendar.month.year(); + var hour = calendar.month.hour(); + var minute = calendar.month.minute(); + var second = calendar.month.second(); + var daysInMonth = moment([year, month]).daysInMonth(); + var firstDay = moment([year, month, 1]); + var lastDay = moment([year, month, daysInMonth]); + var lastMonth = moment(firstDay).subtract(1, 'month').month(); + var lastYear = moment(firstDay).subtract(1, 'month').year(); + var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth(); + var dayOfWeek = firstDay.day(); + + //initialize a 6 rows x 7 columns array for the calendar + var calendar = []; + calendar.firstDay = firstDay; + calendar.lastDay = lastDay; + + for (var i = 0; i < 6; i++) { + calendar[i] = []; + } + + //populate the calendar with date objects + var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1; + if (startDay > daysInLastMonth) + startDay -= 7; + + if (dayOfWeek == this.locale.firstDay) + startDay = daysInLastMonth - 6; + + var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]); + + var col, row; + for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) { + if (i > 0 && col % 7 === 0) { + col = 0; + row++; + } + calendar[row][col] = curDate.clone().hour(hour).minute(minute).second(second); + curDate.hour(12); + + if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') { + calendar[row][col] = this.minDate.clone(); + } + + if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') { + calendar[row][col] = this.maxDate.clone(); + } + + } + + //make the calendar object available to hoverDate/clickDate + if (side == 'left') { + this.leftCalendar.calendar = calendar; + } else { + this.rightCalendar.calendar = calendar; + } + + // + // Display the calendar + // + + var minDate = side == 'left' ? this.minDate : this.startDate; + var maxDate = this.maxDate; + var selected = side == 'left' ? this.startDate : this.endDate; + var arrow = this.locale.direction == 'ltr' ? {left: 'chevron-left', right: 'chevron-right'} : {left: 'chevron-right', right: 'chevron-left'}; + + var html = ''; + html += ''; + html += ''; + + // add empty cell for week number + if (this.showWeekNumbers || this.showISOWeekNumbers) + html += ''; + + if ((!minDate || minDate.isBefore(calendar.firstDay)) && (!this.linkedCalendars || side == 'left')) { + html += ''; + } else { + html += ''; + } + + var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY"); + + if (this.showDropdowns) { + var currentMonth = calendar[1][1].month(); + var currentYear = calendar[1][1].year(); + var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); + var minYear = (minDate && minDate.year()) || (currentYear - 50); + var inMinYear = currentYear == minYear; + var inMaxYear = currentYear == maxYear; + + var monthHtml = '"; + + var yearHtml = ''; + + dateHtml = monthHtml + yearHtml; + } + + html += ''; + if ((!maxDate || maxDate.isAfter(calendar.lastDay)) && (!this.linkedCalendars || side == 'right' || this.singleDatePicker)) { + html += ''; + } else { + html += ''; + } + + html += ''; + html += ''; + + // add week number label + if (this.showWeekNumbers || this.showISOWeekNumbers) + html += ''; + + $.each(this.locale.daysOfWeek, function(index, dayOfWeek) { + html += ''; + }); + + html += ''; + html += ''; + html += ''; + + //adjust maxDate to reflect the dateLimit setting in order to + //grey out end dates beyond the dateLimit + if (this.endDate == null && this.dateLimit) { + var maxLimit = this.startDate.clone().add(this.dateLimit).endOf('day'); + if (!maxDate || maxLimit.isBefore(maxDate)) { + maxDate = maxLimit; + } + } + + for (var row = 0; row < 6; row++) { + html += ''; + + // add week number + if (this.showWeekNumbers) + html += ''; + else if (this.showISOWeekNumbers) + html += ''; + + for (var col = 0; col < 7; col++) { + + var classes = []; + + //highlight today's date + if (calendar[row][col].isSame(new Date(), "day")) + classes.push('today'); + + //highlight weekends + if (calendar[row][col].isoWeekday() > 5) + classes.push('weekend'); + + //grey out the dates in other months displayed at beginning and end of this calendar + if (calendar[row][col].month() != calendar[1][1].month()) + classes.push('off'); + + //don't allow selection of dates before the minimum date + if (this.minDate && calendar[row][col].isBefore(this.minDate, 'day')) + classes.push('off', 'disabled'); + + //don't allow selection of dates after the maximum date + if (maxDate && calendar[row][col].isAfter(maxDate, 'day')) + classes.push('off', 'disabled'); + + //don't allow selection of date if a custom function decides it's invalid + if (this.isInvalidDate(calendar[row][col])) + classes.push('off', 'disabled'); + + //highlight the currently selected start date + if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD')) + classes.push('active', 'start-date'); + + //highlight the currently selected end date + if (this.endDate != null && calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD')) + classes.push('active', 'end-date'); + + //highlight dates in-between the selected dates + if (this.endDate != null && calendar[row][col] > this.startDate && calendar[row][col] < this.endDate) + classes.push('in-range'); + + //apply custom classes for this date + var isCustom = this.isCustomDate(calendar[row][col]); + if (isCustom !== false) { + if (typeof isCustom === 'string') + classes.push(isCustom); + else + Array.prototype.push.apply(classes, isCustom); + } + + var cname = '', disabled = false; + for (var i = 0; i < classes.length; i++) { + cname += classes[i] + ' '; + if (classes[i] == 'disabled') + disabled = true; + } + if (!disabled) + cname += 'available'; + + html += ''; + + } + html += ''; + } + + html += ''; + html += '
' + dateHtml + '
' + this.locale.weekLabel + '' + dayOfWeek + '
' + calendar[row][0].week() + '' + calendar[row][0].isoWeek() + '' + calendar[row][col].date() + '
'; + + this.container.find('.calendar.' + side + ' .calendar-table').html(html); + + }, + + renderTimePicker: function(side) { + + // Don't bother updating the time picker if it's currently disabled + // because an end date hasn't been clicked yet + if (side == 'right' && !this.endDate) return; + + var html, selected, minDate, maxDate = this.maxDate; + + if (this.dateLimit && (!this.maxDate || this.startDate.clone().add(this.dateLimit).isAfter(this.maxDate))) + maxDate = this.startDate.clone().add(this.dateLimit); + + if (side == 'left') { + selected = this.startDate.clone(); + minDate = this.minDate; + } else if (side == 'right') { + selected = this.endDate.clone(); + minDate = this.startDate; + + //Preserve the time already selected + var timeSelector = this.container.find('.calendar.right .calendar-time div'); + if (timeSelector.html() != '') { + + selected.hour(timeSelector.find('.hourselect option:selected').val() || selected.hour()); + selected.minute(timeSelector.find('.minuteselect option:selected').val() || selected.minute()); + selected.second(timeSelector.find('.secondselect option:selected').val() || selected.second()); + + if (!this.timePicker24Hour) { + var ampm = timeSelector.find('.ampmselect option:selected').val(); + if (ampm === 'PM' && selected.hour() < 12) + selected.hour(selected.hour() + 12); + if (ampm === 'AM' && selected.hour() === 12) + selected.hour(0); + } + + } + + if (selected.isBefore(this.startDate)) + selected = this.startDate.clone(); + + if (maxDate && selected.isAfter(maxDate)) + selected = maxDate.clone(); + + } + + // + // hours + // + + html = ' '; + + // + // minutes + // + + html += ': '; + + // + // seconds + // + + if (this.timePickerSeconds) { + html += ': '; + } + + // + // AM/PM + // + + if (!this.timePicker24Hour) { + html += ''; + } + + this.container.find('.calendar.' + side + ' .calendar-time div').html(html); + + }, + + updateFormInputs: function() { + + //ignore mouse movements while an above-calendar text input has focus + if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) + return; + + this.container.find('input[name=daterangepicker_start]').val(this.startDate.format(this.locale.format)); + if (this.endDate) + this.container.find('input[name=daterangepicker_end]').val(this.endDate.format(this.locale.format)); + + if (this.singleDatePicker || (this.endDate && (this.startDate.isBefore(this.endDate) || this.startDate.isSame(this.endDate)))) { + this.container.find('button.applyBtn').removeAttr('disabled'); + } else { + this.container.find('button.applyBtn').attr('disabled', 'disabled'); + } + + }, + + move: function() { + var parentOffset = { top: 0, left: 0 }, + containerTop; + var parentRightEdge = $(window).width(); + if (!this.parentEl.is('body')) { + parentOffset = { + top: this.parentEl.offset().top - this.parentEl.scrollTop(), + left: this.parentEl.offset().left - this.parentEl.scrollLeft() + }; + parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left; + } + + if (this.drops == 'up') + containerTop = this.element.offset().top - this.container.outerHeight() - parentOffset.top; + else + containerTop = this.element.offset().top + this.element.outerHeight() - parentOffset.top; + this.container[this.drops == 'up' ? 'addClass' : 'removeClass']('dropup'); + + if (this.opens == 'left') { + this.container.css({ + top: containerTop, + right: parentRightEdge - this.element.offset().left - this.element.outerWidth(), + left: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } + } else if (this.opens == 'center') { + this.container.css({ + top: containerTop, + left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2 + - this.container.outerWidth() / 2, + right: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } + } else { + this.container.css({ + top: containerTop, + left: this.element.offset().left - parentOffset.left, + right: 'auto' + }); + if (this.container.offset().left + this.container.outerWidth() > $(window).width()) { + this.container.css({ + left: 'auto', + right: 0 + }); + } + } + }, + + show: function(e) { + if (this.isShowing) return; + + // Create a click proxy that is private to this instance of datepicker, for unbinding + this._outsideClickProxy = $.proxy(function(e) { this.outsideClick(e); }, this); + + // Bind global datepicker mousedown for hiding and + $(document) + .on('mousedown.daterangepicker', this._outsideClickProxy) + // also support mobile devices + .on('touchend.daterangepicker', this._outsideClickProxy) + // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them + .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy) + // and also close when focus changes to outside the picker (eg. tabbing between controls) + .on('focusin.daterangepicker', this._outsideClickProxy); + + // Reposition the picker if the window is resized while it's open + $(window).on('resize.daterangepicker', $.proxy(function(e) { this.move(e); }, this)); + + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + this.previousRightTime = this.endDate.clone(); + + this.updateView(); + this.container.show(); + this.move(); + this.element.trigger('show.daterangepicker', this); + this.isShowing = true; + }, + + hide: function(e) { + if (!this.isShowing) return; + + //incomplete date selection, revert to last values + if (!this.endDate) { + this.startDate = this.oldStartDate.clone(); + this.endDate = this.oldEndDate.clone(); + } + + //if a new date range was selected, invoke the user callback function + if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) + this.callback(this.startDate, this.endDate, this.chosenLabel); + + //if picker is attached to a text input, update it + this.updateElement(); + + $(document).off('.daterangepicker'); + $(window).off('.daterangepicker'); + this.container.hide(); + this.element.trigger('hide.daterangepicker', this); + this.isShowing = false; + }, + + toggle: function(e) { + if (this.isShowing) { + this.hide(); + } else { + this.show(); + } + }, + + outsideClick: function(e) { + var target = $(e.target); + // if the page is clicked anywhere except within the daterangerpicker/button + // itself then call this.hide() + if ( + // ie modal dialog fix + e.type == "focusin" || + target.closest(this.element).length || + target.closest(this.container).length || + target.closest('.calendar-table').length + ) return; + this.hide(); + this.element.trigger('outsideClick.daterangepicker', this); + }, + + showCalendars: function() { + this.container.addClass('show-calendar'); + this.move(); + this.element.trigger('showCalendar.daterangepicker', this); + }, + + hideCalendars: function() { + this.container.removeClass('show-calendar'); + this.element.trigger('hideCalendar.daterangepicker', this); + }, + + hoverRange: function(e) { + + //ignore mouse movements while an above-calendar text input has focus + if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) + return; + + var label = e.target.getAttribute('data-range-key'); + + if (label == this.locale.customRangeLabel) { + this.updateView(); + } else { + var dates = this.ranges[label]; + this.container.find('input[name=daterangepicker_start]').val(dates[0].format(this.locale.format)); + this.container.find('input[name=daterangepicker_end]').val(dates[1].format(this.locale.format)); + } + + }, + + clickRange: function(e) { + var label = e.target.getAttribute('data-range-key'); + this.chosenLabel = label; + if (label == this.locale.customRangeLabel) { + this.showCalendars(); + } else { + var dates = this.ranges[label]; + this.startDate = dates[0]; + this.endDate = dates[1]; + + if (!this.timePicker) { + this.startDate.startOf('day'); + this.endDate.endOf('day'); + } + + if (!this.alwaysShowCalendars) + this.hideCalendars(); + this.clickApply(); + } + }, + + clickPrev: function(e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.subtract(1, 'month'); + if (this.linkedCalendars) + this.rightCalendar.month.subtract(1, 'month'); + } else { + this.rightCalendar.month.subtract(1, 'month'); + } + this.updateCalendars(); + }, + + clickNext: function(e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.add(1, 'month'); + } else { + this.rightCalendar.month.add(1, 'month'); + if (this.linkedCalendars) + this.leftCalendar.month.add(1, 'month'); + } + this.updateCalendars(); + }, + + hoverDate: function(e) { + + //ignore mouse movements while an above-calendar text input has focus + //if (this.container.find('input[name=daterangepicker_start]').is(":focus") || this.container.find('input[name=daterangepicker_end]').is(":focus")) + // return; + + //ignore dates that can't be selected + if (!$(e.target).hasClass('available')) return; + + //have the text inputs above calendars reflect the date being hovered over + var title = $(e.target).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; + + if (this.endDate && !this.container.find('input[name=daterangepicker_start]').is(":focus")) { + this.container.find('input[name=daterangepicker_start]').val(date.format(this.locale.format)); + } else if (!this.endDate && !this.container.find('input[name=daterangepicker_end]').is(":focus")) { + this.container.find('input[name=daterangepicker_end]').val(date.format(this.locale.format)); + } + + //highlight the dates between the start date and the date being hovered as a potential end date + var leftCalendar = this.leftCalendar; + var rightCalendar = this.rightCalendar; + var startDate = this.startDate; + if (!this.endDate) { + this.container.find('.calendar tbody td').each(function(index, el) { + + //skip week numbers, only look at dates + if ($(el).hasClass('week')) return; + + var title = $(el).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(el).parents('.calendar'); + var dt = cal.hasClass('left') ? leftCalendar.calendar[row][col] : rightCalendar.calendar[row][col]; + + if ((dt.isAfter(startDate) && dt.isBefore(date)) || dt.isSame(date, 'day')) { + $(el).addClass('in-range'); + } else { + $(el).removeClass('in-range'); + } + + }); + } + + }, + + clickDate: function(e) { + + if (!$(e.target).hasClass('available')) return; + + var title = $(e.target).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; + + // + // this function needs to do a few things: + // * alternate between selecting a start and end date for the range, + // * if the time picker is enabled, apply the hour/minute/second from the select boxes to the clicked date + // * if autoapply is enabled, and an end date was chosen, apply the selection + // * if single date picker mode, and time picker isn't enabled, apply the selection immediately + // * if one of the inputs above the calendars was focused, cancel that manual input + // + + if (this.endDate || date.isBefore(this.startDate, 'day')) { //picking start + if (this.timePicker) { + var hour = parseInt(this.container.find('.left .hourselect').val(), 10); + if (!this.timePicker24Hour) { + var ampm = this.container.find('.left .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + var minute = parseInt(this.container.find('.left .minuteselect').val(), 10); + var second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0; + date = date.clone().hour(hour).minute(minute).second(second); + } + this.endDate = null; + this.setStartDate(date.clone()); + } else if (!this.endDate && date.isBefore(this.startDate)) { + //special case: clicking the same date for start/end, + //but the time of the end date is before the start date + this.setEndDate(this.startDate.clone()); + } else { // picking end + if (this.timePicker) { + var hour = parseInt(this.container.find('.right .hourselect').val(), 10); + if (!this.timePicker24Hour) { + var ampm = this.container.find('.right .ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + var minute = parseInt(this.container.find('.right .minuteselect').val(), 10); + var second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0; + date = date.clone().hour(hour).minute(minute).second(second); + } + this.setEndDate(date.clone()); + if (this.autoApply) { + this.calculateChosenLabel(); + this.clickApply(); + } + } + + if (this.singleDatePicker) { + this.setEndDate(this.startDate); + if (!this.timePicker) + this.clickApply(); + } + + this.updateView(); + + //This is to cancel the blur event handler if the mouse was in one of the inputs + e.stopPropagation(); + + }, + + calculateChosenLabel: function () { + var customRange = true; + var i = 0; + for (var range in this.ranges) { + if (this.timePicker) { + var format = this.timePickerSeconds ? "YYYY-MM-DD hh:mm:ss" : "YYYY-MM-DD hh:mm"; + //ignore times when comparing dates if time picker seconds is not enabled + if (this.startDate.format(format) == this.ranges[range][0].format(format) && this.endDate.format(format) == this.ranges[range][1].format(format)) { + customRange = false; + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').html(); + break; + } + } else { + //ignore times when comparing dates if time picker is not enabled + if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) { + customRange = false; + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').html(); + break; + } + } + i++; + } + if (customRange) { + if (this.showCustomRangeLabel) { + this.chosenLabel = this.container.find('.ranges li:last').addClass('active').html(); + } else { + this.chosenLabel = null; + } + this.showCalendars(); + } + }, + + clickApply: function(e) { + this.hide(); + this.element.trigger('apply.daterangepicker', this); + }, + + clickCancel: function(e) { + this.startDate = this.oldStartDate; + this.endDate = this.oldEndDate; + this.hide(); + this.element.trigger('cancel.daterangepicker', this); + }, + + monthOrYearChanged: function(e) { + var isLeft = $(e.target).closest('.calendar').hasClass('left'), + leftOrRight = isLeft ? 'left' : 'right', + cal = this.container.find('.calendar.'+leftOrRight); + + // Month must be Number for new moment versions + var month = parseInt(cal.find('.monthselect').val(), 10); + var year = cal.find('.yearselect').val(); + + if (!isLeft) { + if (year < this.startDate.year() || (year == this.startDate.year() && month < this.startDate.month())) { + month = this.startDate.month(); + year = this.startDate.year(); + } + } + + if (this.minDate) { + if (year < this.minDate.year() || (year == this.minDate.year() && month < this.minDate.month())) { + month = this.minDate.month(); + year = this.minDate.year(); + } + } + + if (this.maxDate) { + if (year > this.maxDate.year() || (year == this.maxDate.year() && month > this.maxDate.month())) { + month = this.maxDate.month(); + year = this.maxDate.year(); + } + } + + if (isLeft) { + this.leftCalendar.month.month(month).year(year); + if (this.linkedCalendars) + this.rightCalendar.month = this.leftCalendar.month.clone().add(1, 'month'); + } else { + this.rightCalendar.month.month(month).year(year); + if (this.linkedCalendars) + this.leftCalendar.month = this.rightCalendar.month.clone().subtract(1, 'month'); + } + this.updateCalendars(); + }, + + timeChanged: function(e) { + + var cal = $(e.target).closest('.calendar'), + isLeft = cal.hasClass('left'); + + var hour = parseInt(cal.find('.hourselect').val(), 10); + var minute = parseInt(cal.find('.minuteselect').val(), 10); + var second = this.timePickerSeconds ? parseInt(cal.find('.secondselect').val(), 10) : 0; + + if (!this.timePicker24Hour) { + var ampm = cal.find('.ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + + if (isLeft) { + var start = this.startDate.clone(); + start.hour(hour); + start.minute(minute); + start.second(second); + this.setStartDate(start); + if (this.singleDatePicker) { + this.endDate = this.startDate.clone(); + } else if (this.endDate && this.endDate.format('YYYY-MM-DD') == start.format('YYYY-MM-DD') && this.endDate.isBefore(start)) { + this.setEndDate(start.clone()); + } + } else if (this.endDate) { + var end = this.endDate.clone(); + end.hour(hour); + end.minute(minute); + end.second(second); + this.setEndDate(end); + } + + //update the calendars so all clickable dates reflect the new time component + this.updateCalendars(); + + //update the form inputs above the calendars with the new time + this.updateFormInputs(); + + //re-render the time pickers because changing one selection can affect what's enabled in another + this.renderTimePicker('left'); + this.renderTimePicker('right'); + + }, + + formInputsChanged: function(e) { + var isRight = $(e.target).closest('.calendar').hasClass('right'); + var start = moment(this.container.find('input[name="daterangepicker_start"]').val(), this.locale.format); + var end = moment(this.container.find('input[name="daterangepicker_end"]').val(), this.locale.format); + + if (start.isValid() && end.isValid()) { + + if (isRight && end.isBefore(start)) + start = end.clone(); + + this.setStartDate(start); + this.setEndDate(end); + + if (isRight) { + this.container.find('input[name="daterangepicker_start"]').val(this.startDate.format(this.locale.format)); + } else { + this.container.find('input[name="daterangepicker_end"]').val(this.endDate.format(this.locale.format)); + } + + } + + this.updateView(); + }, + + formInputsFocused: function(e) { + + // Highlight the focused input + this.container.find('input[name="daterangepicker_start"], input[name="daterangepicker_end"]').removeClass('active'); + $(e.target).addClass('active'); + + // Set the state such that if the user goes back to using a mouse, + // the calendars are aware we're selecting the end of the range, not + // the start. This allows someone to edit the end of a date range without + // re-selecting the beginning, by clicking on the end date input then + // using the calendar. + var isRight = $(e.target).closest('.calendar').hasClass('right'); + if (isRight) { + this.endDate = null; + this.setStartDate(this.startDate.clone()); + this.updateView(); + } + + }, + + formInputsBlurred: function(e) { + + // this function has one purpose right now: if you tab from the first + // text input to the second in the UI, the endDate is nulled so that + // you can click another, but if you tab out without clicking anything + // or changing the input value, the old endDate should be retained + + if (!this.endDate) { + var val = this.container.find('input[name="daterangepicker_end"]').val(); + var end = moment(val, this.locale.format); + if (end.isValid()) { + this.setEndDate(end); + this.updateView(); + } + } + + }, + + formInputsKeydown: function(e) { + // This function ensures that if the 'enter' key was pressed in the input, then the calendars + // are updated with the startDate and endDate. + // This behaviour is automatic in Chrome/Firefox/Edge but not in IE 11 hence why this exists. + // Other browsers and versions of IE are untested and the behaviour is unknown. + if (e.keyCode === 13) { + // Prevent the calendar from being updated twice on Chrome/Firefox/Edge + e.preventDefault(); + this.formInputsChanged(e); + } + }, + + + elementChanged: function() { + if (!this.element.is('input')) return; + if (!this.element.val().length) return; + + var dateString = this.element.val().split(this.locale.separator), + start = null, + end = null; + + if (dateString.length === 2) { + start = moment(dateString[0], this.locale.format); + end = moment(dateString[1], this.locale.format); + } + + if (this.singleDatePicker || start === null || end === null) { + start = moment(this.element.val(), this.locale.format); + end = start; + } + + if (!start.isValid() || !end.isValid()) return; + + this.setStartDate(start); + this.setEndDate(end); + this.updateView(); + }, + + keydown: function(e) { + //hide on tab or enter + if ((e.keyCode === 9) || (e.keyCode === 13)) { + this.hide(); + } + + //hide on esc and prevent propagation + if (e.keyCode === 27) { + e.preventDefault(); + e.stopPropagation(); + + this.hide(); + } + }, + + updateElement: function() { + if (this.element.is('input') && !this.singleDatePicker && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); + this.element.trigger('change'); + } else if (this.element.is('input') && this.autoUpdateInput) { + this.element.val(this.startDate.format(this.locale.format)); + this.element.trigger('change'); + } + }, + + remove: function() { + this.container.remove(); + this.element.off('.daterangepicker'); + this.element.removeData(); + } + + }; + + $.fn.daterangepicker = function(options, callback) { + var implementOptions = $.extend(true, {}, $.fn.daterangepicker.defaultOptions, options); + this.each(function() { + var el = $(this); + if (el.data('daterangepicker')) + el.data('daterangepicker').remove(); + el.data('daterangepicker', new DateRangePicker(el, implementOptions, callback)); + }); + return this; + }; + + return DateRangePicker; + +})); diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css new file mode 100644 index 00000000..5b96335f --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:"Glyphicons Halflings";src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format("embedded-opentype"),url(../fonts/glyphicons-halflings-regular.woff2) format("woff2"),url(../fonts/glyphicons-halflings-regular.woff) format("woff"),url(../fonts/glyphicons-halflings-regular.ttf) format("truetype"),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*=col-]{padding-right:0;padding-left:0}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;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;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;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,-webkit-box-shadow ease-in-out .15s}.form-control: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)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm input[type=time],input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg input[type=time],input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;background-image:none;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;background-image:none;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;filter:alpha(opacity=90);opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css.map b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css.map new file mode 100644 index 00000000..0ae3de50 --- /dev/null +++ b/project/xxl-job-2.4.0/xxl-job-admin/src/main/resources/static/adminlte/bower_components/bootstrap/css/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap.css","less/normalize.less","dist/css/bootstrap.css","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/mixins/reset-text.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"AAAA;;;;AAKA,4ECKA,KACE,YAAA,WACA,qBAAA,KACA,yBAAA,KAOF,KACE,OAAA,EAaF,QCnBA,MACA,QACA,WACA,OACA,OACA,OACA,OACA,KACA,KACA,IACA,QACA,QDqBE,QAAA,MAQF,MCzBA,OACA,SACA,MD2BE,QAAA,aACA,eAAA,SAQF,sBACE,QAAA,KACA,OAAA,EAQF,SCrCA,SDuCE,QAAA,KAUF,EACE,iBAAA,YAQF,SCnDA,QDqDE,QAAA,EAWF,YACE,cAAA,KACA,gBAAA,UACA,wBAAA,UAAA,OAAA,qBAAA,UAAA,OAAA,gBAAA,UAAA,OAOF,EC/DA,ODiEE,YAAA,IAOF,IACE,WAAA,OAQF,GACE,UAAA,IACA,OAAA,MAAA,EAOF,KACE,WAAA,KACA,MAAA,KAOF,MACE,UAAA,IAOF,ICzFA,ID2FE,UAAA,IACA,YAAA,EACA,SAAA,SACA,eAAA,SAGF,IACE,IAAA,MAGF,IACE,OAAA,OAUF,IACE,OAAA,EAOF,eACE,SAAA,OAUF,OACE,OAAA,IAAA,KAOF,GACE,mBAAA,YAAA,gBAAA,YAAA,WAAA,YACA,OAAA,EAOF,IACE,SAAA,KAOF,KC7HA,IACA,IACA,KD+HE,YAAA,SAAA,CAAA,UACA,UAAA,IAkBF,OC7IA,MACA,SACA,OACA,SD+IE,MAAA,QACA,KAAA,QACA,OAAA,EAOF,OACE,SAAA,QAUF,OC1JA,OD4JE,eAAA,KAWF,OCnKA,wBACA,kBACA,mBDqKE,mBAAA,OACA,OAAA,QAOF,iBCxKA,qBD0KE,OAAA,QAOF,yBC7KA,wBD+KE,OAAA,EACA,QAAA,EAQF,MACE,YAAA,OAWF,qBC5LA,kBD8LE,mBAAA,WAAA,gBAAA,WAAA,WAAA,WACA,QAAA,EASF,8CCjMA,8CDmME,OAAA,KAQF,mBACE,mBAAA,UACA,mBAAA,YAAA,gBAAA,YAAA,WAAA,YASF,iDC5MA,8CD8ME,mBAAA,KAOF,SACE,OAAA,IAAA,MAAA,OACA,OAAA,EAAA,IACA,QAAA,MAAA,OAAA,MAQF,OACE,OAAA,EACA,QAAA,EAOF,SACE,SAAA,KAQF,SACE,YAAA,IAUF,MACE,gBAAA,SACA,eAAA,EAGF,GC3OA,GD6OE,QAAA,EDlPF,qFGhLA,aACE,ED2LA,OADA,QCvLE,MAAA,eACA,YAAA,eACA,WAAA,cACA,mBAAA,eAAA,WAAA,eAGF,ED0LA,UCxLE,gBAAA,UAGF,cACE,QAAA,KAAA,WAAA,IAGF,kBACE,QAAA,KAAA,YAAA,IAKF,mBDqLA,6BCnLE,QAAA,GDuLF,WCpLA,IAEE,OAAA,IAAA,MAAA,KACA,kBAAA,MAGF,MACE,QAAA,mBDqLF,IClLA,GAEE,kBAAA,MAGF,IACE,UAAA,eDmLF,GACA,GCjLA,EAGE,QAAA,EACA,OAAA,EAGF,GD+KA,GC7KE,iBAAA,MAMF,QACE,QAAA,KAEF,YD2KA,oBCxKI,iBAAA,eAGJ,OACE,OAAA,IAAA,MAAA,KAGF,OACE,gBAAA,mBADF,UD2KA,UCtKI,iBAAA,eD0KJ,mBCvKA,mBAGI,OAAA,IAAA,MAAA,gBCrFN,WACE,YAAA,uBACA,IAAA,+CACA,IAAA,sDAAA,2BAAA,CAAA,iDAAA,eAAA,CAAA,gDAAA,cAAA,CAAA,+CAAA,kBAAA,CAAA,2EAAA,cAQF,WACE,SAAA,SACA,IAAA,IACA,QAAA,aACA,YAAA,uBACA,WAAA,OACA,YAAA,IACA,YAAA,EACA,uBAAA,YACA,wBAAA,UAIkC,2BAAW,QAAA,QACX,uBAAW,QAAA,QF2P/C,sBEzPoC,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,wBAAW,QAAA,QACX,2BAAW,QAAA,QACX,yBAAW,QAAA,QACX,wBAAW,QAAA,QACX,wBAAW,QAAA,QACX,yBAAW,QAAA,QACX,wBAAW,QAAA,QACX,uBAAW,QAAA,QACX,6BAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,2BAAW,QAAA,QACX,qBAAW,QAAA,QACX,0BAAW,QAAA,QACX,qBAAW,QAAA,QACX,yBAAW,QAAA,QACX,0BAAW,QAAA,QACX,2BAAW,QAAA,QACX,sBAAW,QAAA,QACX,yBAAW,QAAA,QACX,sBAAW,QAAA,QACX,wBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,+BAAW,QAAA,QACX,2BAAW,QAAA,QACX,yBAAW,QAAA,QACX,wBAAW,QAAA,QACX,8BAAW,QAAA,QACX,yBAAW,QAAA,QACX,0BAAW,QAAA,QACX,2BAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,6BAAW,QAAA,QACX,6BAAW,QAAA,QACX,8BAAW,QAAA,QACX,4BAAW,QAAA,QACX,yBAAW,QAAA,QACX,0BAAW,QAAA,QACX,sBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,2BAAW,QAAA,QACX,wBAAW,QAAA,QACX,yBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,yBAAW,QAAA,QACX,8BAAW,QAAA,QACX,6BAAW,QAAA,QACX,6BAAW,QAAA,QACX,+BAAW,QAAA,QACX,8BAAW,QAAA,QACX,gCAAW,QAAA,QACX,uBAAW,QAAA,QACX,8BAAW,QAAA,QACX,+BAAW,QAAA,QACX,iCAAW,QAAA,QACX,0BAAW,QAAA,QACX,6BAAW,QAAA,QACX,yBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,wBAAW,QAAA,QACX,uBAAW,QAAA,QACX,gCAAW,QAAA,QACX,gCAAW,QAAA,QACX,2BAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,uBAAW,QAAA,QACX,0BAAW,QAAA,QACX,+BAAW,QAAA,QACX,+BAAW,QAAA,QACX,wBAAW,QAAA,QACX,+BAAW,QAAA,QACX,gCAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,8BAAW,QAAA,QACX,0BAAW,QAAA,QACX,gCAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,gCAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,6BAAW,QAAA,QACX,8BAAW,QAAA,QACX,2BAAW,QAAA,QACX,6BAAW,QAAA,QACX,4BAAW,QAAA,QACX,8BAAW,QAAA,QACX,+BAAW,QAAA,QACX,mCAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,2BAAW,QAAA,QACX,4BAAW,QAAA,QACX,+BAAW,QAAA,QACX,wBAAW,QAAA,QACX,2BAAW,QAAA,QACX,yBAAW,QAAA,QACX,0BAAW,QAAA,QACX,yBAAW,QAAA,QACX,6BAAW,QAAA,QACX,+BAAW,QAAA,QACX,0BAAW,QAAA,QACX,gCAAW,QAAA,QACX,+BAAW,QAAA,QACX,8BAAW,QAAA,QACX,kCAAW,QAAA,QACX,oCAAW,QAAA,QACX,sBAAW,QAAA,QACX,2BAAW,QAAA,QACX,uBAAW,QAAA,QACX,8BAAW,QAAA,QACX,4BAAW,QAAA,QACX,8BAAW,QAAA,QACX,6BAAW,QAAA,QACX,4BAAW,QAAA,QACX,0BAAW,QAAA,QACX,4BAAW,QAAA,QACX,qCAAW,QAAA,QACX,oCAAW,QAAA,QACX,kCAAW,QAAA,QACX,oCAAW,QAAA,QACX,wBAAW,QAAA,QACX,yBAAW,QAAA,QACX,wBAAW,QAAA,QACX,yBAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,4BAAW,QAAA,QACX,4BAAW,QAAA,QACX,8BAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,0BAAW,QAAA,QACX,sBAAW,QAAA,QACX,sBAAW,QAAA,QACX,uBAAW,QAAA,QACX,mCAAW,QAAA,QACX,uCAAW,QAAA,QACX,gCAAW,QAAA,QACX,oCAAW,QAAA,QACX,qCAAW,QAAA,QACX,yCAAW,QAAA,QACX,4BAAW,QAAA,QACX,yBAAW,QAAA,QACX,gCAAW,QAAA,QACX,8BAAW,QAAA,QACX,yBAAW,QAAA,QACX,wBAAW,QAAA,QACX,0BAAW,QAAA,QACX,6BAAW,QAAA,QACX,yBAAW,QAAA,QACX,uBAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,yBAAW,QAAA,QACX,yBAAW,QAAA,QACX,uBAAW,QAAA,QACX,8BAAW,QAAA,QACX,+BAAW,QAAA,QACX,gCAAW,QAAA,QACX,8BAAW,QAAA,QACX,8BAAW,QAAA,QACX,8BAAW,QAAA,QACX,2BAAW,QAAA,QACX,0BAAW,QAAA,QACX,yBAAW,QAAA,QACX,6BAAW,QAAA,QACX,2BAAW,QAAA,QACX,4BAAW,QAAA,QACX,wBAAW,QAAA,QACX,wBAAW,QAAA,QACX,2BAAW,QAAA,QACX,2BAAW,QAAA,QACX,4BAAW,QAAA,QACX,+BAAW,QAAA,QACX,8BAAW,QAAA,QACX,4BAAW,QAAA,QACX,4BAAW,QAAA,QACX,4BAAW,QAAA,QACX,iCAAW,QAAA,QACX,oCAAW,QAAA,QACX,iCAAW,QAAA,QACX,+BAAW,QAAA,QACX,+BAAW,QAAA,QACX,iCAAW,QAAA,QACX,qBAAW,QAAA,QACX,4BAAW,QAAA,QACX,4BAAW,QAAA,QACX,2BAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QASX,wBAAW,QAAA,QACX,4BAAW,QAAA,QACX,uBAAW,QAAA,QACX,wBAAW,QAAA,QACX,uBAAW,QAAA,QACX,yBAAW,QAAA,QACX,yBAAW,QAAA,QACX,+BAAW,QAAA,QACX,uBAAW,QAAA,QACX,6BAAW,QAAA,QACX,sBAAW,QAAA,QACX,wBAAW,QAAA,QACX,wBAAW,QAAA,QACX,4BAAW,QAAA,QACX,uBAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,2BAAW,QAAA,QACX,0BAAW,QAAA,QACX,sBAAW,QAAA,QACX,sBAAW,QAAA,QACX,sBAAW,QAAA,QACX,sBAAW,QAAA,QACX,wBAAW,QAAA,QACX,sBAAW,QAAA,QACX,wBAAW,QAAA,QACX,4BAAW,QAAA,QACX,mCAAW,QAAA,QACX,4BAAW,QAAA,QACX,oCAAW,QAAA,QACX,kCAAW,QAAA,QACX,iCAAW,QAAA,QACX,+BAAW,QAAA,QACX,sBAAW,QAAA,QACX,wBAAW,QAAA,QACX,6BAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,kCAAW,QAAA,QACX,mCAAW,QAAA,QACX,sCAAW,QAAA,QACX,0CAAW,QAAA,QACX,oCAAW,QAAA,QACX,wCAAW,QAAA,QACX,qCAAW,QAAA,QACX,iCAAW,QAAA,QACX,gCAAW,QAAA,QACX,kCAAW,QAAA,QACX,+BAAW,QAAA,QACX,0BAAW,QAAA,QACX,8BAAW,QAAA,QACX,4BAAW,QAAA,QACX,4BAAW,QAAA,QACX,6BAAW,QAAA,QACX,4BAAW,QAAA,QACX,0BAAW,QAAA,QCxS/C,ECkEE,mBAAA,WACG,gBAAA,WACK,WAAA,WJo+BV,OGriCA,QC+DE,mBAAA,WACG,gBAAA,WACK,WAAA,WDzDV,KACE,UAAA,KACA,4BAAA,cAGF,KACE,YAAA,gBAAA,CAAA,SAAA,CAAA,KAAA,CAAA,WACA,UAAA,KACA,YAAA,WACA,MAAA,KACA,iBAAA,KHoiCF,OGhiCA,MHiiCA,OACA,SG9hCE,YAAA,QACA,UAAA,QACA,YAAA,QAMF,EACE,MAAA,QACA,gBAAA,KH8hCF,QG5hCE,QAEE,MAAA,QACA,gBAAA,UAGF,QEnDA,QAAA,IAAA,KAAA,yBACA,eAAA,KF6DF,OACE,OAAA,EAMF,IACE,eAAA,OHqhCF,4BADA,0BGhhCA,gBH+gCA,iBADA,eMxlCE,QAAA,MACA,UAAA,KACA,OAAA,KH6EF,aACE,cAAA,IAMF,eACE,QAAA,IACA,YAAA,WACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,cAAA,IC+FA,mBAAA,IAAA,IAAA,YACK,cAAA,IAAA,IAAA,YACG,WAAA,IAAA,IAAA,YE5LR,QAAA,aACA,UAAA,KACA,OAAA,KHiGF,YACE,cAAA,IAMF,GACE,WAAA,KACA,cAAA,KACA,OAAA,EACA,WAAA,IAAA,MAAA,KAQF,SACE,SAAA,SACA,MAAA,IACA,OAAA,IACA,QAAA,EACA,OAAA,KACA,SAAA,OACA,KAAA,cACA,OAAA,EAQA,0BH8/BF,yBG5/BI,SAAA,OACA,MAAA,KACA,OAAA,KACA,OAAA,EACA,SAAA,QACA,KAAA,KAWJ,cACE,OAAA,QH4/BF,IACA,IACA,IACA,IACA,IACA,IOtpCA,GP4oCA,GACA,GACA,GACA,GACA,GO9oCE,YAAA,QACA,YAAA,IACA,YAAA,IACA,MAAA,QPyqCF,WAZA,UAaA,WAZA,UAaA,WAZA,UAaA,WAZA,UAaA,WAZA,UAaA,WAZA,UACA,UOxqCA,SPyqCA,UAZA,SAaA,UAZA,SAaA,UAZA,SAaA,UAZA,SAaA,UAZA,SOxpCI,YAAA,IACA,YAAA,EACA,MAAA,KP8qCJ,IAEA,IAEA,IO9qCA,GP2qCA,GAEA,GO1qCE,WAAA,KACA,cAAA,KPqrCF,WANA,UAQA,WANA,UAQA,WANA,UACA,UOxrCA,SP0rCA,UANA,SAQA,UANA,SO9qCI,UAAA,IPyrCJ,IAEA,IAEA,IO1rCA,GPurCA,GAEA,GOtrCE,WAAA,KACA,cAAA,KPisCF,WANA,UAQA,WANA,UAQA,WANA,UACA,UOpsCA,SPssCA,UANA,SAQA,UANA,SO1rCI,UAAA,IPqsCJ,IOjsCA,GAAU,UAAA,KPqsCV,IOpsCA,GAAU,UAAA,KPwsCV,IOvsCA,GAAU,UAAA,KP2sCV,IO1sCA,GAAU,UAAA,KP8sCV,IO7sCA,GAAU,UAAA,KPitCV,IOhtCA,GAAU,UAAA,KAMV,EACE,OAAA,EAAA,EAAA,KAGF,MACE,cAAA,KACA,UAAA,KACA,YAAA,IACA,YAAA,IAEA,yBAAA,MACE,UAAA,MPitCJ,OOxsCA,MAEE,UAAA,IP0sCF,MOvsCA,KAEE,QAAA,KACA,iBAAA,QAIF,WAAuB,WAAA,KACvB,YAAuB,WAAA,MACvB,aAAuB,WAAA,OACvB,cAAuB,WAAA,QACvB,aAAuB,YAAA,OAGvB,gBAAuB,eAAA,UACvB,gBAAuB,eAAA,UACvB,iBAAuB,eAAA,WAGvB,YACE,MAAA,KAEF,cCvGE,MAAA,QR2zCF,qBQ1zCE,qBAEE,MAAA,QDuGJ,cC1GE,MAAA,QRk0CF,qBQj0CE,qBAEE,MAAA,QD0GJ,WC7GE,MAAA,QRy0CF,kBQx0CE,kBAEE,MAAA,QD6GJ,cChHE,MAAA,QRg1CF,qBQ/0CE,qBAEE,MAAA,QDgHJ,aCnHE,MAAA,QRu1CF,oBQt1CE,oBAEE,MAAA,QDuHJ,YAGE,MAAA,KE7HA,iBAAA,QT+1CF,mBS91CE,mBAEE,iBAAA,QF6HJ,YEhIE,iBAAA,QTs2CF,mBSr2CE,mBAEE,iBAAA,QFgIJ,SEnIE,iBAAA,QT62CF,gBS52CE,gBAEE,iBAAA,QFmIJ,YEtIE,iBAAA,QTo3CF,mBSn3CE,mBAEE,iBAAA,QFsIJ,WEzIE,iBAAA,QT23CF,kBS13CE,kBAEE,iBAAA,QF8IJ,aACE,eAAA,IACA,OAAA,KAAA,EAAA,KACA,cAAA,IAAA,MAAA,KPgvCF,GOxuCA,GAEE,WAAA,EACA,cAAA,KP4uCF,MAFA,MACA,MO9uCA,MAMI,cAAA,EAOJ,eACE,aAAA,EACA,WAAA,KAIF,aALE,aAAA,EACA,WAAA,KAMA,YAAA,KAFF,gBAKI,QAAA,aACA,cAAA,IACA,aAAA,IAKJ,GACE,WAAA,EACA,cAAA,KPouCF,GOluCA,GAEE,YAAA,WAEF,GACE,YAAA,IAEF,GACE,YAAA,EAaA,yBAAA,kBAEI,MAAA,KACA,MAAA,MACA,MAAA,KACA,WAAA,MGxNJ,SAAA,OACA,cAAA,SACA,YAAA,OHiNA,kBASI,YAAA,OP4tCN,0BOjtCA,YAEE,OAAA,KAGF,YACE,UAAA,IA9IqB,eAAA,UAmJvB,WACE,QAAA,KAAA,KACA,OAAA,EAAA,EAAA,KACA,UAAA,OACA,YAAA,IAAA,MAAA,KPitCF,yBO5sCI,wBP2sCJ,yBO1sCM,cAAA,EPgtCN,kBO1tCA,kBPytCA,iBOtsCI,QAAA,MACA,UAAA,IACA,YAAA,WACA,MAAA,KP4sCJ,yBO1sCI,yBPysCJ,wBOxsCM,QAAA,cAQN,oBPqsCA,sBOnsCE,cAAA,KACA,aAAA,EACA,WAAA,MACA,aAAA,IAAA,MAAA,KACA,YAAA,EP0sCF,kCOpsCI,kCPksCJ,iCAGA,oCAJA,oCAEA,mCOnsCe,QAAA,GP4sCf,iCO3sCI,iCPysCJ,gCAGA,mCAJA,mCAEA,kCOzsCM,QAAA,cAMN,QACE,cAAA,KACA,WAAA,OACA,YAAA,WIxSF,KXm/CA,IACA,IACA,KWj/CE,YAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,aAAA,CAAA,UAIF,KACE,QAAA,IAAA,IACA,UAAA,IACA,MAAA,QACA,iBAAA,QACA,cAAA,IAIF,IACE,QAAA,IAAA,IACA,UAAA,IACA,MAAA,KACA,iBAAA,KACA,cAAA,IACA,mBAAA,MAAA,EAAA,KAAA,EAAA,gBAAA,WAAA,MAAA,EAAA,KAAA,EAAA,gBANF,QASI,QAAA,EACA,UAAA,KACA,YAAA,IACA,mBAAA,KAAA,WAAA,KAKJ,IACE,QAAA,MACA,QAAA,MACA,OAAA,EAAA,EAAA,KACA,UAAA,KACA,YAAA,WACA,MAAA,KACA,WAAA,UACA,UAAA,WACA,iBAAA,QACA,OAAA,IAAA,MAAA,KACA,cAAA,IAXF,SAeI,QAAA,EACA,UAAA,QACA,MAAA,QACA,YAAA,SACA,iBAAA,YACA,cAAA,EAKJ,gBACE,WAAA,MACA,WAAA,OC1DF,WCHE,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KDGA,yBAAA,WACE,MAAA,OAEF,yBAAA,WACE,MAAA,OAEF,0BAAA,WACE,MAAA,QAUJ,iBCvBE,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KD6BF,KCvBE,aAAA,MACA,YAAA,MD0BF,gBACE,aAAA,EACA,YAAA,EAFF,8BAKI,cAAA,EACA,aAAA,EZwiDJ,UAoCA,WAIA,WAIA,WAxCA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAjCA,UAoCA,WAIA,WAIA,WAxCA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAjCA,UAoCA,WAIA,WAIA,WAxCA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UatnDC,UbynDD,WAIA,WAIA,WAxCA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UAIA,UcpmDM,SAAA,SAEA,WAAA,IAEA,cAAA,KACA,aAAA,KDtBL,UbmpDD,WACA,WACA,WAVA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,Uc3mDM,MAAA,KDvCL,WC+CG,MAAA,KD/CH,WC+CG,MAAA,aD/CH,WC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,YD/CH,gBC8DG,MAAA,KD9DH,gBC8DG,MAAA,aD9DH,gBC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,EFCJ,yBCzEC,Ub2zDC,WACA,WACA,WAVA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UcnxDI,MAAA,KDvCL,WC+CG,MAAA,KD/CH,WC+CG,MAAA,aD/CH,WC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,YD/CH,gBC8DG,MAAA,KD9DH,gBC8DG,MAAA,aD9DH,gBC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,GFUJ,yBClFC,Ubo+DC,WACA,WACA,WAVA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,Uc57DI,MAAA,KDvCL,WC+CG,MAAA,KD/CH,WC+CG,MAAA,aD/CH,WC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,YD/CH,gBC8DG,MAAA,KD9DH,gBC8DG,MAAA,aD9DH,gBC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,GFmBJ,0BC3FC,Ub6oEC,WACA,WACA,WAVA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UcrmEI,MAAA,KDvCL,WC+CG,MAAA,KD/CH,WC+CG,MAAA,aD/CH,WC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,ID/CH,UC+CG,MAAA,aD/CH,UC+CG,MAAA,YD/CH,gBC8DG,MAAA,KD9DH,gBC8DG,MAAA,aD9DH,gBC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,ID9DH,eC8DG,MAAA,aD9DH,eC8DG,MAAA,YD9DH,eCmEG,MAAA,KDnEH,gBCoDG,KAAA,KDpDH,gBCoDG,KAAA,aDpDH,gBCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,IDpDH,eCoDG,KAAA,aDpDH,eCoDG,KAAA,YDpDH,eCyDG,KAAA,KDzDH,kBCwEG,YAAA,KDxEH,kBCwEG,YAAA,aDxEH,kBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,IDxEH,iBCwEG,YAAA,aDxEH,iBCwEG,YAAA,YDxEH,iBCwEG,YAAA,GCjEJ,MACE,iBAAA,YADF,uBAQI,SAAA,OACA,QAAA,aACA,MAAA,KAKA,sBf+xEJ,sBe9xEM,SAAA,OACA,QAAA,WACA,MAAA,KAKN,QACE,YAAA,IACA,eAAA,IACA,MAAA,KACA,WAAA,KAGF,GACE,WAAA,KAMF,OACE,MAAA,KACA,UAAA,KACA,cAAA,Kf6xEF,mBAHA,mBAIA,mBAHA,mBACA,mBe/xEA,mBAWQ,QAAA,IACA,YAAA,WACA,eAAA,IACA,WAAA,IAAA,MAAA,KAdR,mBAoBI,eAAA,OACA,cAAA,IAAA,MAAA,KfyxEJ,uCe9yEA,uCf+yEA,wCAHA,wCAIA,2CAHA,2Ce/wEQ,WAAA,EA9BR,mBAoCI,WAAA,IAAA,MAAA,KApCJ,cAyCI,iBAAA,KfoxEJ,6BAHA,6BAIA,6BAHA,6BACA,6Be5wEA,6BAOQ,QAAA,IAWR,gBACE,OAAA,IAAA,MAAA,KfqwEF,4BAHA,4BAIA,4BAHA,4BACA,4BerwEA,4BAQQ,OAAA,IAAA,MAAA,KfmwER,4Be3wEA,4BAeM,oBAAA,IAUN,yCAEI,iBAAA,QASJ,4BAEI,iBAAA,QfqvEJ,0BAGA,0BATA,0BAGA,0BAIA,0BAGA,0BATA,0BAGA,0BACA,0BAGA,0BgBt4EE,0BhBg4EF,0BgBz3EM,iBAAA,QhBs4EN,sCAEA,sCADA,oCgBj4EE,sChB+3EF,sCgBz3EM,iBAAA,QhBs4EN,2BAGA,2BATA,2BAGA,2BAIA,2BAGA,2BATA,2BAGA,2BACA,2BAGA,2BgB35EE,2BhBq5EF,2BgB94EM,iBAAA,QhB25EN,uCAEA,uCADA,qCgBt5EE,uChBo5EF,uCgB94EM,iBAAA,QhB25EN,wBAGA,wBATA,wBAGA,wBAIA,wBAGA,wBATA,wBAGA,wBACA,wBAGA,wBgBh7EE,wBhB06EF,wBgBn6EM,iBAAA,QhBg7EN,oCAEA,oCADA,kCgB36EE,oChBy6EF,oCgBn6EM,iBAAA,QhBg7EN,2BAGA,2BATA,2BAGA,2BAIA,2BAGA,2BATA,2BAGA,2BACA,2BAGA,2BgBr8EE,2BhB+7EF,2BgBx7EM,iBAAA,QhBq8EN,uCAEA,uCADA,qCgBh8EE,uChB87EF,uCgBx7EM,iBAAA,QhBq8EN,0BAGA,0BATA,0BAGA,0BAIA,0BAGA,0BATA,0BAGA,0BACA,0BAGA,0BgB19EE,0BhBo9EF,0BgB78EM,iBAAA,QhB09EN,sCAEA,sCADA,oCgBr9EE,sChBm9EF,sCgB78EM,iBAAA,QDoJN,kBACE,WAAA,KACA,WAAA,KAEA,oCAAA,kBACE,MAAA,KACA,cAAA,KACA,WAAA,OACA,mBAAA,yBACA,OAAA,IAAA,MAAA,KALF,yBASI,cAAA,Efq0EJ,qCAHA,qCAIA,qCAHA,qCACA,qCe70EA,qCAkBU,YAAA,OAlBV,kCA0BI,OAAA,Ef+zEJ,0DAHA,0DAIA,0DAHA,0DACA,0Dex1EA,0DAmCU,YAAA,Ef8zEV,yDAHA,yDAIA,yDAHA,yDACA,yDeh2EA,yDAuCU,aAAA,Efg0EV,yDev2EA,yDfw2EA,yDAFA,yDelzEU,cAAA,GEzNZ,SAIE,UAAA,EACA,QAAA,EACA,OAAA,EACA,OAAA,EAGF,OACE,QAAA,MACA,MAAA,KACA,QAAA,EACA,cAAA,KACA,UAAA,KACA,YAAA,QACA,MAAA,KACA,OAAA,EACA,cAAA,IAAA,MAAA,QAGF,MACE,QAAA,aACA,UAAA,KACA,cAAA,IACA,YAAA,IAUF,mBb6BE,mBAAA,WACG,gBAAA,WACK,WAAA,WarBR,mBAAA,KACA,gBAAA,KAAA,WAAA,KjBkgFF,qBiB9/EA,kBAEE,OAAA,IAAA,EAAA,EACA,WAAA,MACA,YAAA,OjBogFF,wCADA,qCADA,8BAFA,+BACA,2BiB3/EE,4BAGE,OAAA,YAIJ,iBACE,QAAA,MAIF,kBACE,QAAA,MACA,MAAA,KAIF,iBjBu/EA,aiBr/EE,OAAA,KjB0/EF,2BiBt/EA,uBjBq/EA,wBK/kFE,QAAA,IAAA,KAAA,yBACA,eAAA,KYgGF,OACE,QAAA,MACA,YAAA,IACA,UAAA,KACA,YAAA,WACA,MAAA,KA0BF,cACE,QAAA,MACA,MAAA,KACA,OAAA,KACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,WACA,MAAA,KACA,iBAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,cAAA,Ib3EA,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBAyHR,mBAAA,aAAA,YAAA,IAAA,CAAA,WAAA,YAAA,KACK,cAAA,aAAA,YAAA,IAAA,CAAA,WAAA,YAAA,KACG,mBAAA,aAAA,YAAA,IAAA,CAAA,mBAAA,YAAA,KAAA,WAAA,aAAA,YAAA,IAAA,CAAA,mBAAA,YAAA,KAAA,WAAA,aAAA,YAAA,IAAA,CAAA,WAAA,YAAA,KAAA,WAAA,aAAA,YAAA,IAAA,CAAA,WAAA,YAAA,IAAA,CAAA,mBAAA,YAAA,Kc1IR,oBACE,aAAA,QACA,QAAA,EdYF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,qBAiCR,gCACE,MAAA,KACA,QAAA,EAEF,oCAA0B,MAAA,KAC1B,yCAAgC,MAAA,Ka+ChC,0BACE,iBAAA,YACA,OAAA,EAQF,wBjBq+EF,wBACA,iCiBn+EI,iBAAA,KACA,QAAA,EAGF,wBjBo+EF,iCiBl+EI,OAAA,YAIF,sBACE,OAAA,KAcJ,qDAKI,8BjBm9EF,wCACA,+BAFA,8BiBj9EI,YAAA,KjB09EJ,iCAEA,2CACA,kCAFA,iCiBx9EE,0BjBq9EF,oCACA,2BAFA,0BiBl9EI,YAAA,KjB+9EJ,iCAEA,2CACA,kCAFA,iCiB79EE,0BjB09EF,oCACA,2BAFA,0BiBv9EI,YAAA,MAWN,YACE,cAAA,KjBy9EF,UiBj9EA,OAEE,SAAA,SACA,QAAA,MACA,WAAA,KACA,cAAA,KjBm9EF,yBiBh9EE,sBjBk9EF,mCADA,gCiB98EM,OAAA,YjBm9EN,gBiB99EA,aAgBI,WAAA,KACA,aAAA,KACA,cAAA,EACA,YAAA,IACA,OAAA,QjBm9EJ,+BACA,sCiBj9EA,yBjB+8EA,gCiB38EE,SAAA,SACA,WAAA,MACA,YAAA,MjBi9EF,oBiB98EA,cAEE,WAAA,KjBg9EF,iBiB58EA,cAEE,SAAA,SACA,QAAA,aACA,aAAA,KACA,cAAA,EACA,YAAA,IACA,eAAA,OACA,OAAA,QjB88EF,0BiB38EE,uBjB68EF,oCADA,iCiB18EI,OAAA,YjB+8EJ,kCiB58EA,4BAEE,WAAA,EACA,YAAA,KASF,qBACE,WAAA,KAEA,YAAA,IACA,eAAA,IAEA,cAAA,EAEA,8BjBm8EF,8BiBj8EI,cAAA,EACA,aAAA,EAaJ,UC3PE,OAAA,KACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IACA,cAAA,IAEA,gBACE,OAAA,KACA,YAAA,KlBsrFJ,0BkBnrFE,kBAEE,OAAA,KDiPJ,6BAEI,OAAA,KACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IACA,cAAA,IANJ,mCASI,OAAA,KACA,YAAA,KjBq8EJ,6CiB/8EA,qCAcI,OAAA,KAdJ,oCAiBI,OAAA,KACA,WAAA,KACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IAIJ,UCvRE,OAAA,KACA,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UACA,cAAA,IAEA,gBACE,OAAA,KACA,YAAA,KlB2tFJ,0BkBxtFE,kBAEE,OAAA,KD6QJ,6BAEI,OAAA,KACA,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UACA,cAAA,IANJ,mCASI,OAAA,KACA,YAAA,KjB88EJ,6CiBx9EA,qCAcI,OAAA,KAdJ,oCAiBI,OAAA,KACA,WAAA,KACA,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UASJ,cAEE,SAAA,SAFF,4BAMI,cAAA,OAIJ,uBACE,SAAA,SACA,IAAA,EACA,MAAA,EACA,QAAA,EACA,QAAA,MACA,MAAA,KACA,OAAA,KACA,YAAA,KACA,WAAA,OACA,eAAA,KjBo8EF,oDADA,uCiBj8EA,iCAGE,MAAA,KACA,OAAA,KACA,YAAA,KjBo8EF,oDADA,uCiBj8EA,iCAGE,MAAA,KACA,OAAA,KACA,YAAA,KjBq8EF,uBAEA,8BAJA,4BiB/7EA,yBjBg8EA,oBAEA,2BAGA,4BAEA,mCAHA,yBAEA,gCkBx1FI,MAAA,QDkZJ,2BC9YI,aAAA,QdiDF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBchDN,iCACE,aAAA,Qd8CJ,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,QACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,Qa4VV,gCCpYI,MAAA,QACA,iBAAA,QACA,aAAA,QDkYJ,oCC9XI,MAAA,QlB61FJ,uBAEA,8BAJA,4BiB19EA,yBjB29EA,oBAEA,2BAGA,4BAEA,mCAHA,yBAEA,gCkBt3FI,MAAA,QDqZJ,2BCjZI,aAAA,QdiDF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBchDN,iCACE,aAAA,Qd8CJ,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,QACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,Qa+VV,gCCvYI,MAAA,QACA,iBAAA,QACA,aAAA,QDqYJ,oCCjYI,MAAA,QlB23FJ,qBAEA,4BAJA,0BiBr/EA,uBjBs/EA,kBAEA,yBAGA,0BAEA,iCAHA,uBAEA,8BkBp5FI,MAAA,QDwZJ,yBCpZI,aAAA,QdiDF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBchDN,+BACE,aAAA,Qd8CJ,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,QACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,CAAA,EAAA,EAAA,IAAA,QakWV,8BC1YI,MAAA,QACA,iBAAA,QACA,aAAA,QDwYJ,kCCpYI,MAAA,QD2YF,2CACE,IAAA,KAEF,mDACE,IAAA,EAUJ,YACE,QAAA,MACA,WAAA,IACA,cAAA,KACA,MAAA,QAkBA,yBAAA,yBAGI,QAAA,aACA,cAAA,EACA,eAAA,OALJ,2BAUI,QAAA,aACA,MAAA,KACA,eAAA,OAZJ,kCAiBI,QAAA,aAjBJ,0BAqBI,QAAA,aACA,eAAA,OjBi/EJ,wCiBvgFA,6CjBsgFA,2CiB3+EM,MAAA,KA3BN,wCAiCI,MAAA,KAjCJ,4BAqCI,cAAA,EACA,eAAA,OjB4+EJ,uBiBlhFA,oBA6CI,QAAA,aACA,WAAA,EACA,cAAA,EACA,eAAA,OjBy+EJ,6BiBzhFA,0BAmDM,aAAA,EjB0+EN,4CiB7hFA,sCAwDI,SAAA,SACA,YAAA,EAzDJ,kDA8DI,IAAA,GjBw+EN,2BAEA,kCiB/9EA,wBjB89EA,+BiBr9EI,YAAA,IACA,WAAA,EACA,cAAA,EjB09EJ,2BiBr+EA,wBAiBI,WAAA,KAjBJ,6BJ9gBE,aAAA,MACA,YAAA,MIwiBA,yBAAA,gCAEI,YAAA,IACA,cAAA,EACA,WAAA,OA/BN,sDAwCI,MAAA,KAQA,yBAAA,+CAEI,YAAA,KACA,UAAA,MAKJ,yBAAA,+CAEI,YAAA,IACA,UAAA,ME9kBR,KACE,QAAA,aACA,cAAA,EACA,YAAA,IACA,WAAA,OACA,YAAA,OACA,eAAA,OACA,iBAAA,aAAA,aAAA,aACA,OAAA,QACA,iBAAA,KACA,OAAA,IAAA,MAAA,YCoCA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,WACA,cAAA,IhBqKA,oBAAA,KACG,iBAAA,KACC,gBAAA,KACI,YAAA,KJs1FV,kBAHA,kBACA,WACA,kBAHA,kBmB1hGI,WdrBF,QAAA,IAAA,KAAA,yBACA,eAAA,KLwjGF,WADA,WmB7hGE,WAGE,MAAA,KACA,gBAAA,KnB+hGJ,YmB5hGE,YAEE,iBAAA,KACA,QAAA,Ef2BF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBexBR,cnB4hGF,eACA,wBmB1hGI,OAAA,YE9CF,OAAA,kBACA,QAAA,IjBiEA,mBAAA,KACQ,WAAA,KefN,enB4hGJ,yBmB1hGM,eAAA,KASN,aC7DE,MAAA,KACA,iBAAA,KACA,aAAA,KpBqlGF,mBoBnlGE,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,mBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpBqlGJ,oBoBnlGE,oBpBolGF,mCoBjlGI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpB2lGJ,0BAHA,0BAHA,0BAKA,0BAHA,0BoBrlGI,0BpB0lGJ,yCAHA,yCAHA,yCoBjlGM,MAAA,KACA,iBAAA,QACA,aAAA,QpBgmGN,4BAHA,4BoBvlGI,4BpB2lGJ,6BAHA,6BAHA,6BAOA,sCAHA,sCAHA,sCoBnlGM,iBAAA,KACA,aAAA,KDuBN,oBClBI,MAAA,KACA,iBAAA,KDoBJ,aChEE,MAAA,KACA,iBAAA,QACA,aAAA,QpB0oGF,mBoBxoGE,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,mBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpB0oGJ,oBoBxoGE,oBpByoGF,mCoBtoGI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpBgpGJ,0BAHA,0BAHA,0BAKA,0BAHA,0BoB1oGI,0BpB+oGJ,yCAHA,yCAHA,yCoBtoGM,MAAA,KACA,iBAAA,QACA,aAAA,QpBqpGN,4BAHA,4BoB5oGI,4BpBgpGJ,6BAHA,6BAHA,6BAOA,sCAHA,sCAHA,sCoBxoGM,iBAAA,QACA,aAAA,QD0BN,oBCrBI,MAAA,QACA,iBAAA,KDwBJ,aCpEE,MAAA,KACA,iBAAA,QACA,aAAA,QpB+rGF,mBoB7rGE,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,mBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpB+rGJ,oBoB7rGE,oBpB8rGF,mCoB3rGI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpBqsGJ,0BAHA,0BAHA,0BAKA,0BAHA,0BoB/rGI,0BpBosGJ,yCAHA,yCAHA,yCoB3rGM,MAAA,KACA,iBAAA,QACA,aAAA,QpB0sGN,4BAHA,4BoBjsGI,4BpBqsGJ,6BAHA,6BAHA,6BAOA,sCAHA,sCAHA,sCoB7rGM,iBAAA,QACA,aAAA,QD8BN,oBCzBI,MAAA,QACA,iBAAA,KD4BJ,UCxEE,MAAA,KACA,iBAAA,QACA,aAAA,QpBovGF,gBoBlvGE,gBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,gBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpBovGJ,iBoBlvGE,iBpBmvGF,gCoBhvGI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpB0vGJ,uBAHA,uBAHA,uBAKA,uBAHA,uBoBpvGI,uBpByvGJ,sCAHA,sCAHA,sCoBhvGM,MAAA,KACA,iBAAA,QACA,aAAA,QpB+vGN,yBAHA,yBoBtvGI,yBpB0vGJ,0BAHA,0BAHA,0BAOA,mCAHA,mCAHA,mCoBlvGM,iBAAA,QACA,aAAA,QDkCN,iBC7BI,MAAA,QACA,iBAAA,KDgCJ,aC5EE,MAAA,KACA,iBAAA,QACA,aAAA,QpByyGF,mBoBvyGE,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,mBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpByyGJ,oBoBvyGE,oBpBwyGF,mCoBryGI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpB+yGJ,0BAHA,0BAHA,0BAKA,0BAHA,0BoBzyGI,0BpB8yGJ,yCAHA,yCAHA,yCoBryGM,MAAA,KACA,iBAAA,QACA,aAAA,QpBozGN,4BAHA,4BoB3yGI,4BpB+yGJ,6BAHA,6BAHA,6BAOA,sCAHA,sCAHA,sCoBvyGM,iBAAA,QACA,aAAA,QDsCN,oBCjCI,MAAA,QACA,iBAAA,KDoCJ,YChFE,MAAA,KACA,iBAAA,QACA,aAAA,QpB81GF,kBoB51GE,kBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAEF,kBACE,MAAA,KACA,iBAAA,QACA,aAAA,QpB81GJ,mBoB51GE,mBpB61GF,kCoB11GI,MAAA,KACA,iBAAA,QACA,iBAAA,KACA,aAAA,QpBo2GJ,yBAHA,yBAHA,yBAKA,yBAHA,yBoB91GI,yBpBm2GJ,wCAHA,wCAHA,wCoB11GM,MAAA,KACA,iBAAA,QACA,aAAA,QpBy2GN,2BAHA,2BoBh2GI,2BpBo2GJ,4BAHA,4BAHA,4BAOA,qCAHA,qCAHA,qCoB51GM,iBAAA,QACA,aAAA,QD0CN,mBCrCI,MAAA,QACA,iBAAA,KD6CJ,UACE,YAAA,IACA,MAAA,QACA,cAAA,EAEA,UnBwzGF,iBADA,iBAEA,oBACA,6BmBrzGI,iBAAA,YfnCF,mBAAA,KACQ,WAAA,KeqCR,UnB0zGF,iBADA,gBADA,gBmBpzGI,aAAA,YnB0zGJ,gBmBxzGE,gBAEE,MAAA,QACA,gBAAA,UACA,iBAAA,YnB2zGJ,0BmBvzGI,0BnBwzGJ,mCAFA,mCmBpzGM,MAAA,KACA,gBAAA,KnB0zGN,mBmBjzGA,QC9EE,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UACA,cAAA,IpBm4GF,mBmBpzGA,QClFE,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IACA,cAAA,IpB04GF,mBmBvzGA,QCtFE,QAAA,IAAA,IACA,UAAA,KACA,YAAA,IACA,cAAA,ID2FF,WACE,QAAA,MACA,MAAA,KAIF,sBACE,WAAA,InBuzGF,6BADA,4BmB/yGE,6BACE,MAAA,KG1JJ,MACE,QAAA,ElBoLA,mBAAA,QAAA,KAAA,OACK,cAAA,QAAA,KAAA,OACG,WAAA,QAAA,KAAA,OkBnLR,SACE,QAAA,EAIJ,UACE,QAAA,KAEA,aAAY,QAAA,MACZ,eAAY,QAAA,UACZ,kBAAY,QAAA,gBAGd,YACE,SAAA,SACA,OAAA,EACA,SAAA,OlBsKA,4BAAA,MAAA,CAAA,WACQ,uBAAA,MAAA,CAAA,WAAA,oBAAA,MAAA,CAAA,WAOR,4BAAA,KACQ,uBAAA,KAAA,oBAAA,KAGR,mCAAA,KACQ,8BAAA,KAAA,2BAAA,KmB5MV,OACE,QAAA,aACA,MAAA,EACA,OAAA,EACA,YAAA,IACA,eAAA,OACA,WAAA,IAAA,OACA,WAAA,IAAA,QACA,aAAA,IAAA,MAAA,YACA,YAAA,IAAA,MAAA,YvBu/GF,UuBn/GA,QAEE,SAAA,SAIF,uBACE,QAAA,EAIF,eACE,SAAA,SACA,IAAA,KACA,KAAA,EACA,QAAA,KACA,QAAA,KACA,MAAA,KACA,UAAA,MACA,QAAA,IAAA,EACA,OAAA,IAAA,EAAA,EACA,UAAA,KACA,WAAA,KACA,WAAA,KACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,KACA,OAAA,IAAA,MAAA,gBACA,cAAA,InBuBA,mBAAA,EAAA,IAAA,KAAA,iBACQ,WAAA,EAAA,IAAA,KAAA,iBmBlBR,0BACE,MAAA,EACA,KAAA,KAzBJ,wBCzBE,OAAA,IACA,OAAA,IAAA,EACA,SAAA,OACA,iBAAA,QDsBF,oBAmCI,QAAA,MACA,QAAA,IAAA,KACA,MAAA,KACA,YAAA,IACA,YAAA,WACA,MAAA,KACA,YAAA,OvB8+GJ,0BuB5+GI,0BAEE,MAAA,QACA,gBAAA,KACA,iBAAA,QAOJ,yBvBw+GF,+BADA,+BuBp+GI,MAAA,KACA,gBAAA,KACA,iBAAA,QACA,QAAA,EASF,2BvBi+GF,iCADA,iCuB79GI,MAAA,KvBk+GJ,iCuB99GE,iCAEE,gBAAA,KACA,OAAA,YACA,iBAAA,YACA,iBAAA,KEzGF,OAAA,0DF+GF,qBAGI,QAAA,MAHJ,QAQI,QAAA,EAQJ,qBACE,MAAA,EACA,KAAA,KAQF,oBACE,MAAA,KACA,KAAA,EAIF,iBACE,QAAA,MACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,WACA,MAAA,KACA,YAAA,OAIF,mBACE,SAAA,MACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,IAIF,2BACE,MAAA,EACA,KAAA,KAQF,evB+7GA,sCuB37GI,QAAA,GACA,WAAA,EACA,cAAA,IAAA,OACA,cAAA,IAAA,QAPJ,uBvBs8GA,8CuB37GI,IAAA,KACA,OAAA,KACA,cAAA,IASJ,yBACE,6BApEA,MAAA,EACA,KAAA,KAmEA,kCA1DA,MAAA,KACA,KAAA,GG1IF,W1BkoHA,oB0BhoHE,SAAA,SACA,QAAA,aACA,eAAA,O1BooHF,yB0BxoHA,gBAMI,SAAA,SACA,MAAA,K1B4oHJ,gCAFA,gCAFA,+BAFA,+BAKA,uBAFA,uBAFA,sB0BroHI,sBAIE,QAAA,EAMN,qB1BooHA,2BACA,2BACA,iC0BjoHI,YAAA,KAKJ,aACE,YAAA,KADF,kB1BmoHA,wBACA,0B0B7nHI,MAAA,KAPJ,kB1BwoHA,wBACA,0B0B7nHI,YAAA,IAIJ,yEACE,cAAA,EAIF,4BACE,YAAA,EACA,mECpDA,wBAAA,EACA,2BAAA,EDwDF,6C1B2nHA,8C2B5qHE,uBAAA,EACA,0BAAA,EDsDF,sBACE,MAAA,KAEF,8DACE,cAAA,EAEF,mE1B0nHA,oE2B/rHE,wBAAA,EACA,2BAAA,ED0EF,oECnEE,uBAAA,EACA,0BAAA,EDuEF,mC1BwnHA,iC0BtnHE,QAAA,EAiBF,iCACE,cAAA,IACA,aAAA,IAEF,oCACE,cAAA,KACA,aAAA,KAKF,iCtB/CE,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBsBkDR,0CtBnDA,mBAAA,KACQ,WAAA,KsByDV,YACE,YAAA,EAGF,eACE,aAAA,IAAA,IAAA,EACA,oBAAA,EAGF,uBACE,aAAA,EAAA,IAAA,IAOF,yB1B4lHA,+BACA,oC0BzlHI,QAAA,MACA,MAAA,KACA,MAAA,KACA,UAAA,KAPJ,oCAcM,MAAA,KAdN,8B1BumHA,oCACA,oCACA,0C0BnlHI,WAAA,KACA,YAAA,EAKF,4DACE,cAAA,EAEF,sDC7KA,uBAAA,IACA,wBAAA,IAOA,2BAAA,EACA,0BAAA,EDwKA,sDCjLA,uBAAA,EACA,wBAAA,EAOA,2BAAA,IACA,0BAAA,ID6KF,uEACE,cAAA,EAEF,4E1BqlHA,6E2BtwHE,2BAAA,EACA,0BAAA,EDsLF,6EC/LE,uBAAA,EACA,wBAAA,EDsMF,qBACE,QAAA,MACA,MAAA,KACA,aAAA,MACA,gBAAA,SAJF,0B1BslHA,gC0B/kHI,QAAA,WACA,MAAA,KACA,MAAA,GATJ,qCAYI,MAAA,KAZJ,+CAgBI,KAAA,K1BmlHJ,gD0BlkHA,6C1BmkHA,2DAFA,wD0B5jHM,SAAA,SACA,KAAA,cACA,eAAA,KE1ON,aACE,SAAA,SACA,QAAA,MACA,gBAAA,SAGA,0BACE,MAAA,KACA,cAAA,EACA,aAAA,EATJ,2BAeI,SAAA,SACA,QAAA,EAKA,MAAA,KAEA,MAAA,KACA,cAAA,EAEA,iCACE,QAAA,EAUN,8B5B2xHA,mCACA,sCkBpwHE,OAAA,KACA,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UACA,cAAA,IAEA,oClBswHF,yCACA,4CkBtwHI,OAAA,KACA,YAAA,KlB4wHJ,8CACA,mDACA,sDkB3wHE,sClBuwHF,2CACA,8CkBtwHI,OAAA,KUhCJ,8B5B6yHA,mCACA,sCkB3xHE,OAAA,KACA,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IACA,cAAA,IAEA,oClB6xHF,yCACA,4CkB7xHI,OAAA,KACA,YAAA,KlBmyHJ,8CACA,mDACA,sDkBlyHE,sClB8xHF,2CACA,8CkB7xHI,OAAA,KlBqyHJ,2B4B5zHA,mB5B2zHA,iB4BxzHE,QAAA,W5B8zHF,8D4B5zHE,sD5B2zHF,oD4B1zHI,cAAA,EAIJ,mB5B2zHA,iB4BzzHE,MAAA,GACA,YAAA,OACA,eAAA,OAKF,mBACE,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IACA,YAAA,EACA,MAAA,KACA,WAAA,OACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,cAAA,IAGA,4BACE,QAAA,IAAA,KACA,UAAA,KACA,cAAA,IAEF,4BACE,QAAA,KAAA,KACA,UAAA,KACA,cAAA,I5ByzHJ,wC4B70HA,qCA0BI,WAAA,EAKJ,uC5BkzHA,+BACA,kCACA,6CACA,8CAEA,6DADA,wE2B55HE,wBAAA,EACA,2BAAA,EC8GF,+BACE,aAAA,EAEF,sC5BmzHA,8BAKA,+DADA,oDAHA,iCACA,4CACA,6C2Bh6HE,uBAAA,EACA,0BAAA,ECkHF,8BACE,YAAA,EAKF,iBACE,SAAA,SAGA,UAAA,EACA,YAAA,OALF,sBAUI,SAAA,SAVJ,2BAYM,YAAA,K5BizHN,6BADA,4B4B7yHI,4BAGE,QAAA,EAKJ,kC5B0yHF,wC4BvyHM,aAAA,KAGJ,iC5BwyHF,uC4BryHM,QAAA,EACA,YAAA,KC/JN,KACE,aAAA,EACA,cAAA,EACA,WAAA,KAHF,QAOI,SAAA,SACA,QAAA,MARJ,UAWM,SAAA,SACA,QAAA,MACA,QAAA,KAAA,K7By8HN,gB6Bx8HM,gBAEE,gBAAA,KACA,iBAAA,KAKJ,mBACE,MAAA,K7Bu8HN,yB6Br8HM,yBAEE,MAAA,KACA,gBAAA,KACA,OAAA,YACA,iBAAA,YAOJ,a7Bi8HJ,mBADA,mB6B77HM,iBAAA,KACA,aAAA,QAzCN,kBLLE,OAAA,IACA,OAAA,IAAA,EACA,SAAA,OACA,iBAAA,QKEF,cA0DI,UAAA,KASJ,UACE,cAAA,IAAA,MAAA,KADF,aAGI,MAAA,KAEA,cAAA,KALJ,eASM,aAAA,IACA,YAAA,WACA,OAAA,IAAA,MAAA,YACA,cAAA,IAAA,IAAA,EAAA,EACA,qBACE,aAAA,KAAA,KAAA,KAMF,sB7B86HN,4BADA,4B6B16HQ,MAAA,KACA,OAAA,QACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,oBAAA,YAKN,wBAqDA,MAAA,KA8BA,cAAA,EAnFA,2BAwDE,MAAA,KAxDF,6BA0DI,cAAA,IACA,WAAA,OA3DJ,iDAgEE,IAAA,KACA,KAAA,KAGF,yBAAA,2BAEI,QAAA,WACA,MAAA,GAHJ,6BAKM,cAAA,GAzEN,6BAuFE,aAAA,EACA,cAAA,IAxFF,kC7Bu8HF,wCADA,wC6Bx2HI,OAAA,IAAA,MAAA,KAGF,yBAAA,6BAEI,cAAA,IAAA,MAAA,KACA,cAAA,IAAA,IAAA,EAAA,EAHJ,kC7Bg3HA,wCADA,wC6Bv2HI,oBAAA,MAhGN,cAEI,MAAA,KAFJ,gBAMM,cAAA,IANN,iBASM,YAAA,IAKA,uB7By8HN,6BADA,6B6Br8HQ,MAAA,KACA,iBAAA,QAQR,gBAEI,MAAA,KAFJ,mBAIM,WAAA,IACA,YAAA,EAYN,eACE,MAAA,KADF,kBAII,MAAA,KAJJ,oBAMM,cAAA,IACA,WAAA,OAPN,wCAYI,IAAA,KACA,KAAA,KAGF,yBAAA,kBAEI,QAAA,WACA,MAAA,GAHJ,oBAKM,cAAA,GASR,oBACE,cAAA,EADF,yBAKI,aAAA,EACA,cAAA,IANJ,8B7By7HA,oCADA,oC6B56HI,OAAA,IAAA,MAAA,KAGF,yBAAA,yBAEI,cAAA,IAAA,MAAA,KACA,cAAA,IAAA,IAAA,EAAA,EAHJ,8B7Bo7HA,oCADA,oC6B36HI,oBAAA,MAUN,uBAEI,QAAA,KAFJ,qBAKI,QAAA,MASJ,yBAEE,WAAA,KF7OA,uBAAA,EACA,wBAAA,EGQF,QACE,SAAA,SACA,WAAA,KACA,cAAA,KACA,OAAA,IAAA,MAAA,YAKA,yBAAA,QACE,cAAA,KAaF,yBAAA,eACE,MAAA,MAeJ,iBACE,cAAA,KACA,aAAA,KACA,WAAA,QACA,WAAA,IAAA,MAAA,YACA,mBAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,WAAA,MAAA,EAAA,IAAA,EAAA,qBAEA,2BAAA,MAEA,oBACE,WAAA,KAGF,yBAAA,iBACE,MAAA,KACA,WAAA,EACA,mBAAA,KAAA,WAAA,KAEA,0BACE,QAAA,gBACA,OAAA,eACA,eAAA,EACA,SAAA,kBAGF,oBACE,WAAA,Q9BknIJ,sC8B7mIE,mC9B4mIF,oC8BzmII,cAAA,EACA,aAAA,G9B+mIN,qB8B1mIA,kBAWE,SAAA,MACA,MAAA,EACA,KAAA,EACA,QAAA,K9BmmIF,sC8BjnIA,mCAGI,WAAA,MAEA,4D9BinIF,sC8BjnIE,mCACE,WAAA,OAWJ,yB9B2mIA,qB8B3mIA,kBACE,cAAA,GAIJ,kBACE,IAAA,EACA,aAAA,EAAA,EAAA,IAEF,qBACE,OAAA,EACA,cAAA,EACA,aAAA,IAAA,EAAA,E9B+mIF,kCAFA,gCACA,4B8BtmIA,0BAII,aAAA,MACA,YAAA,MAEA,yB9BwmIF,kCAFA,gCACA,4B8BvmIE,0BACE,aAAA,EACA,YAAA,GAaN,mBACE,QAAA,KACA,aAAA,EAAA,EAAA,IAEA,yBAAA,mBACE,cAAA,GAOJ,cACE,MAAA,KACA,OAAA,KACA,QAAA,KAAA,KACA,UAAA,KACA,YAAA,K9B8lIF,oB8B5lIE,oBAEE,gBAAA,KATJ,kBAaI,QAAA,MAGF,yBACE,iC9B0lIF,uC8BxlII,YAAA,OAWN,eACE,SAAA,SACA,MAAA,MACA,QAAA,IAAA,KACA,aAAA,KC9LA,WAAA,IACA,cAAA,ID+LA,iBAAA,YACA,iBAAA,KACA,OAAA,IAAA,MAAA,YACA,cAAA,IAIA,qBACE,QAAA,EAdJ,yBAmBI,QAAA,MACA,MAAA,KACA,OAAA,IACA,cAAA,IAtBJ,mCAyBI,WAAA,IAGF,yBAAA,eACE,QAAA,MAUJ,YACE,OAAA,MAAA,MADF,iBAII,YAAA,KACA,eAAA,KACA,YAAA,KAGF,yBAAA,iCAGI,SAAA,OACA,MAAA,KACA,MAAA,KACA,WAAA,EACA,iBAAA,YACA,OAAA,EACA,mBAAA,KAAA,WAAA,K9BykIJ,kD8BllIA,sCAYM,QAAA,IAAA,KAAA,IAAA,KAZN,sCAeM,YAAA,K9B0kIN,4C8BzkIM,4CAEE,iBAAA,MAOR,yBAAA,YACE,MAAA,KACA,OAAA,EAFF,eAKI,MAAA,KALJ,iBAOM,YAAA,KACA,eAAA,MAYR,aACE,QAAA,KAAA,KACA,aAAA,MACA,YAAA,MACA,WAAA,IAAA,MAAA,YACA,cAAA,IAAA,MAAA,Y1B5NA,mBAAA,MAAA,EAAA,IAAA,EAAA,oBAAA,CAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,oBAAA,CAAA,EAAA,IAAA,EAAA,qB2BjER,WAAA,IACA,cAAA,Id6cA,yBAAA,yBAGI,QAAA,aACA,cAAA,EACA,eAAA,OALJ,2BAUI,QAAA,aACA,MAAA,KACA,eAAA,OAZJ,kCAiBI,QAAA,aAjBJ,0BAqBI,QAAA,aACA,eAAA,OjB+4HJ,wCiBr6HA,6CjBo6HA,2CiBz4HM,MAAA,KA3BN,wCAiCI,MAAA,KAjCJ,4BAqCI,cAAA,EACA,eAAA,OjB04HJ,uBiBh7HA,oBA6CI,QAAA,aACA,WAAA,EACA,cAAA,EACA,eAAA,OjBu4HJ,6BiBv7HA,0BAmDM,aAAA,EjBw4HN,4CiB37HA,sCAwDI,SAAA,SACA,YAAA,EAzDJ,kDA8DI,IAAA,GaxOF,yBAAA,yBACE,cAAA,IAEA,oCACE,cAAA,GASN,yBAAA,aACE,MAAA,KACA,YAAA,EACA,eAAA,EACA,aAAA,EACA,YAAA,EACA,OAAA,E1BvPF,mBAAA,KACQ,WAAA,M0B+PV,8BACE,WAAA,EHpUA,uBAAA,EACA,wBAAA,EGuUF,mDACE,cAAA,EHzUA,uBAAA,IACA,wBAAA,IAOA,2BAAA,EACA,0BAAA,EG0UF,YChVE,WAAA,IACA,cAAA,IDkVA,mBCnVA,WAAA,KACA,cAAA,KDqVA,mBCtVA,WAAA,KACA,cAAA,KD+VF,aChWE,WAAA,KACA,cAAA,KDkWA,yBAAA,aACE,MAAA,KACA,aAAA,KACA,YAAA,MAaJ,yBACE,aEtWA,MAAA,eFuWA,cE1WA,MAAA,gBF4WE,aAAA,MAFF,4BAKI,aAAA,GAUN,gBACE,iBAAA,QACA,aAAA,QAFF,8BAKI,MAAA,K9BmlIJ,oC8BllII,oCAEE,MAAA,QACA,iBAAA,YATN,6BAcI,MAAA,KAdJ,iCAmBM,MAAA,K9BglIN,uC8B9kIM,uCAEE,MAAA,KACA,iBAAA,YAIF,sC9B6kIN,4CADA,4C8BzkIQ,MAAA,KACA,iBAAA,QAIF,wC9B2kIN,8CADA,8C8BvkIQ,MAAA,KACA,iBAAA,YAOF,oC9BskIN,0CADA,0C8BlkIQ,MAAA,KACA,iBAAA,QAIJ,yBAAA,sDAIM,MAAA,K9BmkIR,4D8BlkIQ,4DAEE,MAAA,KACA,iBAAA,YAIF,2D9BikIR,iEADA,iE8B7jIU,MAAA,KACA,iBAAA,QAIF,6D9B+jIR,mEADA,mE8B3jIU,MAAA,KACA,iBAAA,aA/EZ,+BAuFI,aAAA,K9B4jIJ,qC8B3jII,qCAEE,iBAAA,KA1FN,yCA6FM,iBAAA,KA7FN,iC9B0pIA,6B8BvjII,aAAA,QAnGJ,6BA4GI,MAAA,KACA,mCACE,MAAA,KA9GN,0BAmHI,MAAA,K9BojIJ,gC8BnjII,gCAEE,MAAA,K9BsjIN,0C8BljIM,0C9BmjIN,mDAFA,mD8B/iIQ,MAAA,KAQR,gBACE,iBAAA,KACA,aAAA,QAFF,8BAKI,MAAA,Q9B+iIJ,oC8B9iII,oCAEE,MAAA,KACA,iBAAA,YATN,6BAcI,MAAA,QAdJ,iCAmBM,MAAA,Q9B4iIN,uC8B1iIM,uCAEE,MAAA,KACA,iBAAA,YAIF,sC9ByiIN,4CADA,4C8BriIQ,MAAA,KACA,iBAAA,QAIF,wC9BuiIN,8CADA,8C8BniIQ,MAAA,KACA,iBAAA,YAMF,oC9BmiIN,0CADA,0C8B/hIQ,MAAA,KACA,iBAAA,QAIJ,yBAAA,kEAIM,aAAA,QAJN,0DAOM,iBAAA,QAPN,sDAUM,MAAA,Q9BgiIR,4D8B/hIQ,4DAEE,MAAA,KACA,iBAAA,YAIF,2D9B8hIR,iEADA,iE8B1hIU,MAAA,KACA,iBAAA,QAIF,6D9B4hIR,mEADA,mE8BxhIU,MAAA,KACA,iBAAA,aApFZ,+BA6FI,aAAA,K9BwhIJ,qC8BvhII,qCAEE,iBAAA,KAhGN,yCAmGM,iBAAA,KAnGN,iC9B4nIA,6B8BnhII,aAAA,QAzGJ,6BA6GI,MAAA,QACA,mCACE,MAAA,KA/GN,0BAoHI,MAAA,Q9BqhIJ,gC8BphII,gCAEE,MAAA,K9BuhIN,0C8BnhIM,0C9BohIN,mDAFA,mD8BhhIQ,MAAA,KGtoBR,YACE,QAAA,IAAA,KACA,cAAA,KACA,WAAA,KACA,iBAAA,QACA,cAAA,IALF,eAQI,QAAA,aARJ,yBAWM,QAAA,EAAA,IACA,MAAA,KACA,QAAA,SAbN,oBAkBI,MAAA,KCpBJ,YACE,QAAA,aACA,aAAA,EACA,OAAA,KAAA,EACA,cAAA,IAJF,eAOI,QAAA,OAPJ,iBlCyrJA,oBkC/qJM,SAAA,SACA,MAAA,KACA,QAAA,IAAA,KACA,YAAA,KACA,YAAA,WACA,MAAA,QACA,gBAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,KlCorJN,uBkClrJM,uBlCmrJN,0BAFA,0BkC/qJQ,QAAA,EACA,MAAA,QACA,iBAAA,KACA,aAAA,KAGJ,6BlCkrJJ,gCkC/qJQ,YAAA,EPnBN,uBAAA,IACA,0BAAA,IOsBE,4BlCirJJ,+B2BhtJE,wBAAA,IACA,2BAAA,IOwCE,sBlC+qJJ,4BAFA,4BADA,yBAIA,+BAFA,+BkC3qJM,QAAA,EACA,MAAA,KACA,OAAA,QACA,iBAAA,QACA,aAAA,QlCmrJN,wBAEA,8BADA,8BkCxuJA,2BlCsuJA,iCADA,iCkCtqJM,MAAA,KACA,OAAA,YACA,iBAAA,KACA,aAAA,KASN,oBlCqqJA,uBmC7uJM,QAAA,KAAA,KACA,UAAA,KACA,YAAA,UAEF,gCnC+uJJ,mC2B1uJE,uBAAA,IACA,0BAAA,IQAE,+BnC8uJJ,kC2BvvJE,wBAAA,IACA,2BAAA,IO2EF,oBlCgrJA,uBmC7vJM,QAAA,IAAA,KACA,UAAA,KACA,YAAA,IAEF,gCnC+vJJ,mC2B1vJE,uBAAA,IACA,0BAAA,IQAE,+BnC8vJJ,kC2BvwJE,wBAAA,IACA,2BAAA,ISHF,OACE,aAAA,EACA,OAAA,KAAA,EACA,WAAA,OACA,WAAA,KAJF,UAOI,QAAA,OAPJ,YpCuxJA,eoC7wJM,QAAA,aACA,QAAA,IAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,cAAA,KpCixJN,kBoC/xJA,kBAmBM,gBAAA,KACA,iBAAA,KApBN,epCoyJA,kBoCzwJM,MAAA,MA3BN,mBpCwyJA,sBoCtwJM,MAAA,KAlCN,mBpC6yJA,yBADA,yBAEA,sBoCnwJM,MAAA,KACA,OAAA,YACA,iBAAA,KC9CN,OACE,QAAA,OACA,QAAA,KAAA,KAAA,KACA,UAAA,IACA,YAAA,IACA,YAAA,EACA,MAAA,KACA,WAAA,OACA,YAAA,OACA,eAAA,SACA,cAAA,MrCuzJF,cqCnzJI,cAEE,MAAA,KACA,gBAAA,KACA,OAAA,QAKJ,aACE,QAAA,KAIF,YACE,SAAA,SACA,IAAA,KAOJ,eCtCE,iBAAA,KtCk1JF,2BsC/0JI,2BAEE,iBAAA,QDqCN,eC1CE,iBAAA,QtCy1JF,2BsCt1JI,2BAEE,iBAAA,QDyCN,eC9CE,iBAAA,QtCg2JF,2BsC71JI,2BAEE,iBAAA,QD6CN,YClDE,iBAAA,QtCu2JF,wBsCp2JI,wBAEE,iBAAA,QDiDN,eCtDE,iBAAA,QtC82JF,2BsC32JI,2BAEE,iBAAA,QDqDN,cC1DE,iBAAA,QtCq3JF,0BsCl3JI,0BAEE,iBAAA,QCFN,OACE,QAAA,aACA,UAAA,KACA,QAAA,IAAA,IACA,UAAA,KACA,YAAA,IACA,YAAA,EACA,MAAA,KACA,WAAA,OACA,YAAA,OACA,eAAA,OACA,iBAAA,KACA,cAAA,KAGA,aACE,QAAA,KAIF,YACE,SAAA,SACA,IAAA,KvCq3JJ,0BuCl3JE,eAEE,IAAA,EACA,QAAA,IAAA,IvCo3JJ,cuC/2JI,cAEE,MAAA,KACA,gBAAA,KACA,OAAA,QAKJ,+BvC42JF,4BuC12JI,MAAA,QACA,iBAAA,KAGF,wBACE,MAAA,MAGF,+BACE,aAAA,IAGF,uBACE,YAAA,IC1DJ,WACE,YAAA,KACA,eAAA,KACA,cAAA,KACA,MAAA,QACA,iBAAA,KxCu6JF,ewC56JA,cASI,MAAA,QATJ,aAaI,cAAA,KACA,UAAA,KACA,YAAA,IAfJ,cAmBI,iBAAA,QAGF,sBxCk6JF,4BwCh6JI,cAAA,KACA,aAAA,KACA,cAAA,IA1BJ,sBA8BI,UAAA,KAGF,oCAAA,WACE,YAAA,KACA,eAAA,KAEA,sBxCi6JF,4BwC/5JI,cAAA,KACA,aAAA,KxCm6JJ,ewC16JA,cAYI,UAAA,MC1CN,WACE,QAAA,MACA,QAAA,IACA,cAAA,KACA,YAAA,WACA,iBAAA,KACA,OAAA,IAAA,MAAA,KACA,cAAA,IrCiLA,mBAAA,OAAA,IAAA,YACK,cAAA,OAAA,IAAA,YACG,WAAA,OAAA,IAAA,YJ+xJV,iByCz9JA,eAaI,aAAA,KACA,YAAA,KzCi9JJ,mBADA,kByC58JE,kBAGE,aAAA,QArBJ,oBA0BI,QAAA,IACA,MAAA,KC3BJ,OACE,QAAA,KACA,cAAA,KACA,OAAA,IAAA,MAAA,YACA,cAAA,IAJF,UAQI,WAAA,EACA,MAAA,QATJ,mBAcI,YAAA,IAdJ,S1Co/JA,U0Ch+JI,cAAA,EApBJ,WAwBI,WAAA,IASJ,mB1C09JA,mB0Cx9JE,cAAA,KAFF,0B1C89JA,0B0Cx9JI,SAAA,SACA,IAAA,KACA,MAAA,MACA,MAAA,QAQJ,eCvDE,MAAA,QACA,iBAAA,QACA,aAAA,QDqDF,kBClDI,iBAAA,QDkDJ,2BC9CI,MAAA,QDkDJ,YC3DE,MAAA,QACA,iBAAA,QACA,aAAA,QDyDF,eCtDI,iBAAA,QDsDJ,wBClDI,MAAA,QDsDJ,eC/DE,MAAA,QACA,iBAAA,QACA,aAAA,QD6DF,kBC1DI,iBAAA,QD0DJ,2BCtDI,MAAA,QD0DJ,cCnEE,MAAA,QACA,iBAAA,QACA,aAAA,QDiEF,iBC9DI,iBAAA,QD8DJ,0BC1DI,MAAA,QCDJ,wCACE,KAAQ,oBAAA,KAAA,EACR,GAAQ,oBAAA,EAAA,GAIV,mCACE,KAAQ,oBAAA,KAAA,EACR,GAAQ,oBAAA,EAAA,GAFV,gCACE,KAAQ,oBAAA,KAAA,EACR,GAAQ,oBAAA,EAAA,GAQV,UACE,OAAA,KACA,cAAA,KACA,SAAA,OACA,iBAAA,QACA,cAAA,IxCsCA,mBAAA,MAAA,EAAA,IAAA,IAAA,eACQ,WAAA,MAAA,EAAA,IAAA,IAAA,ewClCV,cACE,MAAA,KACA,MAAA,GACA,OAAA,KACA,UAAA,KACA,YAAA,KACA,MAAA,KACA,WAAA,OACA,iBAAA,QxCyBA,mBAAA,MAAA,EAAA,KAAA,EAAA,gBACQ,WAAA,MAAA,EAAA,KAAA,EAAA,gBAyHR,mBAAA,MAAA,IAAA,KACK,cAAA,MAAA,IAAA,KACG,WAAA,MAAA,IAAA,KJw6JV,sB4CnjKA,gCCDI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKDEF,wBAAA,KAAA,KAAA,gBAAA,KAAA,K5CwjKF,qB4CjjKA,+BxC5CE,kBAAA,qBAAA,GAAA,OAAA,SACK,aAAA,qBAAA,GAAA,OAAA,SACG,UAAA,qBAAA,GAAA,OAAA,SwCmDV,sBEvEE,iBAAA,QAGA,wCDgDE,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKDsBJ,mBE3EE,iBAAA,QAGA,qCDgDE,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKD0BJ,sBE/EE,iBAAA,QAGA,wCDgDE,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKD8BJ,qBEnFE,iBAAA,QAGA,uCDgDE,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKExDJ,OAEE,WAAA,KAEA,mBACE,WAAA,EAIJ,O/CqpKA,Y+CnpKE,SAAA,OACA,KAAA,EAGF,YACE,MAAA,QAGF,cACE,QAAA,MAGA,4BACE,UAAA,KAIJ,a/CgpKA,mB+C9oKE,aAAA,KAGF,Y/C+oKA,kB+C7oKE,cAAA,K/CkpKF,Y+C/oKA,Y/C8oKA,a+C3oKE,QAAA,WACA,eAAA,IAGF,cACE,eAAA,OAGF,cACE,eAAA,OAIF,eACE,WAAA,EACA,cAAA,IAMF,YACE,aAAA,EACA,WAAA,KCrDF,YAEE,aAAA,EACA,cAAA,KAQF,iBACE,SAAA,SACA,QAAA,MACA,QAAA,KAAA,KAEA,cAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,KAGA,6BrB7BA,uBAAA,IACA,wBAAA,IqB+BA,4BACE,cAAA,ErBzBF,2BAAA,IACA,0BAAA,IqB6BA,0BhDqrKF,gCADA,gCgDjrKI,MAAA,KACA,OAAA,YACA,iBAAA,KALF,mDhD4rKF,yDADA,yDgDlrKM,MAAA,QATJ,gDhDisKF,sDADA,sDgDprKM,MAAA,KAKJ,wBhDqrKF,8BADA,8BgDjrKI,QAAA,EACA,MAAA,KACA,iBAAA,QACA,aAAA,QANF,iDhDisKF,wDAHA,uDADA,uDAMA,8DAHA,6DAJA,uDAMA,8DAHA,6DgDnrKM,MAAA,QAZJ,8ChDwsKF,oDADA,oDgDxrKM,MAAA,QAWN,kBhDkrKA,uBgDhrKE,MAAA,KAFF,2ChDsrKA,gDgDjrKI,MAAA,KhDsrKJ,wBgDlrKE,wBhDmrKF,6BAFA,6BgD/qKI,MAAA,KACA,gBAAA,KACA,iBAAA,QAIJ,uBACE,MAAA,KACA,WAAA,KnCvGD,yBoCIG,MAAA,QACA,iBAAA,QAEA,0BjDuxKJ,+BiDrxKM,MAAA,QAFF,mDjD2xKJ,wDiDtxKQ,MAAA,QjD2xKR,gCiDxxKM,gCjDyxKN,qCAFA,qCiDrxKQ,MAAA,QACA,iBAAA,QAEF,iCjD4xKN,uCAFA,uCADA,sCAIA,4CAFA,4CiDxxKQ,MAAA,KACA,iBAAA,QACA,aAAA,QpCzBP,sBoCIG,MAAA,QACA,iBAAA,QAEA,uBjDozKJ,4BiDlzKM,MAAA,QAFF,gDjDwzKJ,qDiDnzKQ,MAAA,QjDwzKR,6BiDrzKM,6BjDszKN,kCAFA,kCiDlzKQ,MAAA,QACA,iBAAA,QAEF,8BjDyzKN,oCAFA,oCADA,mCAIA,yCAFA,yCiDrzKQ,MAAA,KACA,iBAAA,QACA,aAAA,QpCzBP,yBoCIG,MAAA,QACA,iBAAA,QAEA,0BjDi1KJ,+BiD/0KM,MAAA,QAFF,mDjDq1KJ,wDiDh1KQ,MAAA,QjDq1KR,gCiDl1KM,gCjDm1KN,qCAFA,qCiD/0KQ,MAAA,QACA,iBAAA,QAEF,iCjDs1KN,uCAFA,uCADA,sCAIA,4CAFA,4CiDl1KQ,MAAA,KACA,iBAAA,QACA,aAAA,QpCzBP,wBoCIG,MAAA,QACA,iBAAA,QAEA,yBjD82KJ,8BiD52KM,MAAA,QAFF,kDjDk3KJ,uDiD72KQ,MAAA,QjDk3KR,+BiD/2KM,+BjDg3KN,oCAFA,oCiD52KQ,MAAA,QACA,iBAAA,QAEF,gCjDm3KN,sCAFA,sCADA,qCAIA,2CAFA,2CiD/2KQ,MAAA,KACA,iBAAA,QACA,aAAA,QDiGR,yBACE,WAAA,EACA,cAAA,IAEF,sBACE,cAAA,EACA,YAAA,IExHF,OACE,cAAA,KACA,iBAAA,KACA,OAAA,IAAA,MAAA,YACA,cAAA,I9C0DA,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gB8CtDV,YACE,QAAA,KAKF,eACE,QAAA,KAAA,KACA,cAAA,IAAA,MAAA,YvBtBA,uBAAA,IACA,wBAAA,IuBmBF,0CAMI,MAAA,QAKJ,aACE,WAAA,EACA,cAAA,EACA,UAAA,KACA,MAAA,QlD24KF,oBAEA,sBkDj5KA,elD84KA,mBAEA,qBkDr4KI,MAAA,QAKJ,cACE,QAAA,KAAA,KACA,iBAAA,QACA,WAAA,IAAA,MAAA,KvB1CA,2BAAA,IACA,0BAAA,IuBmDF,mBlD+3KA,mCkD53KI,cAAA,EAHJ,oClDm4KA,oDkD73KM,aAAA,IAAA,EACA,cAAA,EAIF,4DlD63KJ,4EkD33KQ,WAAA,EvBzEN,uBAAA,IACA,wBAAA,IuB8EE,0DlD23KJ,0EkDz3KQ,cAAA,EvBzEN,2BAAA,IACA,0BAAA,IuBmDF,+EvB5DE,uBAAA,EACA,wBAAA,EuB4FF,wDAEI,iBAAA,EAGJ,0BACE,iBAAA,ElDw3KF,8BkDh3KA,clD+2KA,gCkD32KI,cAAA,ElDi3KJ,sCkDr3KA,sBlDo3KA,wCkD72KM,cAAA,KACA,aAAA,KlDk3KN,wDkD13KA,0BvB3GE,uBAAA,IACA,wBAAA,I3B2+KF,yFAFA,yFACA,2DkDh4KA,2DAmBQ,uBAAA,IACA,wBAAA,IlDo3KR,wGAIA,wGANA,wGAIA,wGAHA,0EAIA,0EkD34KA,0ElDy4KA,0EkDj3KU,uBAAA,IlD03KV,uGAIA,uGANA,uGAIA,uGAHA,yEAIA,yEkDr5KA,yElDm5KA,yEkDv3KU,wBAAA,IlD83KV,sDkD15KA,yBvBnGE,2BAAA,IACA,0BAAA,I3BigLF,qFAEA,qFkDj6KA,wDlDg6KA,wDkDv3KQ,2BAAA,IACA,0BAAA,IlD43KR,oGAIA,oGAFA,oGAIA,oGkD56KA,uElDy6KA,uEAFA,uEAIA,uEkD73KU,0BAAA,IlDk4KV,mGAIA,mGAFA,mGAIA,mGkDt7KA,sElDm7KA,sEAFA,sEAIA,sEkDn4KU,2BAAA,IAlDV,0BlD07KA,qCACA,0BACA,qCkDj4KI,WAAA,IAAA,MAAA,KlDq4KJ,kDkDh8KA,kDA+DI,WAAA,EA/DJ,uBlDo8KA,yCkDj4KI,OAAA,ElD44KJ,+CANA,+CAQA,+CANA,+CAEA,+CkD78KA,+ClDg9KA,iEANA,iEAQA,iEANA,iEAEA,iEANA,iEkD93KU,YAAA,ElDm5KV,8CANA,8CAQA,8CANA,8CAEA,8CkD39KA,8ClD89KA,gEANA,gEAQA,gEANA,gEAEA,gEANA,gEkDx4KU,aAAA,ElDu5KV,+CAIA,+CkDz+KA,+ClDu+KA,+CADA,iEAIA,iEANA,iEAIA,iEkDj5KU,cAAA,EAvFV,8ClDi/KA,8CAFA,8CAIA,8CALA,gEAIA,gEAFA,gEAIA,gEkDp5KU,cAAA,EAhGV,yBAsGI,cAAA,EACA,OAAA,EAUJ,aACE,cAAA,KADF,oBAKI,cAAA,EACA,cAAA,IANJ,2BASM,WAAA,IATN,4BAcI,cAAA,ElD04KJ,wDkDx5KA,wDAkBM,WAAA,IAAA,MAAA,KAlBN,2BAuBI,WAAA,EAvBJ,uDAyBM,cAAA,IAAA,MAAA,KAON,eC5PE,aAAA,KAEA,8BACE,MAAA,KACA,iBAAA,QACA,aAAA,KAHF,0DAMI,iBAAA,KANJ,qCASI,MAAA,QACA,iBAAA,KAGJ,yDAEI,oBAAA,KD8ON,eC/PE,aAAA,QAEA,8BACE,MAAA,KACA,iBAAA,QACA,aAAA,QAHF,0DAMI,iBAAA,QANJ,qCASI,MAAA,QACA,iBAAA,KAGJ,yDAEI,oBAAA,QDiPN,eClQE,aAAA,QAEA,8BACE,MAAA,QACA,iBAAA,QACA,aAAA,QAHF,0DAMI,iBAAA,QANJ,qCASI,MAAA,QACA,iBAAA,QAGJ,yDAEI,oBAAA,QDoPN,YCrQE,aAAA,QAEA,2BACE,MAAA,QACA,iBAAA,QACA,aAAA,QAHF,uDAMI,iBAAA,QANJ,kCASI,MAAA,QACA,iBAAA,QAGJ,sDAEI,oBAAA,QDuPN,eCxQE,aAAA,QAEA,8BACE,MAAA,QACA,iBAAA,QACA,aAAA,QAHF,0DAMI,iBAAA,QANJ,qCASI,MAAA,QACA,iBAAA,QAGJ,yDAEI,oBAAA,QD0PN,cC3QE,aAAA,QAEA,6BACE,MAAA,QACA,iBAAA,QACA,aAAA,QAHF,yDAMI,iBAAA,QANJ,oCASI,MAAA,QACA,iBAAA,QAGJ,wDAEI,oBAAA,QChBN,kBACE,SAAA,SACA,QAAA,MACA,OAAA,EACA,QAAA,EACA,SAAA,OALF,yCpDivLA,wBADA,yBAEA,yBACA,wBoDvuLI,SAAA,SACA,IAAA,EACA,OAAA,EACA,KAAA,EACA,MAAA,KACA,OAAA,KACA,OAAA,EAKJ,wBACE,eAAA,OAIF,uBACE,eAAA,IC3BF,MACE,WAAA,KACA,QAAA,KACA,cAAA,KACA,iBAAA,QACA,OAAA,IAAA,MAAA,QACA,cAAA,IjD0DA,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBiDjEV,iBASI,aAAA,KACA,aAAA,gBAKJ,SACE,QAAA,KACA,cAAA,IAEF,SACE,QAAA,IACA,cAAA,ICpBF,OACE,MAAA,MACA,UAAA,KACA,YAAA,IACA,YAAA,EACA,MAAA,KACA,YAAA,EAAA,IAAA,EAAA,KjCTA,OAAA,kBACA,QAAA,GrBkyLF,asDvxLE,aAEE,MAAA,KACA,gBAAA,KACA,OAAA,QjChBF,OAAA,kBACA,QAAA,GiCuBA,aACE,QAAA,EACA,OAAA,QACA,WAAA,IACA,OAAA,EACA,mBAAA,KACA,gBAAA,KAAA,WAAA,KCxBJ,YACE,SAAA,OAIF,OACE,SAAA,MACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,KACA,QAAA,KACA,SAAA,OACA,2BAAA,MAIA,QAAA,EAGA,0BnDiHA,kBAAA,kBACI,cAAA,kBACC,aAAA,kBACG,UAAA,kBAkER,mBAAA,kBAAA,IAAA,SAEK,cAAA,aAAA,IAAA,SACG,WAAA,kBAAA,IAAA,SAAA,WAAA,UAAA,IAAA,SAAA,WAAA,UAAA,IAAA,QAAA,CAAA,kBAAA,IAAA,QAAA,CAAA,aAAA,IAAA,SmDrLR,wBnD6GA,kBAAA,eACI,cAAA,eACC,aAAA,eACG,UAAA,emD9GV,mBACE,WAAA,OACA,WAAA,KAIF,cACE,SAAA,SACA,MAAA,KACA,OAAA,KAIF,eACE,SAAA,SACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,KACA,OAAA,IAAA,MAAA,eACA,cAAA,InDcA,mBAAA,EAAA,IAAA,IAAA,eACQ,WAAA,EAAA,IAAA,IAAA,emDZR,QAAA,EAIF,gBACE,SAAA,MACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,KACA,iBAAA,KAEA,qBlCpEA,OAAA,iBACA,QAAA,EkCoEA,mBlCrEA,OAAA,kBACA,QAAA,GkCyEF,cACE,QAAA,KACA,cAAA,IAAA,MAAA,QAIF,qBACE,WAAA,KAIF,aACE,OAAA,EACA,YAAA,WAKF,YACE,SAAA,SACA,QAAA,KAIF,cACE,QAAA,KACA,WAAA,MACA,WAAA,IAAA,MAAA,QAHF,wBAQI,cAAA,EACA,YAAA,IATJ,mCAaI,YAAA,KAbJ,oCAiBI,YAAA,EAKJ,yBACE,SAAA,SACA,IAAA,QACA,MAAA,KACA,OAAA,KACA,SAAA,OAIF,yBAEE,cACE,MAAA,MACA,OAAA,KAAA,KAEF,enDrEA,mBAAA,EAAA,IAAA,KAAA,eACQ,WAAA,EAAA,IAAA,KAAA,emDyER,UAAY,MAAA,OAGd,yBACE,UAAY,MAAA,OC9Id,SACE,SAAA,SACA,QAAA,KACA,QAAA,MCRA,YAAA,gBAAA,CAAA,SAAA,CAAA,KAAA,CAAA,WAEA,WAAA,OACA,YAAA,IACA,YAAA,WACA,WAAA,KACA,WAAA,KACA,WAAA,MACA,gBAAA,KACA,YAAA,KACA,eAAA,KACA,eAAA,OACA,WAAA,OACA,aAAA,OACA,UAAA,OACA,YAAA,ODHA,UAAA,KnCTA,OAAA,iBACA,QAAA,EmCYA,YnCbA,OAAA,kBACA,QAAA,GmCaA,aACE,QAAA,IAAA,EACA,WAAA,KAEF,eACE,QAAA,EAAA,IACA,YAAA,IAEF,gBACE,QAAA,IAAA,EACA,WAAA,IAEF,cACE,QAAA,EAAA,IACA,YAAA,KAIF,4BACE,OAAA,EACA,KAAA,IACA,YAAA,KACA,aAAA,IAAA,IAAA,EACA,iBAAA,KAEF,iCACE,MAAA,IACA,OAAA,EACA,cAAA,KACA,aAAA,IAAA,IAAA,EACA,iBAAA,KAEF,kCACE,OAAA,EACA,KAAA,IACA,cAAA,KACA,aAAA,IAAA,IAAA,EACA,iBAAA,KAEF,8BACE,IAAA,IACA,KAAA,EACA,WAAA,KACA,aAAA,IAAA,IAAA,IAAA,EACA,mBAAA,KAEF,6BACE,IAAA,IACA,MAAA,EACA,WAAA,KACA,aAAA,IAAA,EAAA,IAAA,IACA,kBAAA,KAEF,+BACE,IAAA,EACA,KAAA,IACA,YAAA,KACA,aAAA,EAAA,IAAA,IACA,oBAAA,KAEF,oCACE,IAAA,EACA,MAAA,IACA,WAAA,KACA,aAAA,EAAA,IAAA,IACA,oBAAA,KAEF,qCACE,IAAA,EACA,KAAA,IACA,WAAA,KACA,aAAA,EAAA,IAAA,IACA,oBAAA,KAKJ,eACE,UAAA,MACA,QAAA,IAAA,IACA,MAAA,KACA,WAAA,OACA,iBAAA,KACA,cAAA,IAIF,eACE,SAAA,SACA,MAAA,EACA,OAAA,EACA,aAAA,YACA,aAAA,MEzGF,SACE,SAAA,SACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,QAAA,KACA,UAAA,MACA,QAAA,IDXA,YAAA,gBAAA,CAAA,SAAA,CAAA,KAAA,CAAA,WAEA,WAAA,OACA,YAAA,IACA,YAAA,WACA,WAAA,KACA,WAAA,KACA,WAAA,MACA,gBAAA,KACA,YAAA,KACA,eAAA,KACA,eAAA,OACA,WAAA,OACA,aAAA,OACA,UAAA,OACA,YAAA,OCAA,UAAA,KACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,KACA,OAAA,IAAA,MAAA,eACA,cAAA,ItDiDA,mBAAA,EAAA,IAAA,KAAA,eACQ,WAAA,EAAA,IAAA,KAAA,esD9CR,aAAQ,WAAA,MACR,eAAU,YAAA,KACV,gBAAW,WAAA,KACX,cAAS,YAAA,MAvBX,gBA4BI,aAAA,KAEA,gB1DkjMJ,sB0DhjMM,SAAA,SACA,QAAA,MACA,MAAA,EACA,OAAA,EACA,aAAA,YACA,aAAA,MAGF,sBACE,QAAA,GACA,aAAA,KAIJ,oBACE,OAAA,MACA,KAAA,IACA,YAAA,MACA,iBAAA,KACA,iBAAA,gBACA,oBAAA,EACA,0BACE,OAAA,IACA,YAAA,MACA,QAAA,IACA,iBAAA,KACA,oBAAA,EAGJ,sBACE,IAAA,IACA,KAAA,MACA,WAAA,MACA,mBAAA,KACA,mBAAA,gBACA,kBAAA,EACA,4BACE,OAAA,MACA,KAAA,IACA,QAAA,IACA,mBAAA,KACA,kBAAA,EAGJ,uBACE,IAAA,MACA,KAAA,IACA,YAAA,MACA,iBAAA,EACA,oBAAA,KACA,oBAAA,gBACA,6BACE,IAAA,IACA,YAAA,MACA,QAAA,IACA,iBAAA,EACA,oBAAA,KAIJ,qBACE,IAAA,IACA,MAAA,MACA,WAAA,MACA,mBAAA,EACA,kBAAA,KACA,kBAAA,gBACA,2BACE,MAAA,IACA,OAAA,MACA,QAAA,IACA,mBAAA,EACA,kBAAA,KAKN,eACE,QAAA,IAAA,KACA,OAAA,EACA,UAAA,KACA,iBAAA,QACA,cAAA,IAAA,MAAA,QACA,cAAA,IAAA,IAAA,EAAA,EAGF,iBACE,QAAA,IAAA,KCpHF,UACE,SAAA,SAGF,gBACE,SAAA,SACA,MAAA,KACA,SAAA,OAHF,sBAMI,SAAA,SACA,QAAA,KvD6KF,mBAAA,IAAA,YAAA,KACK,cAAA,IAAA,YAAA,KACG,WAAA,IAAA,YAAA,KJs/LV,4B2D5qMA,0BAcM,YAAA,EAIF,8BAAA,uBAAA,sBvDuLF,mBAAA,kBAAA,IAAA,YAEK,cAAA,aAAA,IAAA,YACG,WAAA,kBAAA,IAAA,YAAA,WAAA,UAAA,IAAA,YAAA,WAAA,UAAA,IAAA,WAAA,CAAA,kBAAA,IAAA,WAAA,CAAA,aAAA,IAAA,YA7JR,4BAAA,OAEQ,oBAAA,OA+GR,oBAAA,OAEQ,YAAA,OJ0hMR,mC2DrqMI,2BvDmHJ,kBAAA,sBACQ,UAAA,sBuDjHF,KAAA,E3DwqMN,kC2DtqMI,2BvD8GJ,kBAAA,uBACQ,UAAA,uBuD5GF,KAAA,E3D0qMN,6B2DxqMI,gC3DuqMJ,iCI9jMA,kBAAA,mBACQ,UAAA,mBuDtGF,KAAA,GArCR,wB3DgtMA,sBACA,sB2DpqMI,QAAA,MA7CJ,wBAiDI,KAAA,EAjDJ,sB3DwtMA,sB2DlqMI,SAAA,SACA,IAAA,EACA,MAAA,KAxDJ,sBA4DI,KAAA,KA5DJ,sBA+DI,KAAA,MA/DJ,2B3DouMA,4B2DjqMI,KAAA,EAnEJ,6BAuEI,KAAA,MAvEJ,8BA0EI,KAAA,KAQJ,kBACE,SAAA,SACA,IAAA,EACA,OAAA,EACA,KAAA,EACA,MAAA,IACA,UAAA,KACA,MAAA,KACA,WAAA,OACA,YAAA,EAAA,IAAA,IAAA,eACA,iBAAA,ctCpGA,OAAA,kBACA,QAAA,GsCyGA,uBdrGE,iBAAA,sEACA,iBAAA,iEACA,iBAAA,uFAAA,iBAAA,kEACA,OAAA,+GACA,kBAAA,ScoGF,wBACE,MAAA,EACA,KAAA,Kd1GA,iBAAA,sEACA,iBAAA,iEACA,iBAAA,uFAAA,iBAAA,kEACA,OAAA,+GACA,kBAAA,S7C6wMJ,wB2DlqME,wBAEE,MAAA,KACA,gBAAA,KACA,QAAA,EtCxHF,OAAA,kBACA,QAAA,GrB8xMF,0CACA,2CAFA,6B2DpsMA,6BAuCI,SAAA,SACA,IAAA,IACA,QAAA,EACA,QAAA,aACA,WAAA,M3DmqMJ,0C2D9sMA,6BA+CI,KAAA,IACA,YAAA,M3DmqMJ,2C2DntMA,6BAoDI,MAAA,IACA,aAAA,M3DmqMJ,6B2DxtMA,6BAyDI,MAAA,KACA,OAAA,KACA,YAAA,MACA,YAAA,EAIA,oCACE,QAAA,QAIF,oCACE,QAAA,QAUN,qBACE,SAAA,SACA,OAAA,KACA,KAAA,IACA,QAAA,GACA,MAAA,IACA,aAAA,EACA,YAAA,KACA,WAAA,OACA,WAAA,KATF,wBAYI,QAAA,aACA,MAAA,KACA,OAAA,KACA,OAAA,IACA,YAAA,OACA,OAAA,QAUA,iBAAA,OACA,iBAAA,cAEA,OAAA,IAAA,MAAA,KACA,cAAA,KA/BJ,6BAmCI,MAAA,KACA,OAAA,KACA,OAAA,EACA,iBAAA,KAOJ,kBACE,SAAA,SACA,MAAA,IACA,OAAA,KACA,KAAA,IACA,QAAA,GACA,YAAA,KACA,eAAA,KACA,MAAA,KACA,WAAA,OACA,YAAA,EAAA,IAAA,IAAA,eAEA,uBACE,YAAA,KAMJ,oCAGE,0C3D+nMA,2CAEA,6BADA,6B2D3nMI,MAAA,KACA,OAAA,KACA,WAAA,MACA,UAAA,KARJ,0C3DwoMA,6B2D5nMI,YAAA,MAZJ,2C3D4oMA,6B2D5nMI,aAAA,MAKJ,kBACE,MAAA,IACA,KAAA,IACA,eAAA,KAIF,qBACE,OAAA,M3D0oMJ,qCADA,sCADA,mBADA,oBAXA,gB4D73ME,iB5Dm4MF,uBADA,wBADA,iBADA,kBADA,wBADA,yBASA,mCADA,oCAqBA,oBADA,qBADA,oBADA,qBAXA,WADA,YAOA,uBADA,wBADA,qBADA,sBADA,cADA,eAOA,aADA,cAGA,kBADA,mBAjBA,WADA,Y4Dl4MI,QAAA,MACA,QAAA,I5Dm6MJ,qCADA,mB4Dh6ME,gB5D65MF,uBADA,iBADA,wBAIA,mCAUA,oBADA,oBANA,WAGA,uBADA,qBADA,cAGA,aACA,kBATA,W4D75MI,MAAA,K5BNJ,c6BVE,QAAA,MACA,aAAA,KACA,YAAA,K7BWF,YACE,MAAA,gBAEF,WACE,MAAA,eAQF,MACE,QAAA,eAEF,MACE,QAAA,gBAEF,WACE,WAAA,OAEF,W8BzBE,KAAA,CAAA,CAAA,EAAA,EACA,MAAA,YACA,YAAA,KACA,iBAAA,YACA,OAAA,E9B8BF,QACE,QAAA,eAOF,OACE,SAAA,M+BjCF,cACE,MAAA,a/D88MF,YADA,YADA,Y+Dt8MA,YClBE,QAAA,ehEs+MF,kBACA,mBACA,yBALA,kBACA,mBACA,yBALA,kBACA,mBACA,yB+Dz8MA,kB/Dq8MA,mBACA,yB+D17ME,QAAA,eAIA,yBAAA,YCjDA,QAAA,gBACA,iBAAU,QAAA,gBACV,cAAU,QAAA,oBhE4/MV,cgE3/MA,cACU,QAAA,sBDkDV,yBAAA,kBACE,QAAA,iBAIF,yBAAA,mBACE,QAAA,kBAIF,yBAAA,yBACE,QAAA,wBAKF,+CAAA,YCtEA,QAAA,gBACA,iBAAU,QAAA,gBACV,cAAU,QAAA,oBhE0hNV,cgEzhNA,cACU,QAAA,sBDuEV,+CAAA,kBACE,QAAA,iBAIF,+CAAA,mBACE,QAAA,kBAIF,+CAAA,yBACE,QAAA,wBAKF,gDAAA,YC3FA,QAAA,gBACA,iBAAU,QAAA,gBACV,cAAU,QAAA,oBhEwjNV,cgEvjNA,cACU,QAAA,sBD4FV,gDAAA,kBACE,QAAA,iBAIF,gDAAA,mBACE,QAAA,kBAIF,gDAAA,yBACE,QAAA,wBAKF,0BAAA,YChHA,QAAA,gBACA,iBAAU,QAAA,gBACV,cAAU,QAAA,oBhEslNV,cgErlNA,cACU,QAAA,sBDiHV,0BAAA,kBACE,QAAA,iBAIF,0BAAA,mBACE,QAAA,kBAIF,0BAAA,yBACE,QAAA,wBAKF,yBAAA,WC7HA,QAAA,gBDkIA,+CAAA,WClIA,QAAA,gBDuIA,gDAAA,WCvIA,QAAA,gBD4IA,0BAAA,WC5IA,QAAA,gBDuJF,eCvJE,QAAA,eD0JA,aAAA,eClKA,QAAA,gBACA,oBAAU,QAAA,gBACV,iBAAU,QAAA,oBhE2oNV,iBgE1oNA,iBACU,QAAA,sBDkKZ,qBACE,QAAA,eAEA,aAAA,qBACE,QAAA,iBAGJ,sBACE,QAAA,eAEA,aAAA,sBACE,QAAA,kBAGJ,4BACE,QAAA,eAEA,aAAA,4BACE,QAAA,wBAKF,aAAA,cCrLA,QAAA","sourcesContent":["/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: none;\n text-decoration: underline;\n text-decoration: underline dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important;\n text-shadow: none !important;\n background: transparent !important;\n box-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: \"Glyphicons Halflings\";\n src: url(\"../fonts/glyphicons-halflings-regular.eot\");\n src: url(\"../fonts/glyphicons-halflings-regular.eot?#iefix\") format(\"embedded-opentype\"), url(\"../fonts/glyphicons-halflings-regular.woff2\") format(\"woff2\"), url(\"../fonts/glyphicons-halflings-regular.woff\") format(\"woff\"), url(\"../fonts/glyphicons-halflings-regular.ttf\") format(\"truetype\"), url(\"../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular\") format(\"svg\");\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: \"Glyphicons Halflings\";\n font-style: normal;\n font-weight: 400;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\002a\";\n}\n.glyphicon-plus:before {\n content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: 400;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: 700;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: \"\\2014 \\00A0\";\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: \"\";\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: \"\\00A0 \\2014\";\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #fff;\n background-color: #333;\n border-radius: 3px;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n color: #333333;\n word-break: break-all;\n word-wrap: break-word;\n background-color: #f5f5f5;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n.row {\n margin-right: -15px;\n margin-left: -15px;\n}\n.row-no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n.row-no-gutters [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n.col-xs-1,\n.col-sm-1,\n.col-md-1,\n.col-lg-1,\n.col-xs-2,\n.col-sm-2,\n.col-md-2,\n.col-lg-2,\n.col-xs-3,\n.col-sm-3,\n.col-md-3,\n.col-lg-3,\n.col-xs-4,\n.col-sm-4,\n.col-md-4,\n.col-lg-4,\n.col-xs-5,\n.col-sm-5,\n.col-md-5,\n.col-lg-5,\n.col-xs-6,\n.col-sm-6,\n.col-md-6,\n.col-lg-6,\n.col-xs-7,\n.col-sm-7,\n.col-md-7,\n.col-lg-7,\n.col-xs-8,\n.col-sm-8,\n.col-md-8,\n.col-lg-8,\n.col-xs-9,\n.col-sm-9,\n.col-md-9,\n.col-lg-9,\n.col-xs-10,\n.col-sm-10,\n.col-md-10,\n.col-lg-10,\n.col-xs-11,\n.col-sm-11,\n.col-md-11,\n.col-lg-11,\n.col-xs-12,\n.col-sm-12,\n.col-md-12,\n.col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-right: 15px;\n padding-left: 15px;\n}\n.col-xs-1,\n.col-xs-2,\n.col-xs-3,\n.col-xs-4,\n.col-xs-5,\n.col-xs-6,\n.col-xs-7,\n.col-xs-8,\n.col-xs-9,\n.col-xs-10,\n.col-xs-11,\n.col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1,\n .col-sm-2,\n .col-sm-3,\n .col-sm-4,\n .col-sm-5,\n .col-sm-6,\n .col-sm-7,\n .col-sm-8,\n .col-sm-9,\n .col-sm-10,\n .col-sm-11,\n .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1,\n .col-md-2,\n .col-md-3,\n .col-md-4,\n .col-md-5,\n .col-md-6,\n .col-md-7,\n .col-md-8,\n .col-md-9,\n .col-md-10,\n .col-md-11,\n .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1,\n .col-lg-2,\n .col-lg-3,\n .col-lg-4,\n .col-lg-5,\n .col-lg-6,\n .col-lg-7,\n .col-lg-8,\n .col-lg-9,\n .col-lg-10,\n .col-lg-11,\n .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ntable col[class*=\"col-\"] {\n position: static;\n display: table-column;\n float: none;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n display: table-cell;\n float: none;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #ddd;\n}\n.table .table {\n background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n min-height: 0.01%;\n overflow-x: auto;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #ddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: 700;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n -webkit-appearance: none;\n appearance: none;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #fff;\n background-image: none;\n border: 1px solid #ccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999;\n}\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-top: 4px \\9;\n margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n vertical-align: middle;\n cursor: pointer;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\n.form-control-static {\n min-height: 34px;\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-right: 0;\n padding-left: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #3c763d;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #8a6d3b;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n background-color: #f2dede;\n border-color: #a94442;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n padding-top: 7px;\n margin-top: 0;\n margin-bottom: 0;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n padding-top: 7px;\n margin-bottom: 0;\n text-align: right;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 11px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n outline: 0;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n filter: alpha(opacity=65);\n opacity: 0.65;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333;\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333;\n background-color: #e6e6e6;\n background-image: none;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default .badge {\n color: #fff;\n background-color: #333;\n}\n.btn-primary {\n color: #fff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #fff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #fff;\n background-color: #286090;\n background-image: none;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #fff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.btn-success {\n color: #fff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #fff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #fff;\n background-color: #449d44;\n background-image: none;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #fff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #fff;\n}\n.btn-info {\n color: #fff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #fff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #fff;\n background-color: #31b0d5;\n background-image: none;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #fff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #fff;\n}\n.btn-warning {\n color: #fff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #fff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #fff;\n background-color: #ec971f;\n background-image: none;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #fff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #fff;\n}\n.btn-danger {\n color: #fff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #fff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #fff;\n background-color: #c9302c;\n background-image: none;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #fff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #fff;\n}\n.btn-link {\n font-weight: 400;\n color: #337ab7;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n font-size: 14px;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: 400;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n color: #262626;\n text-decoration: none;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #fff;\n text-decoration: none;\n background-color: #337ab7;\n outline: 0;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n content: \"\";\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n right: 0;\n left: auto;\n }\n .navbar-right .dropdown-menu-left {\n right: auto;\n left: 0;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-right: 8px;\n padding-left: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-right: 12px;\n padding-left: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n display: table-cell;\n float: none;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-right: 0;\n padding-left: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group .form-control:focus {\n z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: 400;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n cursor: default;\n background-color: #fff;\n border: 1px solid #ddd;\n border-bottom-color: transparent;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #fff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n padding-right: 15px;\n padding-left: 15px;\n overflow-x: visible;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-right: 0;\n padding-left: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-brand {\n float: left;\n height: 50px;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n padding: 9px 10px;\n margin-right: 15px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n padding: 10px 15px;\n margin-right: -15px;\n margin-left: -15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n padding-top: 0;\n padding-bottom: 0;\n margin-right: 0;\n margin-left: 0;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-right: 15px;\n margin-left: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-toggle {\n border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-link {\n color: #777;\n}\n.navbar-default .navbar-link:hover {\n color: #333;\n}\n.navbar-default .btn-link {\n color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #ccc;\n}\n.navbar-inverse {\n background-color: #222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #fff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n color: #fff;\n background-color: #080808;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #fff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #fff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n padding: 0 5px;\n color: #ccc;\n content: \"/\\00a0\";\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n margin-left: -1px;\n line-height: 1.42857143;\n color: #337ab7;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 2;\n color: #23527c;\n background-color: #eeeeee;\n border-color: #ddd;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 3;\n color: #fff;\n cursor: default;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n border-color: #ddd;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-top-left-radius: 6px;\n border-bottom-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-top-left-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-top-right-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n text-align: center;\n list-style: none;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n}\n.label {\n display: inline;\n padding: 0.2em 0.6em 0.3em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25em;\n}\na.label:hover,\na.label:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n padding-right: 15px;\n padding-left: 15px;\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-right: 60px;\n padding-left: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-right: auto;\n margin-left: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n height: 20px;\n margin-bottom: 20px;\n overflow: hidden;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #fff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n overflow: hidden;\n zoom: 1;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n padding-left: 0;\n margin-bottom: 20px;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #eeeeee;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n color: #555;\n text-decoration: none;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #fff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #ddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-right: 15px;\n padding-left: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n margin-bottom: 0;\n border: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #ddd;\n}\n.panel-default {\n border-color: #ddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n filter: alpha(opacity=20);\n opacity: 0.2;\n}\n.close:hover,\n.close:focus {\n color: #000;\n text-decoration: none;\n cursor: pointer;\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n display: none;\n overflow: hidden;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -moz-transition: -moz-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n.modal-backdrop.fade {\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.modal-backdrop.in {\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-bottom: 0;\n margin-left: 5px;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 12px;\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.tooltip.in {\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.tooltip.top {\n padding: 5px 0;\n margin-top: -3px;\n}\n.tooltip.right {\n padding: 0 5px;\n margin-left: 3px;\n}\n.tooltip.bottom {\n padding: 5px 0;\n margin-top: 3px;\n}\n.tooltip.left {\n padding: 0 5px;\n margin-left: -3px;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n right: 5px;\n bottom: 0;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 14px;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow:after {\n content: \"\";\n border-width: 10px;\n}\n.popover.top > .arrow {\n bottom: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n border-bottom-width: 0;\n}\n.popover.top > .arrow:after {\n bottom: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-color: #fff;\n border-bottom-width: 0;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n border-left-width: 0;\n}\n.popover.right > .arrow:after {\n bottom: -10px;\n left: 1px;\n content: \" \";\n border-right-color: #fff;\n border-left-width: 0;\n}\n.popover.bottom > .arrow {\n top: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n.popover.bottom > .arrow:after {\n top: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-width: 0;\n border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n right: 1px;\n bottom: -10px;\n content: \" \";\n border-right-width: 0;\n border-left-color: #fff;\n}\n.popover-title {\n padding: 8px 14px;\n margin: 0;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner > .item {\n position: relative;\n display: none;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -moz-transition: -moz-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n -moz-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 15%;\n font-size: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n background-color: rgba(0, 0, 0, 0);\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control.right {\n right: 0;\n left: auto;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control:hover,\n.carousel-control:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n margin-top: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n font-family: serif;\n line-height: 1;\n}\n.carousel-control .icon-prev:before {\n content: \"\\2039\";\n}\n.carousel-control .icon-next:before {\n content: \"\\203a\";\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n padding-left: 0;\n margin-left: -30%;\n text-align: center;\n list-style: none;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n border: 1px solid #fff;\n border-radius: 10px;\n}\n.carousel-indicators .active {\n width: 12px;\n height: 12px;\n margin: 0;\n background-color: #fff;\n}\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -10px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -10px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -10px;\n }\n .carousel-caption {\n right: 20%;\n left: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n display: table;\n content: \" \";\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-right: auto;\n margin-left: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable\n\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS and IE text size adjust after device orientation change,\n// without disabling user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11\n// and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background-color: transparent;\n}\n\n//\n// Improve readability of focused elements when they are also in an\n// active/hover state.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// 1. Remove the bottom border in Chrome 57- and Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n//\n\nabbr[title] {\n border-bottom: none; // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome.\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n box-sizing: content-box; //2\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","/*!\n * Bootstrap v3.4.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: none;\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n -moz-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n -webkit-box-sizing: content-box;\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n -webkit-box-sizing: content-box;\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important;\n text-shadow: none !important;\n background: transparent !important;\n -webkit-box-shadow: none !important;\n box-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: \"Glyphicons Halflings\";\n src: url(\"../fonts/glyphicons-halflings-regular.eot\");\n src: url(\"../fonts/glyphicons-halflings-regular.eot?#iefix\") format(\"embedded-opentype\"), url(\"../fonts/glyphicons-halflings-regular.woff2\") format(\"woff2\"), url(\"../fonts/glyphicons-halflings-regular.woff\") format(\"woff\"), url(\"../fonts/glyphicons-halflings-regular.ttf\") format(\"truetype\"), url(\"../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular\") format(\"svg\");\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: \"Glyphicons Halflings\";\n font-style: normal;\n font-weight: 400;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\002a\";\n}\n.glyphicon-plus:before {\n content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: 400;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: 700;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: \"\\2014 \\00A0\";\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: \"\";\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: \"\\00A0 \\2014\";\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #fff;\n background-color: #333;\n border-radius: 3px;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n color: #333333;\n word-break: break-all;\n word-wrap: break-word;\n background-color: #f5f5f5;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n.row {\n margin-right: -15px;\n margin-left: -15px;\n}\n.row-no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n.row-no-gutters [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n.col-xs-1,\n.col-sm-1,\n.col-md-1,\n.col-lg-1,\n.col-xs-2,\n.col-sm-2,\n.col-md-2,\n.col-lg-2,\n.col-xs-3,\n.col-sm-3,\n.col-md-3,\n.col-lg-3,\n.col-xs-4,\n.col-sm-4,\n.col-md-4,\n.col-lg-4,\n.col-xs-5,\n.col-sm-5,\n.col-md-5,\n.col-lg-5,\n.col-xs-6,\n.col-sm-6,\n.col-md-6,\n.col-lg-6,\n.col-xs-7,\n.col-sm-7,\n.col-md-7,\n.col-lg-7,\n.col-xs-8,\n.col-sm-8,\n.col-md-8,\n.col-lg-8,\n.col-xs-9,\n.col-sm-9,\n.col-md-9,\n.col-lg-9,\n.col-xs-10,\n.col-sm-10,\n.col-md-10,\n.col-lg-10,\n.col-xs-11,\n.col-sm-11,\n.col-md-11,\n.col-lg-11,\n.col-xs-12,\n.col-sm-12,\n.col-md-12,\n.col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-right: 15px;\n padding-left: 15px;\n}\n.col-xs-1,\n.col-xs-2,\n.col-xs-3,\n.col-xs-4,\n.col-xs-5,\n.col-xs-6,\n.col-xs-7,\n.col-xs-8,\n.col-xs-9,\n.col-xs-10,\n.col-xs-11,\n.col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1,\n .col-sm-2,\n .col-sm-3,\n .col-sm-4,\n .col-sm-5,\n .col-sm-6,\n .col-sm-7,\n .col-sm-8,\n .col-sm-9,\n .col-sm-10,\n .col-sm-11,\n .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1,\n .col-md-2,\n .col-md-3,\n .col-md-4,\n .col-md-5,\n .col-md-6,\n .col-md-7,\n .col-md-8,\n .col-md-9,\n .col-md-10,\n .col-md-11,\n .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1,\n .col-lg-2,\n .col-lg-3,\n .col-lg-4,\n .col-lg-5,\n .col-lg-6,\n .col-lg-7,\n .col-lg-8,\n .col-lg-9,\n .col-lg-10,\n .col-lg-11,\n .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ntable col[class*=\"col-\"] {\n position: static;\n display: table-column;\n float: none;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n display: table-cell;\n float: none;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #ddd;\n}\n.table .table {\n background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n min-height: 0.01%;\n overflow-x: auto;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #ddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: 700;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #fff;\n background-image: none;\n border: 1px solid #ccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999;\n}\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-top: 4px \\9;\n margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: 400;\n vertical-align: middle;\n cursor: pointer;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\n.form-control-static {\n min-height: 34px;\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-right: 0;\n padding-left: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #3c763d;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #8a6d3b;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n background-color: #f2dede;\n border-color: #a94442;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n padding-top: 7px;\n margin-top: 0;\n margin-bottom: 0;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n padding-top: 7px;\n margin-bottom: 0;\n text-align: right;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 11px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n -ms-touch-action: manipulation;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n outline: 0;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n filter: alpha(opacity=65);\n opacity: 0.65;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333;\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333;\n background-color: #e6e6e6;\n background-image: none;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default .badge {\n color: #fff;\n background-color: #333;\n}\n.btn-primary {\n color: #fff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #fff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #fff;\n background-color: #286090;\n background-image: none;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #fff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.btn-success {\n color: #fff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #fff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #fff;\n background-color: #449d44;\n background-image: none;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #fff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #fff;\n}\n.btn-info {\n color: #fff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #fff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #fff;\n background-color: #31b0d5;\n background-image: none;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #fff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #fff;\n}\n.btn-warning {\n color: #fff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #fff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #fff;\n background-color: #ec971f;\n background-image: none;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #fff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #fff;\n}\n.btn-danger {\n color: #fff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #fff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #fff;\n background-color: #c9302c;\n background-image: none;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #fff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #fff;\n}\n.btn-link {\n font-weight: 400;\n color: #337ab7;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n -o-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n -o-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n -o-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n font-size: 14px;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: 400;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n color: #262626;\n text-decoration: none;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #fff;\n text-decoration: none;\n background-color: #337ab7;\n outline: 0;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n content: \"\";\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n right: 0;\n left: auto;\n }\n .navbar-right .dropdown-menu-left {\n right: auto;\n left: 0;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-right: 8px;\n padding-left: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-right: 12px;\n padding-left: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n display: table-cell;\n float: none;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-right: 0;\n padding-left: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group .form-control:focus {\n z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: 400;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n cursor: default;\n background-color: #fff;\n border: 1px solid #ddd;\n border-bottom-color: transparent;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #fff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n padding-right: 15px;\n padding-left: 15px;\n overflow-x: visible;\n border-top: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-right: 0;\n padding-left: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-brand {\n float: left;\n height: 50px;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n padding: 9px 10px;\n margin-right: 15px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n padding: 10px 15px;\n margin-right: -15px;\n margin-left: -15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n padding-top: 0;\n padding-bottom: 0;\n margin-right: 0;\n margin-left: 0;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-right: 15px;\n margin-left: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-toggle {\n border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-link {\n color: #777;\n}\n.navbar-default .navbar-link:hover {\n color: #333;\n}\n.navbar-default .btn-link {\n color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #ccc;\n}\n.navbar-inverse {\n background-color: #222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #fff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n color: #fff;\n background-color: #080808;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #fff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #fff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n padding: 0 5px;\n color: #ccc;\n content: \"/\\00a0\";\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n margin-left: -1px;\n line-height: 1.42857143;\n color: #337ab7;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 2;\n color: #23527c;\n background-color: #eeeeee;\n border-color: #ddd;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 3;\n color: #fff;\n cursor: default;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n border-color: #ddd;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-top-left-radius: 6px;\n border-bottom-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-top-left-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-top-right-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n text-align: center;\n list-style: none;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n cursor: not-allowed;\n background-color: #fff;\n}\n.label {\n display: inline;\n padding: 0.2em 0.6em 0.3em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25em;\n}\na.label:hover,\na.label:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n padding-right: 15px;\n padding-left: 15px;\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-right: 60px;\n padding-left: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-right: auto;\n margin-left: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@-o-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n height: 20px;\n margin-bottom: 20px;\n overflow: hidden;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #fff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n -webkit-background-size: 40px 40px;\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n overflow: hidden;\n zoom: 1;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n padding-left: 0;\n margin-bottom: 20px;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n color: #777777;\n cursor: not-allowed;\n background-color: #eeeeee;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n color: #555;\n text-decoration: none;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #fff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #ddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-right: 15px;\n padding-left: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n margin-bottom: 0;\n border: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #ddd;\n}\n.panel-default {\n border-color: #ddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n filter: alpha(opacity=20);\n opacity: 0.2;\n}\n.close:hover,\n.close:focus {\n color: #000;\n text-decoration: none;\n cursor: pointer;\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n display: none;\n overflow: hidden;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: -webkit-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out, -o-transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n.modal-backdrop.fade {\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.modal-backdrop.in {\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-bottom: 0;\n margin-left: 5px;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 12px;\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.tooltip.in {\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.tooltip.top {\n padding: 5px 0;\n margin-top: -3px;\n}\n.tooltip.right {\n padding: 0 5px;\n margin-left: 3px;\n}\n.tooltip.bottom {\n padding: 5px 0;\n margin-top: 3px;\n}\n.tooltip.left {\n padding: 0 5px;\n margin-left: -3px;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n right: 5px;\n bottom: 0;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: 400;\n line-height: 1.42857143;\n line-break: auto;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n font-size: 14px;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow:after {\n content: \"\";\n border-width: 10px;\n}\n.popover.top > .arrow {\n bottom: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n border-bottom-width: 0;\n}\n.popover.top > .arrow:after {\n bottom: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-color: #fff;\n border-bottom-width: 0;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n border-left-width: 0;\n}\n.popover.right > .arrow:after {\n bottom: -10px;\n left: 1px;\n content: \" \";\n border-right-color: #fff;\n border-left-width: 0;\n}\n.popover.bottom > .arrow {\n top: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n.popover.bottom > .arrow:after {\n top: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-width: 0;\n border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n right: 1px;\n bottom: -10px;\n content: \" \";\n border-right-width: 0;\n border-left-color: #fff;\n}\n.popover-title {\n padding: 8px 14px;\n margin: 0;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner > .item {\n position: relative;\n display: none;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: -webkit-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out, -o-transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 15%;\n font-size: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n background-color: rgba(0, 0, 0, 0);\n filter: alpha(opacity=50);\n opacity: 0.5;\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001)));\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control.right {\n right: 0;\n left: auto;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5)));\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control:hover,\n.carousel-control:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n filter: alpha(opacity=90);\n opacity: 0.9;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n margin-top: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n font-family: serif;\n line-height: 1;\n}\n.carousel-control .icon-prev:before {\n content: \"\\2039\";\n}\n.carousel-control .icon-next:before {\n content: \"\\203a\";\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n padding-left: 0;\n margin-left: -30%;\n text-align: center;\n list-style: none;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n border: 1px solid #fff;\n border-radius: 10px;\n}\n.carousel-indicators .active {\n width: 12px;\n height: 12px;\n margin: 0;\n background-color: #fff;\n}\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -10px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -10px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -10px;\n }\n .carousel-caption {\n right: 20%;\n left: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n display: table;\n content: \" \";\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-right: auto;\n margin-left: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable declaration-no-important, selector-no-qualifying-type\n\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request: h5bp.com/r\n// ==========================================================================\n\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important; // Black prints faster: h5bp.com/s\n text-shadow: none !important;\n background: transparent !important;\n box-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n}\n","// stylelint-disable value-list-comma-newline-after, value-list-comma-space-after, indentation, declaration-colon-newline-after, font-family-no-missing-generic-family-keyword\n\n//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// Star\n\n// Import the fonts\n@font-face {\n font-family: \"Glyphicons Halflings\";\n src: url(\"@{icon-font-path}@{icon-font-name}.eot\");\n src: url(\"@{icon-font-path}@{icon-font-name}.eot?#iefix\") format(\"embedded-opentype\"),\n url(\"@{icon-font-path}@{icon-font-name}.woff2\") format(\"woff2\"),\n url(\"@{icon-font-path}@{icon-font-name}.woff\") format(\"woff\"),\n url(\"@{icon-font-path}@{icon-font-name}.ttf\") format(\"truetype\"),\n url(\"@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}\") format(\"svg\");\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: \"Glyphicons Halflings\";\n font-style: normal;\n font-weight: 400;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\002a\"; } }\n.glyphicon-plus { &:before { content: \"\\002b\"; } }\n.glyphicon-euro,\n.glyphicon-eur { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n.glyphicon-cd { &:before { content: \"\\e201\"; } }\n.glyphicon-save-file { &:before { content: \"\\e202\"; } }\n.glyphicon-open-file { &:before { content: \"\\e203\"; } }\n.glyphicon-level-up { &:before { content: \"\\e204\"; } }\n.glyphicon-copy { &:before { content: \"\\e205\"; } }\n.glyphicon-paste { &:before { content: \"\\e206\"; } }\n// The following 2 Glyphicons are omitted for the time being because\n// they currently use Unicode codepoints that are outside the\n// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle\n// non-BMP codepoints in CSS string escapes, and thus can't display these two icons.\n// Notably, the bug affects some older versions of the Android Browser.\n// More info: https://github.com/twbs/bootstrap/issues/10106\n// .glyphicon-door { &:before { content: \"\\1f6aa\"; } }\n// .glyphicon-key { &:before { content: \"\\1f511\"; } }\n.glyphicon-alert { &:before { content: \"\\e209\"; } }\n.glyphicon-equalizer { &:before { content: \"\\e210\"; } }\n.glyphicon-king { &:before { content: \"\\e211\"; } }\n.glyphicon-queen { &:before { content: \"\\e212\"; } }\n.glyphicon-pawn { &:before { content: \"\\e213\"; } }\n.glyphicon-bishop { &:before { content: \"\\e214\"; } }\n.glyphicon-knight { &:before { content: \"\\e215\"; } }\n.glyphicon-baby-formula { &:before { content: \"\\e216\"; } }\n.glyphicon-tent { &:before { content: \"\\26fa\"; } }\n.glyphicon-blackboard { &:before { content: \"\\e218\"; } }\n.glyphicon-bed { &:before { content: \"\\e219\"; } }\n.glyphicon-apple { &:before { content: \"\\f8ff\"; } }\n.glyphicon-erase { &:before { content: \"\\e221\"; } }\n.glyphicon-hourglass { &:before { content: \"\\231b\"; } }\n.glyphicon-lamp { &:before { content: \"\\e223\"; } }\n.glyphicon-duplicate { &:before { content: \"\\e224\"; } }\n.glyphicon-piggy-bank { &:before { content: \"\\e225\"; } }\n.glyphicon-scissors { &:before { content: \"\\e226\"; } }\n.glyphicon-bitcoin { &:before { content: \"\\e227\"; } }\n.glyphicon-btc { &:before { content: \"\\e227\"; } }\n.glyphicon-xbt { &:before { content: \"\\e227\"; } }\n.glyphicon-yen { &:before { content: \"\\00a5\"; } }\n.glyphicon-jpy { &:before { content: \"\\00a5\"; } }\n.glyphicon-ruble { &:before { content: \"\\20bd\"; } }\n.glyphicon-rub { &:before { content: \"\\20bd\"; } }\n.glyphicon-scale { &:before { content: \"\\e230\"; } }\n.glyphicon-ice-lolly { &:before { content: \"\\e231\"; } }\n.glyphicon-ice-lolly-tasted { &:before { content: \"\\e232\"; } }\n.glyphicon-education { &:before { content: \"\\e233\"; } }\n.glyphicon-option-horizontal { &:before { content: \"\\e234\"; } }\n.glyphicon-option-vertical { &:before { content: \"\\e235\"; } }\n.glyphicon-menu-hamburger { &:before { content: \"\\e236\"; } }\n.glyphicon-modal-window { &:before { content: \"\\e237\"; } }\n.glyphicon-oil { &:before { content: \"\\e238\"; } }\n.glyphicon-grain { &:before { content: \"\\e239\"; } }\n.glyphicon-sunglasses { &:before { content: \"\\e240\"; } }\n.glyphicon-text-size { &:before { content: \"\\e241\"; } }\n.glyphicon-text-color { &:before { content: \"\\e242\"; } }\n.glyphicon-text-background { &:before { content: \"\\e243\"; } }\n.glyphicon-object-align-top { &:before { content: \"\\e244\"; } }\n.glyphicon-object-align-bottom { &:before { content: \"\\e245\"; } }\n.glyphicon-object-align-horizontal{ &:before { content: \"\\e246\"; } }\n.glyphicon-object-align-left { &:before { content: \"\\e247\"; } }\n.glyphicon-object-align-vertical { &:before { content: \"\\e248\"; } }\n.glyphicon-object-align-right { &:before { content: \"\\e249\"; } }\n.glyphicon-triangle-right { &:before { content: \"\\e250\"; } }\n.glyphicon-triangle-left { &:before { content: \"\\e251\"; } }\n.glyphicon-triangle-bottom { &:before { content: \"\\e252\"; } }\n.glyphicon-triangle-top { &:before { content: \"\\e253\"; } }\n.glyphicon-console { &:before { content: \"\\e254\"; } }\n.glyphicon-superscript { &:before { content: \"\\e255\"; } }\n.glyphicon-subscript { &:before { content: \"\\e256\"; } }\n.glyphicon-menu-left { &:before { content: \"\\e257\"; } }\n.glyphicon-menu-right { &:before { content: \"\\e258\"; } }\n.glyphicon-menu-down { &:before { content: \"\\e259\"; } }\n.glyphicon-menu-up { &:before { content: \"\\e260\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// https://getbootstrap.com/docs/3.4/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: https://a11yproject.com/posts/how-to-hide-content\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n\n\n// iOS \"clickable elements\" fix for role=\"button\"\n//\n// Fixes \"clickability\" issue (and more generally, the firing of events such as focus as well)\n// for traditionally non-focusable elements with role=\"button\"\n// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n\n[role=\"button\"] {\n cursor: pointer;\n}\n","// stylelint-disable indentation, property-no-vendor-prefix, selector-no-vendor-prefix\n\n// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They have been removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility) {\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n word-wrap: break-word;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // WebKit-specific. Other browsers will keep their default outline style.\n // (Initially tried to also force default via `outline: initial`,\n // but that seems to erroneously remove the outline in Firefox altogether.)\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// stylelint-disable media-feature-name-no-vendor-prefix, media-feature-parentheses-space-inside, media-feature-name-no-unknown, indentation, at-rule-name-space-after\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","// stylelint-disable selector-list-comma-newline-after, selector-no-qualifying-type\n\n//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: 400;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n padding: .2em;\n background-color: @state-warning-bg;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: 700;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @dl-horizontal-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n}\n\n.initialism {\n font-size: 90%;\n .text-uppercase();\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: \"\\2014 \\00A0\"; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: \"\"; }\n &:after {\n content: \"\\00A0 \\2014\"; // nbsp, em dash\n }\n }\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover,\n a&:focus {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover,\n a&:focus {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n color: @pre-color;\n word-break: break-all;\n word-wrap: break-word;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n.row-no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n padding-right: ceil((@gutter / 2));\n padding-left: floor((@gutter / 2));\n margin-right: auto;\n margin-left: auto;\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-right: floor((@gutter / -2));\n margin-left: ceil((@gutter / -2));\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-right: (@gutter / 2);\n padding-left: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-right: floor((@grid-gutter-width / 2));\n padding-left: ceil((@grid-gutter-width / 2));\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","// stylelint-disable selector-max-type, selector-max-compound-selectors, selector-no-qualifying-type\n\n//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n\n // Table cell sizing\n //\n // Reset default table behavior\n\n col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n display: table-column;\n float: none;\n }\n\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n display: table-cell;\n float: none;\n }\n }\n}\n\ncaption {\n padding-top: @table-cell-padding;\n padding-bottom: @table-cell-padding;\n color: @text-muted;\n text-align: left;\n}\n\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-of-type(odd) {\n background-color: @table-bg-accent;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n background-color: @table-bg-hover;\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n min-height: .01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)\n overflow-x: auto;\n\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * .75);\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","// stylelint-disable selector-no-qualifying-type, property-no-vendor-prefix, media-feature-name-no-vendor-prefix\n\n//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: 700;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\ninput[type=\"search\"] {\n // Override content-box in Normalize (* isn't specific enough)\n .box-sizing(border-box);\n\n // Search inputs in iOS\n //\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n -webkit-appearance: none;\n appearance: none;\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n\n // Apply same disabled cursor tweak as for inputs\n // Some special care is needed because