commit ff3fb2cfe107c5722ab8490f30c8ed1bf192e6e5 Author: John.Miao Date: Tue May 13 09:52:42 2025 +0800 init diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..f808c94 Binary files /dev/null and b/.DS_Store differ diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..dc1fcdc --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,60 @@ +#!groovy +pipeline { + agent any + + stages { + stage('Build') { + steps { + sh 'mvn -B -DskipTests clean package' + } + } + stage('Test') { + steps { + echo 'Testing..' + } + } + stage('Deploy') { + steps { + echo 'Deploying....' + sshPublisher(publishers: [sshPublisherDesc( + configName: 'bnhz_test', + transfers: [ + sshTransfer(cleanRemote: false, + excludes: '', + execCommand: ''' + JAR_NAME="bnhz-admin.jar"; + PID=$(ps aux | grep "$JAR_NAME" | grep -v grep | awk \\'{print $2}\\') + if [ -n "$PID" ]; then + echo "Killing process $PID" + sudo kill $PID + sleep 5 + if ps -p $PID > /dev/null; then + echo "Process $PID did not terminate, force killing" + sudo kill -9 $PID && echo "Process $PID has been force killed" + else + echo "Process $PID terminated gracefully" + fi + else + echo "No process found related to $JAR_NAME" + fi + cd app/service + source /etc/profile + ./start.sh + echo $? + ''', + execTimeout: 120000, flatten: false, + makeEmptyDirs: false, + noDefaultExcludes: false, + patternSeparator: '[, ]+', + remoteDirectory: '/app/service', + remoteDirectorySDF: false, + removePrefix: 'bnhz-admin/target', + sourceFiles: 'bnhz-admin/target/bnhz-admin.jar')], + usePromotionTimestamp: false, + useWorkspaceInPromotion: false, + verbose: true) + ]) + } + } + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8564f29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2018 RuoYi + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..edd3735 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# 大气环保 +## 项目构建 +项目根目录执行 +```bash +mvn clean install +``` +## 项目启动 +项目产物位置`bnhz-admin/target/bnhz-admin.jar` +执行下面命令进行启动 +```bash +java -jar bnhz-admin.jar +``` +## 心跳包说明 +### 示例 +心跳包信息:**7e81ZHKCEAMS24080500017e** +### 包信息说明 +**7e**-是包头包尾 +**81**-代表标识符 +**HKCEAMS2408050001**-设备编号 + + diff --git a/bin/clean.bat b/bin/clean.bat new file mode 100644 index 0000000..24c0974 --- /dev/null +++ b/bin/clean.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] target· +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean + +pause \ No newline at end of file diff --git a/bin/package.bat b/bin/package.bat new file mode 100644 index 0000000..c693ec0 --- /dev/null +++ b/bin/package.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [Ϣ] Weḅwar/jarļ +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean package -Dmaven.test.skip=true + +pause \ No newline at end of file diff --git a/bin/run.bat b/bin/run.bat new file mode 100644 index 0000000..bcc4dd1 --- /dev/null +++ b/bin/run.bat @@ -0,0 +1,14 @@ +@echo off +echo. +echo [��Ϣ] ʹ��Jar��������Web���̡� +echo. + +cd %~dp0 +cd ../bnhz-admin/target + +set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m + +java -jar %JAVA_OPTS% bnhz-admin.jar + +cd bin +pause diff --git a/bnhz-adapter/pom.xml b/bnhz-adapter/pom.xml new file mode 100644 index 0000000..3ff05c2 --- /dev/null +++ b/bnhz-adapter/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + com.bnhz + daqi-back + 3.8.5 + + + bnhz-adapter + + + 8 + 8 + UTF-8 + + + + + com.bnhz + mqtt-broker + + + + com.bnhz + sip-server + + + com.bnhz + bnhz-common + + + + diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/controller/blackcar/BlackCarController.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/controller/blackcar/BlackCarController.java new file mode 100644 index 0000000..5f97218 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/controller/blackcar/BlackCarController.java @@ -0,0 +1,79 @@ +package com.bnhz.adapter.controller.blackcar; + +import cn.hutool.json.JSONUtil; +import com.bnhz.adapter.model.blackcar.*; +import com.bnhz.common.core.domain.BlackCarResult; +import com.bnhz.adapter.service.blackcar.IBlackCarService; +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 java.util.List; + +/** + * @author Leo + * @date 2024/6/15 16:03 + */ +@Slf4j +@RestController +@RequestMapping("/YC") +public class BlackCarController { + + @Autowired + private IBlackCarService blackCarService; + + + /** + * 新增点位信息 + */ + @PostMapping("/DWXX") + public BlackCarResult addDwxx(@RequestBody List points) { + blackCarService.insertPoint(points); + return BlackCarResult.ok(); + } + + + /** + * 新增交通流量信息 + */ + @PostMapping("/JTLL") + public BlackCarResult addJtll(@RequestBody List trafficFlowInfos) { + blackCarService.insertTrafficFlowInfo(trafficFlowInfos); + return BlackCarResult.ok(); + } + + + /** + * 新增黑烟车信息 + */ + @PostMapping("/HYC") + public BlackCarResult addHyc(@RequestBody List blackSmokeVehicles) { + blackCarService.insertBlackSmokeVehicle(blackSmokeVehicles); + return BlackCarResult.ok(); + } + + + /** + * 新增车流量 + */ + @PostMapping("/CLL") + public BlackCarResult addCll(@RequestBody List vehicleFlows) { + blackCarService.insertVehicleFlow(vehicleFlows); + return BlackCarResult.ok(); + } + + + /** + * 新增摄像头信息 + */ + @PostMapping("/SXTXX") + public BlackCarResult addSxtxx(@RequestBody List cameraInfos) { + blackCarService.insertCameraInfo(cameraInfos); + return BlackCarResult.ok(); + } + + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/controller/kacheck/KaCheckController.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/controller/kacheck/KaCheckController.java new file mode 100644 index 0000000..6aa002d --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/controller/kacheck/KaCheckController.java @@ -0,0 +1,71 @@ +package com.bnhz.adapter.controller.kacheck; + +import com.bnhz.adapter.model.kacheck.KaCameraInfo; +import com.bnhz.adapter.model.kacheck.KaPoint; +import com.bnhz.adapter.model.kacheck.KaTrafficFlowInfo; +import com.bnhz.adapter.model.kacheck.KaVehicleFlow; +import com.bnhz.adapter.service.kacheck.IKaCheckService; +import com.bnhz.common.core.domain.BlackCarResult; +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 java.util.List; + +/** + * @author Leo + * @date 2024/9/10 11:28 + */ + +@RestController +@RequestMapping("/KC/YC") +public class KaCheckController { + + + @Autowired + private IKaCheckService kaCheckService; + + + /** + * 新增点位信息 + */ + @PostMapping("/DWXX") + public BlackCarResult addDwxx(@RequestBody List points) { + kaCheckService.insertPoint(points); + return BlackCarResult.ok(); + } + + + /** + * 新增交通流量信息 + */ + @PostMapping("/JTLL") + public BlackCarResult addJtll(@RequestBody List trafficFlowInfos) { + kaCheckService.insertTrafficFlowInfo(trafficFlowInfos); + return BlackCarResult.ok(); + } + + + /** + * 新增车流量 + */ + @PostMapping("/CLL") + public BlackCarResult addCll(@RequestBody List vehicleFlows) { + kaCheckService.insertVehicleFlow(vehicleFlows); + return BlackCarResult.ok(); + } + + + /** + * 新增摄像头信息 + */ + @PostMapping("/SXTXX") + public BlackCarResult addSxtxx(@RequestBody List cameraInfos) { + kaCheckService.insertCameraInfo(cameraInfos); + return BlackCarResult.ok(); + } + + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/BlackCarId.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/BlackCarId.java new file mode 100644 index 0000000..331b411 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/BlackCarId.java @@ -0,0 +1,32 @@ +package com.bnhz.adapter.model.blackcar; + +import com.bnhz.iot.model.DataId; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @author Leo + * @date 2024/6/28 16:56 + */ +@Data +public abstract class BlackCarId implements DataId { + + /** + * 点位编号 + */ + @ApiModelProperty("点位编号") + @JsonProperty("DWBH") + private String dwbh; + + + public abstract LocalDateTime getDataTime(); + + + @Override + public String getSerialNumber() { + return this.dwbh; + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/BlackSmokeVehicle.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/BlackSmokeVehicle.java new file mode 100644 index 0000000..8174747 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/BlackSmokeVehicle.java @@ -0,0 +1,284 @@ +package com.bnhz.adapter.model.blackcar; + +import com.bnhz.common.annotation.Length; +import com.bnhz.common.utils.DateUtils; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 黑烟车信息表 + * + * @author Leo + * @date 2024/6/15 12:07 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class BlackSmokeVehicle extends BlackCarId { + + private Long id; + + // 黑烟车记录编号 + @JsonProperty("JLBH") + @ApiModelProperty("黑烟车记录编号") + private String jlbh; + + + // 线编号 + @JsonProperty("XBH") + @ApiModelProperty("线编号") + private String xbh; + + // 车道序号 + @JsonProperty("CDXH") + @ApiModelProperty("车道序号") + private String cdxh; + + // 抓拍时间 + @JsonProperty("ZPSJ") + @ApiModelProperty("抓拍时间") + private Date zpsj; + + // 号牌号码 + @JsonProperty("HPHM") + @ApiModelProperty("号牌号码") + private String hphm; + + // 车牌颜色 + @JsonProperty("HPYS") + @ApiModelProperty("车牌颜色") + private String hpys; + + // 号牌种类 + @JsonProperty("HPZL") + @ApiModelProperty("号牌种类") + private String hpzl; + + // 车辆类型 + @JsonProperty("CLLX") + @ApiModelProperty("车辆类型") + private String cllx; + + // 公安车辆类型 + @JsonProperty("GAVTYPE") + @ApiModelProperty("公安车辆类型") + private String gavtype; + + // 抓拍次数 + @JsonProperty("ZPCS") + @ApiModelProperty("抓拍次数") + private Integer zpcs; + + // 黑烟等级 + @JsonProperty("HYDJ") + @ApiModelProperty("黑烟等级") + private Integer hydj; + + // 判定结果 + @JsonProperty("PDJG") + @ApiModelProperty("判定结果") + private Integer pdjg; + + // 车辆所属地 + @JsonProperty("CLSSD") + @ApiModelProperty("车辆所属地") + private String clssd; + + // 后置车尾图片1 + @JsonProperty("TP1") + @ApiModelProperty("后置车尾图片1") + @Length(200) + private String tp1; + + // 后置车尾图片2 + @JsonProperty("TP2") + @ApiModelProperty("后置车尾图片2") + @Length(200) + private String tp2; + + // 前置车头图片1 + @JsonProperty("TP3") + @ApiModelProperty("前置车头图片1") + @Length(200) + private String tp3; + + // 车牌号码图片 + @JsonProperty("TP4") + @ApiModelProperty("车牌号码图片") + @Length(200) + private String tp4; + + // 黑烟车证据图片 + @JsonProperty("TP5") + @ApiModelProperty("黑烟车证据图片") + @Length(200) + private String tp5; + + // 前置车头图片2 + @JsonProperty("TP6") + @ApiModelProperty("前置车头图片2") + @Length(200) + private String tp6; + + // 车牌特写图 + @JsonProperty("TP7") + @ApiModelProperty("车牌特写图") + @Length(200) + private String tp7; + + // 黑烟车证据视频 + @ApiModelProperty("黑烟车证据视频") + @JsonProperty("SP") + @Length(200) + private String sp; + + // 是否已推送交警 + @ApiModelProperty("是否已推送交警") + @JsonProperty("SFYTS") + private String sfyts; + + // 推送时间 + @ApiModelProperty("推送时间") + @JsonProperty("TSSJ") + private Date tssj; + + // 审核时间 + @JsonProperty("SHSJ") + @ApiModelProperty("审核时间") + private Date shsj; + + // 燃料种类 + @JsonProperty("RLZL") + @ApiModelProperty("燃料种类") + private String rlzl; + + // 城市编号 + @JsonProperty("CityCode") + @ApiModelProperty("城市编号") + private String cityCode; + + // 区县编号 + @JsonProperty("CountyCode") + @ApiModelProperty("区县编号") + private String countyCode; + + // 车辆速度 + @JsonProperty("CLSD") + @ApiModelProperty("车辆速度") + private BigDecimal clsd; + + // 车辆加速度 + @JsonProperty("CLJSD") + @ApiModelProperty("车辆加速度") + private BigDecimal cljsd; + + // VSP + @JsonProperty("VSP") + @ApiModelProperty("VSP") + private BigDecimal vsp; + + // 风速 + @JsonProperty("FS") + @ApiModelProperty("风速") + private BigDecimal fs; + + // 风向 + @JsonProperty("FX") + @ApiModelProperty("风向") + private String fx; + + // 环境温度 + @JsonProperty("HJWD") + @ApiModelProperty("环境温度") + private BigDecimal hjwd; + + // 湿度 + @JsonProperty("SD") + @ApiModelProperty("湿度") + private BigDecimal sd; + + // 大气压 + @JsonProperty("DQY") + @ApiModelProperty("大气压") + private BigDecimal dqy; + + // 地点经度 + @JsonProperty("DDJD") + @ApiModelProperty("地点经度") + private BigDecimal ddjd; + + // 地点纬度 + @JsonProperty("DDWD") + @ApiModelProperty("地点纬度") + private BigDecimal ddwd; + + // 车道坡度 + @JsonProperty("CDPD") + @ApiModelProperty("车道坡度") + private BigDecimal cdpd; + + // 后置车尾图片1的HTTP链接 + @JsonProperty("TP1_HTTP") + @ApiModelProperty("后置车尾图片1的HTTP链接") + private String tp1Http; + + // 后置车尾图片2的HTTP链接 + @JsonProperty("TP2_HTTP") + @ApiModelProperty("后置车尾图片2的HTTP链接") + private String tp2Http; + + // 前置车头图片1的HTTP链接 + @JsonProperty("TP3_HTTP") + @ApiModelProperty("前置车头图片1的HTTP链接") + private String tp3Http; + + // 车牌号码图片的HTTP链接 + @JsonProperty("TP4_HTTP") + @ApiModelProperty("车牌号码图片的HTTP链接") + private String tp4Http; + + // 黑烟车证据图片的HTTP链接 + @JsonProperty("TP5_HTTP") + @ApiModelProperty("黑烟车证据图片的HTTP链接") + private String tp5Http; + + // 前置车头图片2的HTTP链接 + @JsonProperty("TP6_HTTP") + @ApiModelProperty("前置车头图片2的HTTP链接") + private String tp6Http; + + // 车牌特写图的HTTP链接 + @JsonProperty("TP7_HTTP") + @ApiModelProperty("车牌特写图的HTTP链接") + private String tp7Http; + + // 黑烟车证据视频的HTTP链接 + @JsonProperty("SP_HTTP") + @ApiModelProperty("黑烟车证据视频的HTTP链接") + private String spHttp; + + // 点位地址 + @JsonProperty("DWDZ") + @ApiModelProperty("点位地址") + @Length(200) + private String dwdz; + + // 设备编号 + @JsonProperty("SBBH") + @ApiModelProperty("设备编号") + private String sbbh; + + private Date createTime; + + private Date updateTime; + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(zpsj); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/CameraInfo.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/CameraInfo.java new file mode 100644 index 0000000..d798fad --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/CameraInfo.java @@ -0,0 +1,112 @@ +package com.bnhz.adapter.model.blackcar; + +import com.bnhz.common.annotation.Length; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 摄像头信息表 + * @author Leo + * @date 2024/6/15 15:15 + */ +@Data +public class CameraInfo extends BlackCarId { + + private Long id; + + // 摄像头编号 + @JsonProperty("SXTBH") + @ApiModelProperty("摄像头编号") + private String sxtbh; + + // 摄像头名称 + @JsonProperty("SXTMC") + @ApiModelProperty("摄像头名称") + private String sxtmc; + + // 摄像头类型,1: 海康,2: 大华,3: 其他 + @JsonProperty("SPLX") + @ApiModelProperty("摄像头类型") + private Integer splx; + + // 是否有效,0: 无效,1: 有效 + @JsonProperty("SFYX") + @ApiModelProperty("是否有效") + private Integer sfyx; + + + // IP地址 + @JsonProperty("IP") + @ApiModelProperty("IP地址") + private String ip; + + // 联网状态,0: 断开,1: 在线 + @JsonProperty("LWZT") + @ApiModelProperty("联网状态") + private Integer lwzt; + + // 摄像头类型,1: 前置,2: 后置 + @JsonProperty("SXTLX") + @ApiModelProperty("摄像头类型") + private Integer sxtlx; + + // 摄像头朝向 + @JsonProperty("SXTCX") + @ApiModelProperty("摄像头朝向") + private String sxtcx; + + // 抓拍车道数 + @JsonProperty("ZPCDS") + @ApiModelProperty("抓拍车道数") + private Integer zpcds; + + // 通道号 + @JsonProperty("TDH") + @ApiModelProperty("通道号") + private String tdh; + + // 摄像头备案号 + @JsonProperty("SXTBAH") + @ApiModelProperty("摄像头备案号") + private String sxtbah; + + // HLS播放地址高清 + @JsonProperty("HLSGQ") + @ApiModelProperty("HLS播放地址高清") + @Length(200) + private String hlsgq; + + // HLS播放地址流畅 + @JsonProperty("HLSLC") + @ApiModelProperty("HLS播放地址流畅") + @Length(200) + private String hlslc; + + // 是否启用,1: 正常,2: 调试,3: 暂停 + @JsonProperty("SFQY") + @ApiModelProperty("是否启用") + private Integer sfqy; + + // 登录账号 + @JsonProperty("DLZH") + @ApiModelProperty("登录账号") + private String dlzh; + + // 登录密码 + @JsonProperty("DLMM") + @ApiModelProperty("登录密码") + private String dlmm; + + private Date createTime; + + private Date updateTime; + + @Override + public LocalDateTime getDataTime() { + return LocalDateTime.now(); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/Point.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/Point.java new file mode 100644 index 0000000..55e4f5b --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/Point.java @@ -0,0 +1,111 @@ +package com.bnhz.adapter.model.blackcar; + +import com.bnhz.common.annotation.Length; +import com.bnhz.common.utils.DateUtils; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.experimental.FieldNameConstants; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 点位信息 + * + * @author Leo + * @date 2024/6/14 17:46 + */ + +@ToString +@EqualsAndHashCode(callSuper = true) +@Data +@FieldNameConstants +public class Point extends BlackCarId { + + private Long bisId; + + @JsonProperty("DWMC") + @ApiModelProperty("点位名称") + private String dwmc; + + @JsonProperty("DWLX") + @ApiModelProperty("点位类型") + private String dwlx; + + @JsonProperty("YXRQ") + @JsonFormat(pattern = "yyyy-MM-dd") + @ApiModelProperty("运行日期") + private Date yxrq; + + @JsonProperty("DWZT") + @ApiModelProperty("点位状态") + private String dwzt; + + @JsonProperty("DWDZ") + @ApiModelProperty("点位地址") + @Length(200) + private String dwdz; + + @JsonProperty("DDJD") + @ApiModelProperty("地点经度") + private BigDecimal ddjd; + + @JsonProperty("DDWD") + @ApiModelProperty("地点纬度") + private BigDecimal ddwd; + + @JsonProperty("CLFX") + @ApiModelProperty("车流方向") + private String clfx; + + @JsonProperty("CDSL") + @ApiModelProperty("车道数量") + private Integer cdsl; + + @JsonProperty("CDPD") + @ApiModelProperty("车道坡度") + private BigDecimal cdpd; + + @JsonProperty("YCXS") + @ApiModelProperty("遥测线数") + private Integer ycxs; + + @JsonProperty("HPHM") + @ApiModelProperty("号牌号码") + private String hphm; + + @JsonProperty("CLXH") + @ApiModelProperty("装载车型号") + private String clxh; + + @JsonProperty("ZYLX") + @ApiModelProperty("点位作用类型") + private String zylx; + + @JsonProperty("SSSP") + @ApiModelProperty("实时视频链接") + @Length(value = 200) + private String sssp; + + @JsonProperty("CityCode") + @ApiModelProperty("城市编号") + private String cityCode; + + @JsonProperty("CountyCode") + @ApiModelProperty("区县编号") + private String countyCode; + + private Date createTime; + + private Date updateTime; + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(yxrq); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/PointEnvironmentalData.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/PointEnvironmentalData.java new file mode 100644 index 0000000..dbbf1bc --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/PointEnvironmentalData.java @@ -0,0 +1,91 @@ +package com.bnhz.adapter.model.blackcar; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 点位环境空气质量记录表 + * @author Leo + * @date 2024/6/15 11:14 + */ +@Data +public class PointEnvironmentalData { + + private Long id; + + // 记录时间,主键,格式为YYYY-MM-DD HH:mm:SS + @JsonProperty("JLSJ") + //@JsonFormat(pattern = "yyyy-MM-ddTHH:mm:ss") + private Date jlsj; + + // 点位编号,主键 + @JsonProperty("DWBH") + private String dwbh; + + // PM2.5浓度,单位为微克/立方米 + @JsonProperty("PM25") + private BigDecimal pm25; + + // PM10浓度,单位为微克/立方米 + @JsonProperty("PM10") + private BigDecimal pm10; + + // 一氧化碳浓度,单位为毫克/立方米 + @JsonProperty("CO") + private BigDecimal co; + + // 二氧化硫浓度,单位为微克/立方米 + @JsonProperty("SO2") + private BigDecimal so2; + + // 臭氧浓度,单位为微克/立方米 + @JsonProperty("O3") + private BigDecimal o3; + + // 二氧化氮浓度,单位为微克/立方米 + @JsonProperty("NO2") + private BigDecimal no2; + + // 氨气浓度,单位为微克/立方米 + @JsonProperty("NH3") + private BigDecimal nh3; + + // 挥发性有机化合物浓度,单位为微克/立方米 + @JsonProperty("VOC") + private BigDecimal voc; + + // 总挥发性有机化合物浓度,单位为微克/立方米 + @JsonProperty("TVOC") + private BigDecimal tvoc; + + // 硫化氢浓度,单位为微克/立方米 + @JsonProperty("H2S") + private BigDecimal h2s; + + // 温度,单位为摄氏度 + @JsonProperty("WD") + private BigDecimal wd; + + // 湿度,百分比 + @JsonProperty("SD") + private BigDecimal sd; + + // 大气压,单位为帕斯卡 + @JsonProperty("DQY") + private BigDecimal dqy; + + // 风速,单位为米/秒 + @JsonProperty("FS") + private BigDecimal fs; + + // 空气质量指数 + @JsonProperty("AQI") + private BigDecimal aqi; + + private Date createTime; + + private Date updateTime; +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/PointMonitoringLine.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/PointMonitoringLine.java new file mode 100644 index 0000000..23fe098 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/PointMonitoringLine.java @@ -0,0 +1,108 @@ +package com.bnhz.adapter.model.blackcar; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 点位遥测线信息表 + * + * @author Leo + * @date 2024/6/14 18:16 + */ +@Data +public class PointMonitoringLine { + + private Long id; + + @JsonProperty("DWBH") + private String dwbh; // 点位编号 + + @JsonProperty("YCXBH") + private String ycxbh; // 运测线编号 + + @JsonProperty("CDXH") + private String cdxh; // 车道序号 + + @JsonProperty("JCXTXH") + private String jcxtxh; // 监测系统型号 + + @JsonProperty("JCXTMC") + private String jcxtmc; // 监测系统名称 + + @JsonProperty("JCXTBH") + private String jcxtbh; // 监测系统编号 + + @JsonProperty("JCXTZZC") + private String jcxtzzc; // 监测系统制造厂 + + @JsonProperty("CSYXH") + private String csyxh; // 测速仪型号 + + @JsonProperty("CSYSCC") + private String csyscc; // 测速仪生产厂 + + @JsonProperty("CSYYXQ") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date csyyxq; // 测速仪有效期 + + @JsonProperty("QTCSYXH") + private String qtcsxh; // 气体测试仪型号 + + @JsonProperty("QTCSYSCC") + private String qtyssc; // 气体测试仪生产厂 + + @JsonProperty("QTCSYYXQ") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date qtcsyyxq; // 气体测试仪有效期 + + @JsonProperty("YDJXH") + private String ydjxh; // 烟度计型号 + + @JsonProperty("YDJSCC") + private String ydjscc; // 烟度计生产厂 + + @JsonProperty("YDJYXQ") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date ydjyxq; // 烟度计有效期 + + @JsonProperty("SXJXH") + private String sxjxh; // 摄像机型号 + + @JsonProperty("SXXTSCC") + private String sxxtscc; // 摄像机生产厂 + + @JsonProperty("SXXTYXQ") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date sxxtyxq; // 摄像机有效期 + + @JsonProperty("PDJXH") + private String pdjxh; // 坡度计型号 + + @JsonProperty("PDJSCC") + private String pdjscc; // 坡度计生产厂 + + @JsonProperty("PDJYXQ") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date pdjyxq; // 坡度计有效期 + + @JsonProperty("QXZXH") + private String qxzxh; // 气象站型号 + + @JsonProperty("QXZSCC") + private String qxzscc; // 气象站生产厂 + + @JsonProperty("QXZYXQ") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date qxzyxq; // 气象站有效期 + + @JsonProperty("YCXLX") + private String ycxlx; // 运测线类型 + + private Date createTime; + + private Date updateTime; +} + diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/RunLocationRecord.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/RunLocationRecord.java new file mode 100644 index 0000000..82c2a90 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/RunLocationRecord.java @@ -0,0 +1,89 @@ +package com.bnhz.adapter.model.blackcar; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * @author Leo + * @date 2024/6/15 09:56 + */ +@Data +public class RunLocationRecord { + + private Long id; + + /** + * 点位编号 + */ + @JsonProperty("DWBH") + private String dwbh; + + /** + * 遥测线编号 + */ + @JsonProperty("YCXBH") + private String ycxbh; + + /** + * 监测点位日志号 + */ + @JsonProperty("JCDWRZH") + private String jcdwrzh; + + /** + * 车道序号 + */ + @JsonProperty("CDXH") + private String cdxh; + + /** + * 车流方向 1-上行 2-下行 + */ + @JsonProperty("CLFX") + private String clfx; + + /** + * 运行地址 + */ + @JsonProperty("YXDZ") + private String yxdz; + + /** + * 地点经度 + */ + @JsonProperty("DDJD") + private BigDecimal ddjd; + + /** + * 地点纬度 + */ + @JsonProperty("DDWD") + private BigDecimal ddwd; + + /** + * 车道坡度 + */ + @JsonProperty("CDPD") + private BigDecimal cdpd; + + /** + * 运行开始时间 + */ + @JsonProperty("YXKSSJ") + //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date yxkssj; + + /** + * 运行结束时间 + */ + @JsonProperty("YXJSSJ") + //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date yxjssj; + + private Date createTime; + + private Date updateTime; +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/TrafficFlowInfo.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/TrafficFlowInfo.java new file mode 100644 index 0000000..5d110e6 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/TrafficFlowInfo.java @@ -0,0 +1,195 @@ +package com.bnhz.adapter.model.blackcar; + +import com.bnhz.common.annotation.Length; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 交通流量信息 + * + * @author Leo + * @date 2024/6/15 10:05 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class TrafficFlowInfo extends BlackCarId{ + + private Long id; + + + /** + * 流量编号 + */ + @JsonProperty("LLBH") + @ApiModelProperty("流量编号") + private String llbh; + + /** + * 监测点位日志号 + */ + @JsonProperty("JCDWRZH") + @ApiModelProperty("监测点位日志号") + private String jcdwrzh; + + /** + * 所属道路 + */ + @JsonProperty("SSDL") + @ApiModelProperty("所属道路") + @Length(value = 200) + private String ssdl; + + /** + * 流量分类 + */ + @JsonProperty("LLFL") + @ApiModelProperty("流量分类") + private String llfl; + + /** + * 统计时长 + */ + @JsonProperty("TJSC") + @ApiModelProperty("统计时长") + private String tjsc; + + /** + * 采集时段 + */ + @JsonProperty("CJSD") + @ApiModelProperty("采集时段") + private Integer cjsd; + + /** + * 采集序号 + */ + @JsonProperty("CJXH") + @ApiModelProperty("采集序号") + private Integer cjxh; + + /** + * 统计日期 + */ + @JsonProperty("TTRQ") + @ApiModelProperty("统计日期") + private Date ttrq; + + /** + * 车道序号 + */ + @JsonProperty("CDXH") + @ApiModelProperty("车道序号") + private String cdxh; + + /** + * 微小型客车数 + */ + @JsonProperty("WXXKCS") + @ApiModelProperty("微小型客车数") + private Integer wxxkcs; + + /** + * 中型客车数 + */ + @JsonProperty("ZXKCS") + @ApiModelProperty("中型客车数") + private Integer zxkcs; + + /** + * 大型客车数 + */ + @JsonProperty("DXKCS") + @ApiModelProperty("大型客车数") + private Integer dxkcs; + + /** + * 小型货车数 + */ + @JsonProperty("XXHCS") + @ApiModelProperty("小型货车数") + private Integer xxhcs; + + /** + * 中型货车数 + */ + @JsonProperty("ZXHCS") + @ApiModelProperty("中型货车数") + private Integer zxhcs; + + /** + * 重型货车数 + */ + @JsonProperty("ZXHCS1") + @ApiModelProperty("重型货车数") + private Integer zxhcs1; + + /** + * 通行车辆数 + */ + @JsonProperty("TXCLS") + @ApiModelProperty("通行车辆数") + private Integer txcls; + + /** + * 平均速度 + */ + @JsonProperty("PJSD") + @ApiModelProperty("平均速度") + private Integer pjsd; + + /** + * 平均排队长度 + */ + @JsonProperty("PJPDCD") + @ApiModelProperty("平均排队长度") + private Integer pjpdc; + + /** + * 汽油车数 + */ + @JsonProperty("QYCS") + @ApiModelProperty("汽油车数") + private Integer qycs; + + /** + * 柴油车数 + */ + @JsonProperty("CYCS") + @ApiModelProperty("柴油车数") + private Integer cycs; + + /** + * 本地市车辆数 + */ + @JsonProperty("BDCS") + @ApiModelProperty("本地市车辆数") + private Integer bdcs; + + /** + * 本省车辆数 + */ + @JsonProperty("BSCS") + @ApiModelProperty("本省车辆数") + private Integer bscs; + + /** + * 外省车辆数 + */ + @JsonProperty("WSCS") + @ApiModelProperty("外省车辆数") + private Integer wscs; + + private Date createTime; + + private Date updateTime; + + @Override + public LocalDateTime getDataTime() { + return LocalDateTime.now(); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/VehicleData.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/VehicleData.java new file mode 100644 index 0000000..f7bf7b3 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/VehicleData.java @@ -0,0 +1,78 @@ +package com.bnhz.adapter.model.blackcar; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 车辆数据信息表 + * @author Leo + * @date 2024/6/15 11:56 + */ +@Data +public class VehicleData { + + private Long id; + + // 行政区划代码,符合GB/T2260,车辆注册地的6位区划代码 + @JsonProperty("XZQHDM") + private String xzqhdm; + // 号牌号码 + @JsonProperty("HPHM") + private String hphm; + // 车牌颜色 + @JsonProperty("HPYS") + private String hpys; + // 号牌种类,参考号牌种类数据约束,例如:01 + @JsonProperty("HPZL") + private String hpzl; + // 车辆型号 + @JsonProperty("CLXH") + private String clxh; + // 生产企业 + @JsonProperty("SCQY") + private String scqy; + // 燃料种类 + @JsonProperty("RLZL") + private String rlzl; + // 使用性质 + @JsonProperty("SYXZ") + private String syxz; + // 初次登记日期,格式YYYY-MM-DD + @JsonProperty("CCDJRQ") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date ccdjrq; + // 车辆识别代号 + @JsonProperty("CLSBDH") + private String clsbdh; + // 排放标准阶段 + @JsonProperty("PFBZJD") + private String pfbzjd; + // 车辆类型,参考公安车辆类型数据约束 + @JsonProperty("GAVTYPE") + private String gavtype; + // 车辆品牌 + @JsonProperty("CLPP") + private String clpp; + // 发动机型号 + @JsonProperty("FDJXH") + private String fdjxh; + // 发动机生产厂家 + @JsonProperty("FDJSCCJ") + private String fdjsccj; + //车主 + @JsonProperty("CZ") + private String cz; + // 车主地址 + @JsonProperty("CZDZ") + private String czdz; + // 车主联系电话 + @JsonProperty("CZLXDH") + private String czlxdh; + + private Date createTime; + + private Date updateTime; +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/VehicleFlow.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/VehicleFlow.java new file mode 100644 index 0000000..7d97066 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/VehicleFlow.java @@ -0,0 +1,134 @@ +package com.bnhz.adapter.model.blackcar; + +import com.bnhz.common.annotation.Length; +import com.bnhz.common.utils.DateUtils; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 车流量 + * + * @author Leo + * @date 2024/6/15 14:53 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class VehicleFlow extends BlackCarId { + + + private Long id; + + //车流量记录编号 + @JsonProperty("LLBH") + @ApiModelProperty("车流量记录编号") + private String llbh; + + // 车牌号 + @JsonProperty("HPHM") + @ApiModelProperty("车牌号") + private String hphm; + + // 车牌颜色 + @JsonProperty("HPYS") + @ApiModelProperty("车牌颜色") + private String hpys; + // 抓拍时间 + @JsonProperty("ZPSJ") + @ApiModelProperty("抓拍时间") + //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date zpsj; + + // 是否被抓拍 + @JsonProperty("SFBZP") + @ApiModelProperty("是否被抓拍") + private Integer sfbzp; + // 行政区划代码 + @JsonProperty("XZQHDM") + @ApiModelProperty("行政区划代码") + private String xzqhdm; + + // 车流速度 + @JsonProperty("CLSD") + @ApiModelProperty("车流速度") + private BigDecimal clsd; + + // 车辆所属地 + @JsonProperty("CLSSD") + @ApiModelProperty("车辆所属地") + private Integer clssd; + + // 车道号 + @JsonProperty("CDXH") + @ApiModelProperty("车道号") + private Integer cdxh; + + // 车身颜色中文名(可空) + @JsonProperty("CSYS") + @ApiModelProperty("车身颜色中文名") + private String csys; + // 号牌种类(可空) + @JsonProperty("HPZL") + @ApiModelProperty("号牌种类") + private String hpzl; + // 车辆类型 + @JsonProperty("CLLX") + @ApiModelProperty("车辆类型") + private String cllx; + // 燃料种类 + @JsonProperty("RLZL") + @ApiModelProperty("燃料种类") + private String rlzl; + // 全景图(可空) + @JsonProperty("TP1") + @ApiModelProperty("全景图") + @Length(200) + private String tp1; + // 号牌图片(可空) + @JsonProperty("tp2") + @ApiModelProperty("号牌图片") + @Length(200) + private String tp2; + // 视频(可空) + @JsonProperty("sp1") + @ApiModelProperty("视频") + @Length(200) + private String sp1; + // 后置车尾图片1 + @JsonProperty("TP1_HTTP") + @ApiModelProperty("后置车尾图片1") + @Length(200) + private String tp1Http; + + @JsonProperty("TP2_HTTP") + @ApiModelProperty("后置车尾图片2") + @Length(200) + private String tp2Http; // 后置车尾图片2 + + @JsonProperty("SP1_HTTP") + @ApiModelProperty("黑烟车证据视频") + @Length(200) + private String sp1Http; // 黑烟车证据视频 + + @JsonProperty("CityCode") + @ApiModelProperty("城市编号") + private String cityCode; // 城市编号 + + @JsonProperty("CountyCode") + @ApiModelProperty("区县编号") + private String countyCode; // 区县编号 + + private Date createTime; + + private Date updateTime; + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(zpsj); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcDeviceCheck.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcDeviceCheck.java new file mode 100644 index 0000000..7ecda3a --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcDeviceCheck.java @@ -0,0 +1,78 @@ +package com.bnhz.adapter.model.blackcar; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 遥测设备检查信息表 + * + * @author Leo + * @date 2024/6/15 11:44 + */ +@Data +public class YcDeviceCheck { + + private Long id; + + // 点位编号 + @JsonProperty("DWBH") + private String dwbh; + // 遥测线编号 + @JsonProperty("YCXBH") + private String ycxbh; + // 检查记录编号,主键:点位编号+遥测线编号+检查开始时间 + @JsonProperty("JCJILBH") + private String jcjilbh; + // 检查类型,1-静态,2-动态 + @JsonProperty("JCLX") + private Integer jclx; + // 检查开始时间 + @JsonProperty("JCKSSJ") + //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date jckssj; + + // 是否通过,Y/N + @JsonProperty("SFTG") + private String sftg; + // 备注 + @JsonProperty("BZ") + private String bz; + // 检查单位 + @JsonProperty("JCDW") + private String jcdw; + // 检查人员 + @JsonProperty("JCRY") + private String jcry; + // 标气类别,1-高标气(标准)0低标气(检查) + @JsonProperty("BQLB") + private String bqlb; + // 行驶速度,动态检查 + @JsonProperty("XSSD") + private BigDecimal xssd; + // CO2标准值 + @JsonProperty("CO2BZZ") + private BigDecimal co2bzz; + // CO2测量值 + @JsonProperty("CO2CLZ") + private BigDecimal co2clz; + // CO标准值 + @JsonProperty("COBZZ") + private BigDecimal cobzz; + // CO测量值 + @JsonProperty("COCLZ") + private BigDecimal cocLz; + // NO标准值 + @JsonProperty("NOBZZ") + private BigDecimal nobzz; + // NO测量值 + @JsonProperty("NOCLZ") + private BigDecimal noclz; + + private Date createTime; + + private Date updateTime; + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcDeviceSelfCheck.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcDeviceSelfCheck.java new file mode 100644 index 0000000..3b8d7f4 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcDeviceSelfCheck.java @@ -0,0 +1,102 @@ +package com.bnhz.adapter.model.blackcar; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 遥测设备自检信息表 + * + * @author Leo + * @date 2024/6/15 11:30 + */ +@Data +public class YcDeviceSelfCheck { + + private Long id; + + @JsonProperty("DWBH") + private String dwbh; + + @JsonProperty("YCXBH") + private String ycxbh; + + @JsonProperty("ZJJLBH") + private String zjjlbh; + + @JsonProperty("ZJKSRQ") + //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date zjksrq; + + @JsonProperty("SFTG") + private String sftg; // 是否通过 + + @JsonProperty("BZ") + private String bz; // 备注 + + @JsonProperty("CO2BZZ") + private BigDecimal co2bzz; // CO2标准值 + + @JsonProperty("COBZZ") + private BigDecimal cobzz; // CO标准值 + + @JsonProperty("DEXBZZ") + private BigDecimal dexbzz; // 1,3-丁二烯标准值 + + @JsonProperty("BWBZZ") + private BigDecimal bwbzz; // 丙烷标准值 + + @JsonProperty("NOBZZ") + private BigDecimal nobzz; // NO标准值 + + @JsonProperty("CO2CLZ") + private BigDecimal co2clz; // CO2测量值 + + @JsonProperty("COCLZ") + private BigDecimal coclz; // CO测量值 + + @JsonProperty("DEXCLZ") + private BigDecimal dexclz; // 1,3-丁二烯测量值 + + @JsonProperty("BWCLZ") + private BigDecimal bwclz; // 丙烷测量值 + + @JsonProperty("NOCLZ") + private BigDecimal noclz; // NO测量值 + + @JsonProperty("YDP1BZZ") + private BigDecimal ydp1bzz; // 烟度片1标准值 + + @JsonProperty("YDP2BZZ") + private BigDecimal ydp2bzz; // 烟度片2标准值 + + @JsonProperty("YDP3BZZ") + private BigDecimal ydp3bzz; // 烟度片3标准值 + + @JsonProperty("YDP4BZZ") + private BigDecimal ydp4bzz; // 烟度片4标准值 + + @JsonProperty("YDP5BZZ") + private BigDecimal ydp5bzz; // 烟度片5标准值 + + @JsonProperty("YDP1CLZ") + private BigDecimal ydp1clz; // 烟度片1测量值 + + @JsonProperty("YDP2CLZ") + private BigDecimal ydp2clz; // 烟度片2测量值 + + @JsonProperty("YDP3CLZ") + private BigDecimal ydp3clz; // 烟度片3测量值 + + @JsonProperty("YDP4CLZ") + private BigDecimal ydp4clz; // 烟度片4测量值 + + @JsonProperty("YDP5CLZ") + private BigDecimal ydp5clz; // 烟度片5测量值 + + private Date createTime; + + private Date updateTime; +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcFailure.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcFailure.java new file mode 100644 index 0000000..8540aee --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcFailure.java @@ -0,0 +1,49 @@ +package com.bnhz.adapter.model.blackcar; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.Date; + +/** + * 遥测不合格表 + * + * @author Leo + * @date 2024/6/15 15:20 + */ +@Data +public class YcFailure { + + + private Long id; + + // 监测时间 + @JsonProperty("JCSJ") + //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date jcsj; + + + // 号牌号码 + @JsonProperty("HPHM") + private String hphm; + + // 车牌颜色 + @JsonProperty("hpys") + private String hpys; + + // 车辆类型 + @JsonProperty("CLLX") + private String cllx; + + // 燃料种类 + @JsonProperty("RLZL") + private String rlzl; + + // 在该点位被抓拍次数 + @JsonProperty("ZPCS") + private Integer zpcs; + + private Date createTime; + + private Date updateTime; +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcMonitoringData.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcMonitoringData.java new file mode 100644 index 0000000..1e59143 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/blackcar/YcMonitoringData.java @@ -0,0 +1,249 @@ +package com.bnhz.adapter.model.blackcar; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 遥感监测数据信息表 + * @author Leo + * @date 2024/6/15 10:32 + */ +@Data +public class YcMonitoringData { + + private Long id; + + // 记录编号,长度为30,不可为空 + @JsonProperty("JLBH") + private String jlbh; + + // 点位编号,长度为10,不可为空 + @JsonProperty("DWBH") + private String dwbh; + + // 遥测线编号,长度为2,不可为空 + @JsonProperty("YCXBH") + private String ycxbh; + + // 监测点位日志号,长度为8,可为空 + @JsonProperty("JCDWRZH") + private String jcdwrzh; + + // 监测人员姓名,长度为50,可为空 + @JsonProperty("JCRYXM") + private String jcryxm; + + // 车道序号,长度为6,不可为空 + @JsonProperty("CDXH") + private String cdxh; + + // 监测时间 + @JsonProperty("JCRQ") + private Date jcrq; + + // 地点经度,长度为10,小数点后5位,不可为空 + @JsonProperty("DDJD") + private BigDecimal ddjd; + + // 地点纬度,长度为10,小数点后5位,不可为空 + @JsonProperty("DDWD") + private BigDecimal ddwd; + + // 车道坡度,长度为3,小数点后2位,不可为空 + @JsonProperty("CDPD") + private BigDecimal cdpd; + + // 判定结果,长度为1,不可为空,0-超标,1-合格,2-不判定,3-无效 + @JsonProperty("PDJG") + private String pdjg; + // 号牌号码,长度为18,不可为空 + @JsonProperty("HPHM") + private String hphm; + + // 车牌颜色,长度为2,不可为空 + @JsonProperty("CPYS") + private String cpys; + + // 号牌种类,长度为2,不可为空 + @JsonProperty("HPZL") + private String hpzl; + + // 燃料种类,长度为1,不可为空 + @JsonProperty("RLZL") + private String rlzl; + + // CO2 结果,单位为%,长度为3.2,可为空 + @JsonProperty("CO2JG") + private BigDecimal co2jg; + + // CO/CO2 比率,单位为%,长度为3.2,可为空 + @JsonProperty("COCO2") + private BigDecimal coco2; + + // HC/CO2 比率,单位为%,长度为3.2,可为空 + @JsonProperty("HCCO2") + private BigDecimal hcco2; + + // NO/CO2 比率,单位为%,长度为3.2,可为空 + @JsonProperty("NOCO2") + private BigDecimal noco2; + + // CO 结果,单位为%,长度为3.2,可为空 + @JsonProperty("COJG") + private BigDecimal cojg; + + // HC 结果,单位为ppm,长度为4.2,可为空 + @JsonProperty("HCJG") + private BigDecimal hcjg; + + // NO 结果,单位为ppm,长度为5.2,可为空 + @JsonProperty("NOJG") + private BigDecimal nojg; + + // 不透光度结果,单位为%,长度为3.2,柴油车适用,可为空 + @JsonProperty("BTGDJG") + private BigDecimal btgdjg; + + // 林格曼黑度,0~5,长度为1,可为空 + @JsonProperty("LGMHD") + private Integer lgmhd; + + // CO 限值,单位为%,长度为3.2,可为空 + @JsonProperty("COXZ") + private BigDecimal coxz; + + // NO 限值,单位为ppm,长度为5.2,不可为空 + @JsonProperty("NOXZ") + private BigDecimal noxz; + + // 不透光度限值,单位为%,长度为3.2,柴油车适用,可为空 + @JsonProperty("BTGDXZ") + private BigDecimal btgdxz; + + // 黑度限值,0~5,长度为1,可为空 + @JsonProperty("HDXZ") + private Integer hdxz; + + // HC限值,单位为ppm,长度为5.2,可为空 + @JsonProperty("HCXZ") + private BigDecimal hcxz; + + // 车辆速度,长度为5.2,不可为空 + @JsonProperty("CLSD") + private BigDecimal clsd; + + // 车辆加速度,长度为5.2,不可为空 + @JsonProperty("CLJSD") + private BigDecimal cljsd; + + // VSP,长度为3.2,不可为空 + @JsonProperty("VSP") + private BigDecimal vsp; + + // 风速,单位m/s,长度为3.2,不可为空 + @JsonProperty("FS") + private BigDecimal fs; + + // 风向,长度为3,不可为空 + @JsonProperty("FX") + private String fx; + + // 环境温度,单位℃,长度为3.2,不可为空 + @JsonProperty("HJWD") + private BigDecimal hjwd; + + // 湿度,单位%,长度为3.2,不可为空 + @JsonProperty("SD") + private BigDecimal sd; + + // 大气压,单位kPa,长度为3.2,不可为空 + @JsonProperty("DQY") + private BigDecimal dqy; + + // 轨迹信息编号,长度为12,不可为空 + @JsonProperty("GJXXBH") + private String gjxxbh; + + // 车头图像1,绝对路径,长度为200,不可为空 + @JsonProperty("TP1") + private String tp1; + + // 车头图像2,绝对路径,长度为200,不可为空 + @JsonProperty("TP2") + private String tp2; + + // 车牌图片,绝对路径,长度为200,不可为空 + @JsonProperty("TP3") + private String tp3; + + // 视频文件,绝对路径,长度为200,不可为空 + @JsonProperty("SP1") + private String sp1; + + // 车辆归属地,长度为1,不可为空 + @JsonProperty("CLSSD") + private Integer clssd; + + // 车辆类型,中文名称,长度为20,不可为空 + @JsonProperty("CLLX") + private String cllx; + + // 公安车辆类型,长度为10,可为空 + @JsonProperty("GAVTYPE") + private String gavtype; + + // 车身颜色中文名,长度为2,可为空 + @JsonProperty("CSYS") + private String csys; + + // CO 判定结果,0:不合格,1:合格,长度为1,可为空 + @JsonProperty("COPDJG") + private Integer copdjg; + + // HC 判定结果,0:不合格,1:合格,长度为1,可为空 + @JsonProperty("HCPDJG") + private Integer hcpdjg; + + // NO 判定结果,0:不合格,1:合格,长度为1,不可为空 + @JsonProperty("NOPDJG") + private Integer nopdjg; + + // 不透光度判定结果,0:不合格,1:合格,长度为1,不可为空 + @JsonProperty("BTGDPDJG") + private Integer btgdpdjg; + + // 林格曼黑度判定结果,0:不合格,1:合格,长度为1,不可为空 + @JsonProperty("LGMHDPDJG") + private Integer lgmhdpdjg; + + // 城市编号,长度为6,不可为空 + @JsonProperty("CityCode") + private String cityCode; + + // 区县编号,长度为6,不可为空 + @JsonProperty("CountyCode") + private String countyCode; + + // 车头图像1的HTTP链接,长度为200,不可为空 + @JsonProperty("TP1_HTTP") + private String tp1Http; + + // 车头图像2的HTTP链接,长度为200,不可为空 + @JsonProperty("TP2_HTTP") + private String tp2Http; + + // 车牌图片的HTTP链接,长度为200,不可为空 + @JsonProperty("TP3_HTTP") + private String tp3Http; + + // 视频的HTTP链接,长度为200,不可为空 + @JsonProperty("SP1_HTTP") + private String sp1Http; + + private Date createTime; + + private Date updateTime; +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesAlarmMsg.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesAlarmMsg.java new file mode 100644 index 0000000..78d5666 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesAlarmMsg.java @@ -0,0 +1,65 @@ +package com.bnhz.adapter.model.fumes; + +import com.bnhz.iot.model.DataId; +import com.bnhz.common.annotation.Length; +import com.bnhz.common.utils.DateUtils; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 报警信息 + * + * @author Leo + * @date 2024/7/1 17:14 + */ +@Data +public class FumesAlarmMsg implements DataId { + + @JsonProperty("Id") + @ApiModelProperty("id") + private String bisId; + + @JsonProperty("MN") + @ApiModelProperty("设备编号") + private String mn; + + @JsonProperty("AcquitAt") + @ApiModelProperty("数据收集时间") + private long acquitAt; + + @JsonProperty("Owner") + @ApiModelProperty("持有人") + private String owner; + + @JsonProperty("Content") + @ApiModelProperty("内容") + @Length(300) + private String content; + + @JsonProperty("MsgType") + @ApiModelProperty("消息类型") + private String msgType; + + @JsonProperty("Addr") + @ApiModelProperty("地址") + @Length(200) + private String addr; + + @JsonProperty("Name") + @ApiModelProperty("点位名称") + @Length(200) + private String name; + + @Override + public String getSerialNumber() { + return this.mn; + } + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(acquitAt * 1000); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesCustomer.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesCustomer.java new file mode 100644 index 0000000..f98f306 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesCustomer.java @@ -0,0 +1,67 @@ +package com.bnhz.adapter.model.fumes; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * @author Leo + * @date 2024/7/1 16:33 + */ +@Data +public class FumesCustomer { + + @JsonProperty("Id") + private int id; + @JsonProperty("Pid") + private String pid; + @JsonProperty("BlNo") + private String blNo; + @JsonProperty("BlName") + private String blName; + @JsonProperty("Org") + private String org; + @JsonProperty("Name") + private String name; + @JsonProperty("Contact") + private String contact; + @JsonProperty("Mobile") + private String mobile; + @JsonProperty("Telephone") + private String telephone; + @JsonProperty("Typ") + private int type; + @JsonProperty("Creator") + private String creator; + @JsonProperty("CreateAt") + private long createAt; + @JsonProperty("Status") + private int status; + @JsonProperty("Address") + private String address; + @JsonProperty("Logo") + private String logo; + @JsonProperty("BusinessHour") + private String businessHour; + @JsonProperty("Desc") + private String desc; + @JsonProperty("MaintainerId") + private String maintainerId; + @JsonProperty("AreaIds") + private Object areaIds; // Assuming it is an object; replace with appropriate class if necessary + @JsonProperty("AreaIdCascades") + private Object areaIdCascades; // Assuming it is an object; replace with appropriate class if necessary + @JsonProperty("owner_name") + private String ownerName; + @JsonProperty("maintainer_name") + private String maintainerName; + @JsonProperty("Areas") + private Object areas; // Assuming it is an object; replace with appropriate class if necessary + @JsonProperty("PerPage") + private int perPage; + @JsonProperty("Page") + private int page; + @JsonProperty("IsDownload") + private boolean isDownload; + @JsonProperty("health_code_color") + private String healthCodeColor; +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesDetectorDaily.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesDetectorDaily.java new file mode 100644 index 0000000..1683f66 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesDetectorDaily.java @@ -0,0 +1,207 @@ +package com.bnhz.adapter.model.fumes; + +import com.bnhz.common.annotation.Length; +import com.bnhz.iot.model.DataId; +import com.bnhz.common.utils.DateUtils; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Date; + +import static com.bnhz.common.utils.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * 报警管理-监测 + * @author Leo + * @date 2024/7/1 17:34 + */ +@Data +public class FumesDetectorDaily implements DataId { + + @JsonProperty("Id") + @ApiModelProperty("id") + private String bisId; + + @JsonProperty("MN") + @ApiModelProperty("设备编号") + private String mn; + + @JsonProperty("FanMN") + private String fanMn; + + @JsonProperty("FilterMN") + private String filterMn; + + @JsonProperty("Owner") + @ApiModelProperty("持有者") + private String owner; + + @JsonProperty("AcquitAt") + @ApiModelProperty("数据采集时间") + private long acquitAt; + + @JsonProperty("AcquitDate") + @ApiModelProperty("数据采集日") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private Date acquitDate; + + @JsonProperty("CreateAt") + @ApiModelProperty("创建日期") + private long createAt; + + @JsonProperty("CEmissions") + @ApiModelProperty("排放物浓度折算") + private BigDecimal cEmissions; + + @JsonProperty("CGranule") + @ApiModelProperty("颗粒物浓度折算") + private BigDecimal cGranule; + + @JsonProperty("CHydrocarbon") + @ApiModelProperty("非甲烷总烃浓度折算") + private BigDecimal cHydrocarbon; + + @JsonProperty("Emissions") + private BigDecimal emissions; + + @JsonProperty("Granule") + private BigDecimal granule; + + @JsonProperty("Hydrocarbon") + private BigDecimal hydrocarbon; + + @JsonProperty("Velocity") + @ApiModelProperty("转速") + private BigDecimal velocity; + + @JsonProperty("Temperature") + @ApiModelProperty("温度") + private BigDecimal temperature; + + @JsonProperty("Moisture") + @ApiModelProperty("湿度") + private BigDecimal moisture; + + @JsonProperty("RedPm25") + @ApiModelProperty("Pm2.5减排量") + private BigDecimal redPm25; + + @JsonProperty("RedPm10") + @ApiModelProperty("Pm10减排量") + private BigDecimal redPm10; + + @JsonProperty("RedEmissions") + @ApiModelProperty("排放物减排量") + private BigDecimal redEmissions; + + @JsonProperty("RedVocs") + @ApiModelProperty("Vocs减排量") + private BigDecimal redVocs; + + @JsonProperty("DeviceNum") + @ApiModelProperty("设备数") + private int deviceNum; + + /** + * 1:正常,2:超标,3:正常离线,4异常离线 + */ + @JsonProperty("Status") + @ApiModelProperty("状态") + private int status; + + @JsonProperty("OnlineStatus") + @ApiModelProperty("在线状态") + private int onlineStatus; + + @JsonProperty("SystemStatus") + @ApiModelProperty("系统状态") + private int systemStatus; + + @JsonProperty("FanStatus") + private int fanStatus; + + @JsonProperty("FilterStatus") + private int filterStatus; + + @JsonProperty("FilterFanLinkRatio") + @ApiModelProperty("净化器风机联动比") + private int filterFanLinkRatio; + + @JsonProperty("FilterAbnormallyUsed") + @ApiModelProperty("净化器是否非正常使用") + private boolean filterAbnormallyUsed; + + @JsonProperty("CEmissionsExceedStandard") + @ApiModelProperty("排放物超标") + private boolean cEmissionsExceedStandard; + + @JsonProperty("CGranuleExceedStandard") + @ApiModelProperty("颗粒物超标") + private boolean cGranuleExceedStandard; + + @JsonProperty("CHydrocarbonExceedStandard") + @ApiModelProperty("非甲烷总烃超标") + private boolean cHydrocarbonExceedStandard; + + @JsonProperty("LocaleId") + @ApiModelProperty("地区id") + private String localeId; + + @JsonProperty("CustomerId") + @ApiModelProperty("所属单位Id") + private int customerId; + + @JsonProperty("Customer") + @ApiModelProperty("用户") + private String customer; + + @JsonProperty("LocaleEmissionsSill") + @ApiModelProperty("排放物阈值") + private BigDecimal localeEmissionsSill; + + @JsonProperty("LocaleGranuleSill") + @ApiModelProperty("颗粒物阈值") + private BigDecimal localeGranuleSill; + + @JsonProperty("LocaleHydrocarbonSill") + @ApiModelProperty("非甲烷总烃阈值") + private BigDecimal localeHydrocarbonSill; + + @JsonProperty("LocaleName") + @ApiModelProperty("监测点名") + @Length(200) + private String localeName; + + @JsonProperty("LocaleAddr") + @ApiModelProperty("监测点") + @Length(200) + private String localeAddr; + + @JsonProperty("AbnormalOffline") + @ApiModelProperty("异常离线") + private boolean abnormalOffline; + + @JsonProperty("StatusDesc") + @ApiModelProperty("状态描述") + private String statusDesc; + + @JsonProperty("fan_current") + private BigDecimal fanCurrent; + + @JsonProperty("pur_current") + private BigDecimal purCurrent; + + @Override + public String getSerialNumber() { + return this.mn; + } + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(acquitAt * 1000); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesMonitorPoint.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesMonitorPoint.java new file mode 100644 index 0000000..d297eac --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesMonitorPoint.java @@ -0,0 +1,283 @@ +package com.bnhz.adapter.model.fumes; + +import com.bnhz.common.annotation.Length; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * 监控点信息 + * @author Leo + * @date 2024/7/1 16:31 + */ +@Data +public class FumesMonitorPoint { + + @JsonProperty("Id") + @ApiModelProperty("业务id") + private String bisId; + + @JsonProperty("Name") + @ApiModelProperty("烟道名称") + @Length(150) + private String name; + + @JsonProperty("Owner") + @ApiModelProperty("单位") + private String owner; + + @JsonProperty("CustomerId") + @ApiModelProperty("组织id") + private int customerId; + + @JsonProperty("Customer") + private FumesCustomer customer; + + @JsonProperty("Cuisine") + @ApiModelProperty("菜系") + private int cuisine; + + @JsonProperty("CuisineObj") + private Object cuisineObj; // Assuming it is an object; replace with appropriate class if necessary + + @JsonProperty("SendMode") + private int sendMode; + + @JsonProperty("SurpassCalcMethod") + private int surpassCalcMethod; + + @JsonProperty("FanSpeed") + @ApiModelProperty("风速") + private int fanSpeed; + + @JsonProperty("FanQuantity") + @ApiModelProperty("风量") + private int fanQuantity; + + @JsonProperty("FanInfo") + private String fanInfo; + + @JsonProperty("FanStandardCurrent") + private int fanStandardCurrent; + @JsonProperty("PowerSupplyMode") + private String powerSupplyMode; + + @JsonProperty("PipeArea") + @ApiModelProperty("管道截面面积") + private int pipeArea; + + @JsonProperty("StoveNum") + @ApiModelProperty("灶头数量") + private int stoveNum; + + @JsonProperty("StoveLength") + @ApiModelProperty("灶头长度") + private int stoveLength; + + @JsonProperty("StoveWidth") + @ApiModelProperty("灶头宽带") + private int stoveWidth; + + @JsonProperty("Samplings") + @ApiModelProperty("抽样次数") + private int samplings; + + @JsonProperty("LinkStatus") + @ApiModelProperty("是否联动") + private int linkStatus; + + @JsonProperty("LinkRatioSill") + private BigDecimal linkRatioSill; + + @JsonProperty("FanStatus") + @ApiModelProperty("风机状态") + private int fanStatus; + + @JsonProperty("FilterStandardCurrent") + private int filterStandardCurrent; + + @JsonProperty("FilterInfo") + @ApiModelProperty("净化器信息") + private String filterInfo; + + @JsonProperty("FilterStatus") + @ApiModelProperty("净化器状态") + private int filterStatus; + + @JsonProperty("OfflineJudge") + private int offlineJudge; + + @JsonProperty("EmissionsSill") + @ApiModelProperty("超标阈值") + private int emissionsSill; + + @JsonProperty("GranuleSill") + private int granuleSill; + @JsonProperty("HydrocarbonSill") + private int hydrocarbonSill; + @JsonProperty("StoveArea") + @ApiModelProperty("集气灶面积") + private int stoveArea; + + @JsonProperty("ExhaustTime") + @ApiModelProperty("日均排烟事件") + private String exhaustTime; + + @JsonProperty("Remark") + @ApiModelProperty("备注") + private String remark; + + + @JsonProperty("Addr") + @ApiModelProperty("地址") + @Length(300) + private String addr; + + @JsonProperty("AreaId") + @ApiModelProperty("地区") + private String areaId; + + @ApiModelProperty("经度") + @JsonProperty("Lng") + private String lng; + + @ApiModelProperty("纬度") + @JsonProperty("Lat") + private String lat; + + @ApiModelProperty("创建者") + @JsonProperty("Creator") + private String creator; + + @ApiModelProperty("创建时间") + @JsonProperty("CreateAt") + private long createAt; + + @JsonProperty("StartAt") + private long startAt; + @JsonProperty("Status") + private int status; + @JsonProperty("StatusOfRecord") + @ApiModelProperty("设备状态") + private String statusOfRecord; + + @JsonProperty("HealthCodeColor") + @ApiModelProperty("健康码颜色") + private String healthCodeColor; + + @JsonProperty("LocalePics") + private Object localePics; // Assuming it is an object; replace with appropriate class if necessary + @JsonProperty("LocalePicS") + private List localePicsList; // Assuming it is a list of objects; replace with appropriate class if necessary + @JsonProperty("MnLast") + @ApiModelProperty("设备编号") + private String mnLast; + + @JsonProperty("MnTypLast") + @ApiModelProperty("设备类型") + private int mnTypLast; + + @JsonProperty("Mobile") + private String mobile; + @JsonProperty("Contact") + private String contact; + + @JsonProperty("MaintainerId") + @ApiModelProperty("运维人员id") + private String maintainerId; + + @JsonProperty("HealthCodeX") + @ApiModelProperty("绿码") + private int healthCodeX; + + @JsonProperty("HealthCodeValue1") + private int healthCodeValue1; + @JsonProperty("HealthCodeValue2") + private int healthCodeValue2; + @JsonProperty("HealthCodeValue3") + private int healthCodeValue3; + @JsonProperty("HealthCodeValue4") + private int healthCodeValue4; + @JsonProperty("HealthCodeValue5") + private int healthCodeValue5; + @JsonProperty("HealthCodeValue6") + private int healthCodeValue6; + @JsonProperty("HealthCodeValue11") + private int healthCodeValue11; + @JsonProperty("HealthCodeValue12") + private int healthCodeValue12; + + @JsonProperty("AbnormalValue7") + @ApiModelProperty("油烟浓度7") + private BigDecimal abnormalValue7; + + @JsonProperty("AbnormalValue8") + @ApiModelProperty("油烟浓度8") + private int abnormalValue8; + + @JsonProperty("AbnormalValue9") + @ApiModelProperty("油烟浓度9") + private int abnormalValue9; + + + @JsonProperty("AbnormalValue10") + @ApiModelProperty("油烟浓度10") + private int abnormalValue10; + + @JsonProperty("AliIotDeviceName") + @ApiModelProperty("Ali设备名称") + private String aliIotDeviceName; + @JsonProperty("AliIot") + private Object aliIot; // Assuming it is an object; replace with appropriate class if necessary + @JsonProperty("CreatedAt") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createdAt; + @JsonProperty("UpdatedAt") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updatedAt; + @JsonProperty("RemarkOfRecord") + private String remarkOfRecord; + @JsonProperty("Cause") + private String cause; + @JsonProperty("scale") + private String scale; + @JsonProperty("management_types") + private String managementTypes; + @JsonProperty("fan_brand") + private String fanBrand; + @JsonProperty("fan_model") + private String fanModel; + @JsonProperty("fan_power") + private int fanPower; + @JsonProperty("filter_brand") + private String filterBrand; + @JsonProperty("filter_model") + private String filterModel; + @JsonProperty("purifying_rate") + private int purifyingRate; + @JsonProperty("filter_power") + private int filterPower; + @JsonProperty("principle") + private String principle; + @JsonProperty("is_in_time") + private int isInTime; + @JsonProperty("owner_name") + private String ownerName; + @JsonProperty("area_name") + private String areaName; + @JsonProperty("area_detail") + private String areaDetail; + @JsonProperty("cuisine_name") + private String cuisineName; + @JsonProperty("maintainer_name") + private String maintainerName; + @JsonProperty("device_mns") + private String deviceMns; + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesOneMin.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesOneMin.java new file mode 100644 index 0000000..caafeae --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesOneMin.java @@ -0,0 +1,95 @@ +package com.bnhz.adapter.model.fumes; + +import com.bnhz.iot.model.DataId; +import com.bnhz.common.annotation.Length; +import com.bnhz.common.utils.DateUtils; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 一分钟设备数据 + * @author Leo + * @date 2024/7/1 20:38 + */ +@Data +public class FumesOneMin implements DataId { + + @JsonProperty("Id") + @ApiModelProperty("id") + private String bisId; + + @JsonProperty("Message") + @ApiModelProperty("一分钟报文数据") + @Length(300) + private String message; + + @JsonProperty("MN") + @ApiModelProperty("设备编号") + private String mn; + + @JsonProperty("AcquitAt") + @ApiModelProperty("采集时间") + private long acquitAt; + + @JsonProperty("CreateAt") + @ApiModelProperty("创建时间") + private long createAt; + + @JsonProperty("FanStatus") + @ApiModelProperty("风机状态") + private String fanStatus; + + @JsonProperty("FilterStatus") + @ApiModelProperty("净化器状态") + private String filterStatus; + + @JsonProperty("EmissionsConc") + @ApiModelProperty("实时油烟排放量") + private BigDecimal emissionsConc; + + @JsonProperty("GranuleConc") + @ApiModelProperty("颗粒物含量") + private BigDecimal granuleConc; + + @JsonProperty("HydrocarbonConc") + @ApiModelProperty("非甲烷总烃") + private BigDecimal hydrocarbonConc; + + @JsonProperty("Velocity") + @ApiModelProperty("流速") + private BigDecimal velocity; + + @JsonProperty("Temperature") + @ApiModelProperty("温度") + private BigDecimal temperature; + + @JsonProperty("Moisture") + @ApiModelProperty("湿度") + private BigDecimal moisture; + + @JsonProperty("Flag") + @ApiModelProperty("设备状态") + private String flag; + + @JsonProperty("fan_current") + @ApiModelProperty("风机电流值") + private BigDecimal fanCurrent; + + @JsonProperty("pur_current") + @ApiModelProperty("净化器电流值") + private BigDecimal purCurrent; + + @Override + public String getSerialNumber() { + return this.mn; + } + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(acquitAt * 1000); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesPage.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesPage.java new file mode 100644 index 0000000..ab12d80 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesPage.java @@ -0,0 +1,30 @@ +package com.bnhz.adapter.model.fumes; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * @author Leo + * @date 2024/7/1 16:28 + */ +@Data +public class FumesPage { + + @JsonProperty("DeviceNum") + private int deviceNum; + @JsonProperty("content") + private List content; + @JsonProperty("total") + private int total; + + private ResultsPageInfo resultsPageInfo; + + @Data + public static class ResultsPageInfo { + + @JsonProperty("Total") + private Integer total; + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesPointEventRecord.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesPointEventRecord.java new file mode 100644 index 0000000..e93baeb --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesPointEventRecord.java @@ -0,0 +1,63 @@ +package com.bnhz.adapter.model.fumes; + +import com.bnhz.common.annotation.Length; +import com.bnhz.iot.model.DataId; +import com.bnhz.common.utils.DateUtils; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 所有点位事件处理记录 + * + * @author Leo + * @date 2024/7/2 10:17 + */ + +@Data +public class FumesPointEventRecord { + + private List onlineEventList; + + @Data + public static class Event implements DataId { + + @JsonProperty("CalculateDate") + @ApiModelProperty("时间戳") + private long calculateDate; + + @JsonProperty("LocaleName") + @ApiModelProperty("监测点名") + private String localeName; + + @JsonProperty("LocaleAddr") + @ApiModelProperty("监测点地址") + private String localeAddr; + + @JsonProperty("LocaleMn") + @ApiModelProperty("设备序列号") + private String localeMn; + + @JsonProperty("EventType") + @ApiModelProperty("事件类型") + private String eventType; + + @JsonProperty("HandleMeasure") + @ApiModelProperty("处理措施") + @Length(200) + private String handleMeasure; + + @Override + public String getSerialNumber() { + return localeMn; + } + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(calculateDate * 1000); + } + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesRealTime.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesRealTime.java new file mode 100644 index 0000000..42954a0 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesRealTime.java @@ -0,0 +1,173 @@ +package com.bnhz.adapter.model.fumes; + +import com.bnhz.common.annotation.Length; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.iot.model.DataId; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 实时数据 + * + * @author Leo + * @date 2024/8/1 15:58 + */ +@Data +public class FumesRealTime implements DataId { + + /** + * + */ + @JsonProperty("Id") + @ApiModelProperty(value = "实时数据的设备编号",notes = "中间有“-”连接代表该监测点有多个设备绑定") + @Length(200) + private String id; + + @JsonProperty("AcquitAt") + @ApiModelProperty("采集时间") + private long acquitAt; + + @JsonProperty("LastAt") + @ApiModelProperty("最后确认时间") + private long lastAt; + + @JsonProperty("Data") + private String data; + + @JsonProperty("CEmissions") + @ApiModelProperty("排放物折算浓度") + private BigDecimal cEmissions; + + @JsonProperty("CGranule") + @ApiModelProperty("颗粒物折算浓度") + private BigDecimal cGranule; + + @JsonProperty("CHydrocarbon") + @ApiModelProperty("非甲烷总烃折算浓度") + private BigDecimal cHydrocarbon; + + @JsonProperty("EmissionsConc") + @ApiModelProperty("实时排放量") + private BigDecimal emissionsConc; + + @JsonProperty("GranuleConc") + @ApiModelProperty("颗粒物含量") + private BigDecimal granuleConc; + + @JsonProperty("HydrocarbonConc") + @ApiModelProperty("非甲烷总烃含量") + private BigDecimal hydrocarbonConc; + + @JsonProperty("FanStatus") + @ApiModelProperty("风机状态") + private String fanStatus; + + @JsonProperty("FilterStatus") + @ApiModelProperty("净化器状态") + private String filterStatus; + + @JsonProperty("Typ") + @ApiModelProperty("类型") + private int typ; + + @JsonProperty("Status") + @ApiModelProperty("状态") + private String status; + + @JsonProperty("Velocity") + @ApiModelProperty("流速") + private BigDecimal velocity; + + @JsonProperty("Temperature") + @ApiModelProperty("温度") + private BigDecimal temperature; + + @JsonProperty("Moisture") + @ApiModelProperty("湿度") + private BigDecimal moisture; + + @JsonProperty("Locale") + @ApiModelProperty("监测点名称") + @Length(100) + private String locale; + + @JsonProperty("Lid") + @ApiModelProperty("监测点id") + private String lid; + + @JsonProperty("Owner") + @ApiModelProperty("所属单位") + private String owner; + + @JsonProperty("Addr") + @ApiModelProperty("安装地址") + @Length(200) + private String addr; + + @JsonProperty("EmissionsSill") + @ApiModelProperty("超标阈值") + private BigDecimal emissionsSill; + + @JsonProperty("GranuleSill") + @ApiModelProperty("颗粒物超标阈值") + private BigDecimal granuleSill; + + @JsonProperty("HydrocarbonSill") + @ApiModelProperty("非甲烷总烃超标阈值") + private BigDecimal hydrocarbonSill; + + @JsonProperty("LinkStatus") + @ApiModelProperty("是否联动") + private int linkStatus; + + @JsonProperty("CustomerMobile") + @ApiModelProperty("联系电话") + private String customerMobile; + + @JsonProperty("CustomeName") + private String customeName; + + @JsonProperty("LocaleLng") + @ApiModelProperty("经度") + private String localeLng; + + @JsonProperty("LocaleLat") + @ApiModelProperty("纬度") + private String localeLat; + + @JsonProperty("HealthCodeColor") + private String healthCodeColor; + + @JsonProperty("fan_current") + @ApiModelProperty("风机电流值") + private BigDecimal fanCurrent; + + @JsonProperty("pur_current") + @ApiModelProperty("净化器电流值") + private BigDecimal purCurrent; + + @JsonProperty("SendMode") + @ApiModelProperty("监测方式") + private int sendMode; + + @JsonProperty("OnlineStatus") + @ApiModelProperty(value = "整体状态", notes = "1:在线2:离线3:异常离线") + private int onlineStatus; + + @JsonProperty("StatusOfRecord") + @ApiModelProperty(value = "记录状态", notes = "NORMAL:正常、OFFLINE:下线、ABANDONED:废弃") + private String statusOfRecord; + @Override + public String getSerialNumber() { + return id; + } + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(acquitAt * 1000); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesReduce.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesReduce.java new file mode 100644 index 0000000..a30881a --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesReduce.java @@ -0,0 +1,99 @@ +package com.bnhz.adapter.model.fumes; + +import com.bnhz.common.utils.DateUtils; +import com.bnhz.iot.model.DataId; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 监测点位减排统计 + * + * @author Leo + * @date 2024/8/1 15:38 + */ +@Data +public class FumesReduce implements DataId { + + + @JsonProperty("locale_id") + @ApiModelProperty("监测点ID") + private String localeId; + + @JsonProperty("owner") + @ApiModelProperty("所属单位") + private String owner; + + @JsonProperty("mn") + @ApiModelProperty("设备序列号") + private String mn; + + @JsonProperty("hour") + @ApiModelProperty("小时") + private BigDecimal hour; + + @JsonProperty("red_emissions") + @ApiModelProperty("油烟减排量") + private BigDecimal redEmissions; + + @JsonProperty("red_pm25") + @ApiModelProperty("PM2.5减排量") + private BigDecimal redPm25; + + @JsonProperty("red_pm10") + @ApiModelProperty("PM10减排量") + private BigDecimal redPm10; + + @JsonProperty("red_vocs") + @ApiModelProperty("Vocs减排量") + private BigDecimal redVocs; + + @JsonProperty("se_red_emissions") + @ApiModelProperty("油烟实际排放量") + private BigDecimal seRedEmissions; + + @JsonProperty("se_red_pm25") + @ApiModelProperty("PM2.5实际排放量") + private BigDecimal seRedPm25; + + @JsonProperty("se_red_pm10") + @ApiModelProperty("PM10实际排放量") + private BigDecimal seRedPm10; + + @JsonProperty("se_red_vocs") + @ApiModelProperty("Vocs实际排放量") + private BigDecimal seRedVocs; + + @JsonProperty("acquit_date") + @ApiModelProperty("采集时间") + private long acquitDate; + + @JsonProperty("avg_red_emissions") + @ApiModelProperty("油烟平均折算浓度") + private BigDecimal avgRedEmissions; + + @JsonProperty("avg_red_pm25") + @ApiModelProperty("PM2.5平均折算浓度") + private BigDecimal avgRedPm25; + + @JsonProperty("avg_red_pm10") + @ApiModelProperty("PM10平均折算浓度") + private BigDecimal avgRedPm10; + + @JsonProperty("avg_red_vocs") + @ApiModelProperty("Vocs平均折算浓度") + private BigDecimal avgRedVocs; + + @Override + public String getSerialNumber() { + return mn; + } + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(acquitDate * 1000); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesRes.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesRes.java new file mode 100644 index 0000000..12e78ec --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesRes.java @@ -0,0 +1,26 @@ +package com.bnhz.adapter.model.fumes; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * @author Leo + * @date 2024/7/1 16:27 + */ +@Data +public class FumesRes { + + @JsonProperty("Data") + private T data; + @JsonProperty("Msg") + private String msg; + @JsonProperty("Status") + private int status; + + @Data + public static class DataInfo { + private List data; + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesTenMin.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesTenMin.java new file mode 100644 index 0000000..9e44b20 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/fumes/FumesTenMin.java @@ -0,0 +1,77 @@ +package com.bnhz.adapter.model.fumes; + +import com.bnhz.iot.model.DataId; +import com.bnhz.common.utils.DateUtils; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 十分钟设备数据 + * + * @author Leo + * @date 2024/7/1 19:53 + */ +@Data +public class FumesTenMin implements DataId { + + /** + * 设备采集时间戳/600 + */ + @JsonProperty("acquit_at") + @ApiModelProperty("设备采集时间") + private long acquitAt; + + @JsonProperty("counter") + @ApiModelProperty("发送数据的次数") + private int counter; + + @JsonProperty("create_at") + @ApiModelProperty("创建时间") + private long createAt; + + @JsonProperty("emissions_conc") + @ApiModelProperty("实时排放量") + private BigDecimal emissionsConc; + + @JsonProperty("granule_conc") + @ApiModelProperty("颗粒物含量") + private BigDecimal granuleConc; + + @JsonProperty("hydrocarbon_conc") + @ApiModelProperty("非甲烷总烃") + private BigDecimal hydrocarbonConc; + + @JsonProperty("id") + @ApiModelProperty("id") + private String bisId; + + @JsonProperty("mn") + @ApiModelProperty("设备编号") + private String mn; + + @JsonProperty("moisture") + @ApiModelProperty("湿度") + private BigDecimal moisture; + + @JsonProperty("temperature") + @ApiModelProperty("温度") + private BigDecimal temperature; + + @JsonProperty("velocity") + @ApiModelProperty("流速") + private BigDecimal velocity; + + @Override + public String getSerialNumber() { + return this.mn; + } + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(acquitAt * 600 * 1000); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaCameraInfo.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaCameraInfo.java new file mode 100644 index 0000000..3859e02 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaCameraInfo.java @@ -0,0 +1,113 @@ +package com.bnhz.adapter.model.kacheck; + +import com.bnhz.adapter.model.blackcar.BlackCarId; +import com.bnhz.common.annotation.Length; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 摄像头信息表 + * @author Leo + * @date 2024/6/15 15:15 + */ +@Data +public class KaCameraInfo extends BlackCarId { + + private Long id; + + // 摄像头编号 + @JsonProperty("SXTBH") + @ApiModelProperty("摄像头编号") + private String sxtbh; + + // 摄像头名称 + @JsonProperty("SXTMC") + @ApiModelProperty("摄像头名称") + private String sxtmc; + + // 摄像头类型,1: 海康,2: 大华,3: 其他 + @JsonProperty("SPLX") + @ApiModelProperty("摄像头类型") + private Integer splx; + + // 是否有效,0: 无效,1: 有效 + @JsonProperty("SFYX") + @ApiModelProperty("是否有效") + private Integer sfyx; + + + // IP地址 + @JsonProperty("IP") + @ApiModelProperty("IP地址") + private String ip; + + // 联网状态,0: 断开,1: 在线 + @JsonProperty("LWZT") + @ApiModelProperty("联网状态") + private Integer lwzt; + + // 摄像头类型,1: 前置,2: 后置 + @JsonProperty("SXTLX") + @ApiModelProperty("摄像头类型") + private Integer sxtlx; + + // 摄像头朝向 + @JsonProperty("SXTCX") + @ApiModelProperty("摄像头朝向") + private String sxtcx; + + // 抓拍车道数 + @JsonProperty("ZPCDS") + @ApiModelProperty("抓拍车道数") + private Integer zpcds; + + // 通道号 + @JsonProperty("TDH") + @ApiModelProperty("通道号") + private String tdh; + + // 摄像头备案号 + @JsonProperty("SXTBAH") + @ApiModelProperty("摄像头备案号") + private String sxtbah; + + // HLS播放地址高清 + @JsonProperty("HLSGQ") + @ApiModelProperty("HLS播放地址高清") + @Length(200) + private String hlsgq; + + // HLS播放地址流畅 + @JsonProperty("HLSLC") + @ApiModelProperty("HLS播放地址流畅") + @Length(200) + private String hlslc; + + // 是否启用,1: 正常,2: 调试,3: 暂停 + @JsonProperty("SFQY") + @ApiModelProperty("是否启用") + private Integer sfqy; + + // 登录账号 + @JsonProperty("DLZH") + @ApiModelProperty("登录账号") + private String dlzh; + + // 登录密码 + @JsonProperty("DLMM") + @ApiModelProperty("登录密码") + private String dlmm; + + private Date createTime; + + private Date updateTime; + + @Override + public LocalDateTime getDataTime() { + return LocalDateTime.now(); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaPoint.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaPoint.java new file mode 100644 index 0000000..3c4af94 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaPoint.java @@ -0,0 +1,111 @@ +package com.bnhz.adapter.model.kacheck; + +import com.bnhz.adapter.model.blackcar.BlackCarId; +import com.bnhz.common.annotation.Length; +import com.bnhz.common.utils.DateUtils; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.experimental.FieldNameConstants; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 点位信息 + * + * @author Leo + * @date 2024/6/14 17:46 + */ + +@ToString +@EqualsAndHashCode(callSuper = true) +@Data +@FieldNameConstants +public class KaPoint extends BlackCarId { + + private Long bisId; + + @JsonProperty("DWMC") + @ApiModelProperty("点位名称") + private String dwmc; + + @JsonProperty("DWLX") + @ApiModelProperty("点位类型") + private String dwlx; + + @JsonProperty("YXRQ") + @JsonFormat(pattern = "yyyy-MM-dd") + @ApiModelProperty("运行日期") + private Date yxrq; + + @JsonProperty("DWZT") + @ApiModelProperty("点位状态") + private String dwzt; + + @JsonProperty("DWDZ") + @ApiModelProperty("点位地址") + private String dwdz; + + @JsonProperty("DDJD") + @ApiModelProperty("地点经度") + private BigDecimal ddjd; + + @JsonProperty("DDWD") + @ApiModelProperty("地点纬度") + private BigDecimal ddwd; + + @JsonProperty("CLFX") + @ApiModelProperty("车流方向") + private String clfx; + + @JsonProperty("CDSL") + @ApiModelProperty("车道数量") + private Integer cdsl; + + @JsonProperty("CDPD") + @ApiModelProperty("车道坡度") + private BigDecimal cdpd; + + @JsonProperty("YCXS") + @ApiModelProperty("遥测线数") + private Integer ycxs; + + @JsonProperty("HPHM") + @ApiModelProperty("号牌号码") + private String hphm; + + @JsonProperty("CLXH") + @ApiModelProperty("装载车型号") + private String clxh; + + @JsonProperty("ZYLX") + @ApiModelProperty("点位作用类型") + private String zylx; + + @JsonProperty("SSSP") + @ApiModelProperty("实时视频链接") + @Length(value = 200) + private String sssp; + + @JsonProperty("CityCode") + @ApiModelProperty("城市编号") + private String cityCode; + + @JsonProperty("CountyCode") + @ApiModelProperty("区县编号") + private String countyCode; + + private Date createTime; + + private Date updateTime; + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(yxrq); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaTrafficFlowInfo.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaTrafficFlowInfo.java new file mode 100644 index 0000000..7cd0fdc --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaTrafficFlowInfo.java @@ -0,0 +1,196 @@ +package com.bnhz.adapter.model.kacheck; + +import com.bnhz.adapter.model.blackcar.BlackCarId; +import com.bnhz.common.annotation.Length; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 交通流量信息 + * + * @author Leo + * @date 2024/6/15 10:05 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class KaTrafficFlowInfo extends BlackCarId { + + private Long id; + + + /** + * 流量编号 + */ + @JsonProperty("LLBH") + @ApiModelProperty("流量编号") + private String llbh; + + /** + * 监测点位日志号 + */ + @JsonProperty("JCDWRZH") + @ApiModelProperty("监测点位日志号") + private String jcdwrzh; + + /** + * 所属道路 + */ + @JsonProperty("SSDL") + @ApiModelProperty("所属道路") + @Length(value = 200) + private String ssdl; + + /** + * 流量分类 + */ + @JsonProperty("LLFL") + @ApiModelProperty("流量分类") + private String llfl; + + /** + * 统计时长 + */ + @JsonProperty("TJSC") + @ApiModelProperty("统计时长") + private String tjsc; + + /** + * 采集时段 + */ + @JsonProperty("CJSD") + @ApiModelProperty("采集时段") + private Integer cjsd; + + /** + * 采集序号 + */ + @JsonProperty("CJXH") + @ApiModelProperty("采集序号") + private Integer cjxh; + + /** + * 统计日期 + */ + @JsonProperty("TTRQ") + @ApiModelProperty("统计日期") + private Date ttrq; + + /** + * 车道序号 + */ + @JsonProperty("CDXH") + @ApiModelProperty("车道序号") + private String cdxh; + + /** + * 微小型客车数 + */ + @JsonProperty("WXXKCS") + @ApiModelProperty("微小型客车数") + private Integer wxxkcs; + + /** + * 中型客车数 + */ + @JsonProperty("ZXKCS") + @ApiModelProperty("中型客车数") + private Integer zxkcs; + + /** + * 大型客车数 + */ + @JsonProperty("DXKCS") + @ApiModelProperty("大型客车数") + private Integer dxkcs; + + /** + * 小型货车数 + */ + @JsonProperty("XXHCS") + @ApiModelProperty("小型货车数") + private Integer xxhcs; + + /** + * 中型货车数 + */ + @JsonProperty("ZXHCS") + @ApiModelProperty("中型货车数") + private Integer zxhcs; + + /** + * 重型货车数 + */ + @JsonProperty("ZXHCS1") + @ApiModelProperty("重型货车数") + private Integer zxhcs1; + + /** + * 通行车辆数 + */ + @JsonProperty("TXCLS") + @ApiModelProperty("通行车辆数") + private Integer txcls; + + /** + * 平均速度 + */ + @JsonProperty("PJSD") + @ApiModelProperty("平均速度") + private Integer pjsd; + + /** + * 平均排队长度 + */ + @JsonProperty("PJPDCD") + @ApiModelProperty("平均排队长度") + private Integer pjpdc; + + /** + * 汽油车数 + */ + @JsonProperty("QYCS") + @ApiModelProperty("汽油车数") + private Integer qycs; + + /** + * 柴油车数 + */ + @JsonProperty("CYCS") + @ApiModelProperty("柴油车数") + private Integer cycs; + + /** + * 本地市车辆数 + */ + @JsonProperty("BDCS") + @ApiModelProperty("本地市车辆数") + private Integer bdcs; + + /** + * 本省车辆数 + */ + @JsonProperty("BSCS") + @ApiModelProperty("本省车辆数") + private Integer bscs; + + /** + * 外省车辆数 + */ + @JsonProperty("WSCS") + @ApiModelProperty("外省车辆数") + private Integer wscs; + + private Date createTime; + + private Date updateTime; + + @Override + public LocalDateTime getDataTime() { + return LocalDateTime.now(); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaVehicleFlow.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaVehicleFlow.java new file mode 100644 index 0000000..3819734 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/model/kacheck/KaVehicleFlow.java @@ -0,0 +1,144 @@ +package com.bnhz.adapter.model.kacheck; + +import com.bnhz.adapter.model.blackcar.BlackCarId; +import com.bnhz.common.annotation.Length; +import com.bnhz.common.utils.DateUtils; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Date; + +/** + * 车流量 + * + * @author Leo + * @date 2024/6/15 14:53 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class KaVehicleFlow extends BlackCarId { + + + private Long id; + + //车流量记录编号 + @JsonProperty("LLBH") + @ApiModelProperty("车流量记录编号") + private String llbh; + + // 车牌号 + @JsonProperty("HPHM") + @ApiModelProperty("车牌号") + private String hphm; + + // 车牌颜色 + @JsonProperty("HPYS") + @ApiModelProperty("车牌颜色") + private String hpys; + // 抓拍时间 + @JsonProperty("ZPSJ") + @ApiModelProperty("抓拍时间") + //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date zpsj; + + // 是否被抓拍 + @JsonProperty("SFBZP") + @ApiModelProperty("是否被抓拍") + private Integer sfbzp; + // 行政区划代码 + @JsonProperty("XZQHDM") + @ApiModelProperty("行政区划代码") + private String xzqhdm; + + // 车流速度 + @JsonProperty("CLSD") + @ApiModelProperty("车流速度") + private BigDecimal clsd; + + // 车辆所属地 + @JsonProperty("CLSSD") + @ApiModelProperty("车辆所属地") + private Integer clssd; + + // 车道号 + @JsonProperty("CDXH") + @ApiModelProperty("车道号") + private Integer cdxh; + + // 车身颜色中文名(可空) + @JsonProperty("CSYS") + @ApiModelProperty("车身颜色中文名") + private String csys; + // 号牌种类(可空) + @JsonProperty("HPZL") + @ApiModelProperty("号牌种类") + private String hpzl; + // 车辆类型 + @JsonProperty("CLLX") + @ApiModelProperty("车辆类型") + private String cllx; + // 燃料种类 + @JsonProperty("RLZL") + @ApiModelProperty("燃料种类") + private String rlzl; + // 全景图(可空) + @JsonProperty("TP1") + @ApiModelProperty("全景图") + @Length(200) + private String tp1; + // 号牌图片(可空) + @JsonProperty("tp2") + @ApiModelProperty("号牌图片") + @Length(200) + private String tp2; + // 视频(可空) + @JsonProperty("sp1") + @ApiModelProperty("视频") + @Length(200) + private String sp1; + // 后置车尾图片1 + @JsonProperty("TP1_HTTP") + @ApiModelProperty("后置车尾图片1") + @Length(200) + private String tp1Http; + + @JsonProperty("TP2_HTTP") + @ApiModelProperty("后置车尾图片2") + @Length(200) + private String tp2Http; // 后置车尾图片2 + + @JsonProperty("SP1_HTTP") + @ApiModelProperty("黑烟车证据视频") + @Length(200) + private String sp1Http; // 黑烟车证据视频 + + @JsonProperty("CityCode") + @ApiModelProperty("城市编号") + private String cityCode; // 城市编号 + + @JsonProperty("CountyCode") + @ApiModelProperty("区县编号") + private String countyCode; // 区县编号 + + @Deprecated + @JsonProperty("wspeed") + @ApiModelProperty("车速-作废") + private BigDecimal wspeed; + + @JsonProperty("SD") + @ApiModelProperty("车速") + private BigDecimal sd; + + private Date createTime; + + private Date updateTime; + + @Override + public LocalDateTime getDataTime() { + return DateUtils.toLocalDateTime(zpsj); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/blackcar/IBlackCarService.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/blackcar/IBlackCarService.java new file mode 100644 index 0000000..1eeb067 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/blackcar/IBlackCarService.java @@ -0,0 +1,59 @@ +package com.bnhz.adapter.service.blackcar; + +import com.bnhz.adapter.model.blackcar.*; + +import java.util.List; + +/** + * @author Leo + * @date 2024/6/28 09:58 + */ +public interface IBlackCarService { + + /** + * 新增黑烟车信息 + * + * @param blackSmokeVehicles 黑烟车信息 + * @return 结果 + */ + int insertBlackSmokeVehicle(List blackSmokeVehicles); + + + /** + * 新增摄像头信息 + * + * @param cameraInfos 摄像头信息 + * @return 结果 + */ + int insertCameraInfo(List cameraInfos); + + + /** + * 新增点位信息 + * 一个点位对应一个设备 + * + * @param points 点位信息 + * @return 结果 + */ + int insertPoint(List points); + + + + /** + * 新增交通流量信息 + * + * @param trafficFlowInfos 交通流量信息 + * @return 结果 + */ + int insertTrafficFlowInfo(List trafficFlowInfos); + + + /** + * 新增车流量 + * + * @param vehicleFlows 车流量 + * @return 结果 + */ + int insertVehicleFlow(List vehicleFlows); + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/blackcar/impl/BlackCarServiceImpl.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/blackcar/impl/BlackCarServiceImpl.java new file mode 100644 index 0000000..f569bc0 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/blackcar/impl/BlackCarServiceImpl.java @@ -0,0 +1,225 @@ +package com.bnhz.adapter.service.blackcar.impl; + +import com.bnhz.adapter.model.blackcar.*; +import com.bnhz.adapter.service.common.CommonService; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.reflect.ReflectUtils; +import com.bnhz.iot.domain.Device; +import com.bnhz.iot.domain.Product; +import com.bnhz.iot.domain.ThingsModel; +import com.bnhz.iot.enums.DeviceType; +import com.bnhz.iot.model.ThingsModels.ThingsModelEventVO; +import com.bnhz.iot.service.IDeviceService; +import com.bnhz.iot.service.IProductService; +import com.bnhz.iot.service.IThingsModelService; +import com.bnhz.iot.service.base.SyncDevice; +import com.bnhz.iot.tdengine.service.IColumnModeOperationsService; +import com.bnhz.mq.model.ReportDataBo; +import com.bnhz.mq.service.IDataHandler; +import com.bnhz.adapter.service.blackcar.IBlackCarService; +import com.bnhz.adapter.util.ThingsModelUtils; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author Leo + * @date 2024/6/28 10:00 + */ +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(rollbackFor = Exception.class) +public class BlackCarServiceImpl implements IBlackCarService, SyncDevice { + + private final IDataHandler dataHandler; + + private final IDeviceService deviceService; + + private final IProductService productService; + + private final IThingsModelService thingsModelService; + + private final IColumnModeOperationsService columnModeOperationsService; + + private final CommonService commonService; + + + + @Override + public int insertPoint(List points) { + //1.初始化设备 + Product product = initProduct(); + initModel(new Point(), product.getProductId(), product.getProductName()); + Device queryDevice = new Device(); + queryDevice.setProductId(product.getProductId()); + Map existDeviceMap = deviceService.selectDeviceList(queryDevice) + .stream() + .collect(Collectors.toMap(Device::getSerialNumber, Function.identity())); + points.forEach(point -> { + Device existDevice = existDeviceMap.get(point.getDwbh()); + Device device = addDevice(point, product, ObjectUtils.isEmpty(existDevice) ? null : existDevice.getDeviceId()); + //2.插入数据属性数据 + ReportDataBo propertyReportDataBo; + try { + propertyReportDataBo = ThingsModelUtils.toPropertyReportDataBo(ReflectUtils.getAllFields(point), product.getProductId(), device.getSerialNumber()); + } catch (IllegalAccessException e) { + log.error("转换错误", e); + throw new RuntimeException(e); + } + dataHandler.reportData(propertyReportDataBo); + }); + return points.size(); + } + + + @Override + public int insertBlackSmokeVehicle(List blackSmokeVehicles) { + commonService.saveEvent(blackSmokeVehicles, BnhzConstant.BlackCarEvent.HYC, DateUtils.getTimestamp()); + return blackSmokeVehicles.size(); + } + + + + + @Override + public int insertCameraInfo(List cameraInfos) { + commonService.saveEvent(cameraInfos, BnhzConstant.BlackCarEvent.SXTXX, DateUtils.getTimestamp()); + return cameraInfos.size(); + } + + + @Override + public int insertTrafficFlowInfo(List trafficFlowInfos) { + commonService.saveEvent(trafficFlowInfos, BnhzConstant.BlackCarEvent.JTLL, DateUtils.getTimestamp()); + return trafficFlowInfos.size(); + } + + @Override + public int insertVehicleFlow(List vehicleFlows) { + commonService.saveEvent(vehicleFlows, BnhzConstant.BlackCarEvent.CLL, DateUtils.getTimestamp()); + return vehicleFlows.size(); + } + + @Override + public void syncDevice() { + //黑烟车属于自动推送,入口为insertPoint + } + + @Override + public String getProductName() { + return "黑烟车点位设备"; + } + + private Product initProduct() { + Product query = new Product(); + String transport = BnhzConstant.TRANSPORT.HTTP; + query.setTransport(transport); + query.setProductName(getProductName()); + List products = productService.selectProductList(query); + Product product; + if (CollectionUtils.isEmpty(products)) { + //初始化产品 + product = new Product(); + product.setTenantId(1L); + product.setProductName(getProductName()); + product.setTransport(transport); + //默认选择其他 + product.setCategoryId(7L); + product.setCategoryName("其他"); + + //直连设备 + product.setDeviceType(DeviceType.DIRECT_DEVICE.getCode()); + product.setLocationWay(3); + //默认以太网 + product.setNetworkMethod(3); + //默认认证方式HTTP + product.setVertificateMethod(3); + product.setProtocolCode(BnhzConstant.TRANSPORT.HTTP); + product.setIsSys(1); + product.setRemark("系统自动同步"); + productService.insertProduct(product); + } else { + product = products.stream().findFirst().get(); + } + return product; + } + + @SneakyThrows + public void initModel(Point point, Long productId, String productName) { + Map allFields = ReflectUtils.getAllFields(point); + List thingsModelEventVOS = thingsModelService.listEventModeList(productId); + if (CollectionUtils.isEmpty(thingsModelEventVOS)) { + //初始化属性 + List propertyThingsModel = ThingsModelUtils.toPropertyThingsModel(allFields, productId, productName); + propertyThingsModel.forEach(thingsModelService::insertThingsModel); + //初始化事件 + //1.交通流量 + Map trafficFlowInfoFields = ReflectUtils.getAllFields(new TrafficFlowInfo()); + ThingsModel trafficFlowInfoModel = ThingsModelUtils.toEventThingsModel(trafficFlowInfoFields, productId, productName,"交通流量", BnhzConstant.BlackCarEvent.JTLL); + thingsModelService.insertThingsModel(trafficFlowInfoModel); + //2.黑烟车 + Map blackCarFields = ReflectUtils.getAllFields(new BlackSmokeVehicle()); + ThingsModel blackCarModel = ThingsModelUtils.toEventThingsModel(blackCarFields, productId, productName,"黑烟车信息", BnhzConstant.BlackCarEvent.HYC); + thingsModelService.insertThingsModel(blackCarModel); + //3.车流量 + Map vehicleFlowFields = ReflectUtils.getAllFields(new VehicleFlow()); + ThingsModel vehicleFlowModel = ThingsModelUtils.toEventThingsModel(vehicleFlowFields, productId, productName,"车流量", BnhzConstant.BlackCarEvent.CLL); + thingsModelService.insertThingsModel(vehicleFlowModel); + //4.摄像头信息 + Map cameraInfoFields = ReflectUtils.getAllFields(new CameraInfo()); + ThingsModel cameraInfoModel = ThingsModelUtils.toEventThingsModel(cameraInfoFields, productId, productName,"摄像头信息", BnhzConstant.BlackCarEvent.SXTXX); + thingsModelService.insertThingsModel(cameraInfoModel); + propertyThingsModel.add(trafficFlowInfoModel); + propertyThingsModel.add(blackCarModel); + propertyThingsModel.add(vehicleFlowModel); + propertyThingsModel.add(cameraInfoModel); + columnModeOperationsService.ddl(productId, propertyThingsModel); + Product productUpdate = new Product(); + productUpdate.setProductId(productId); + productUpdate.setStatus(2); + productService.updateProduct(productUpdate); + } + } + + + private Device addDevice(Point point, Product product, Long deviceId) { + Device device = new Device(); + device.setDeviceId(deviceId); + device.setProductId(product.getProductId()); + device.setProductName(product.getProductName()); + boolean online = "1".equals(point.getDwzt()); + device.setStatus(online ? 3 : 4); + device.setLocationWay(3); + device.setFirmwareVersion(new BigDecimal(1)); + device.setSerialNumber(point.getDwbh()); + device.setDeviceType(DeviceType.DIRECT_DEVICE.getCode()); + device.setIsSimulate(0); + device.setDeviceName(point.getDwmc()); + device.setTenantId(1L); + device.setLongitude(point.getDdjd()); + device.setLatitude(point.getDdwd()); + device.setRemark("系统自动同步"); + if (ObjectUtils.isEmpty(device.getDeviceId())) { + //第一次保存就为激活时间 + device.setActiveTime(new Date()); + deviceService.insertDevice(device); + } else { + deviceService.updateDevice(device); + } + return device; + } + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/common/CommonService.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/common/CommonService.java new file mode 100644 index 0000000..5679d8f --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/common/CommonService.java @@ -0,0 +1,14 @@ +package com.bnhz.adapter.service.common; + +import com.bnhz.iot.model.DataId; + +import java.util.List; + +/** + * @author Leo + * @date 2024/7/11 16:58 + */ +public interface CommonService { + + void saveEvent(List blackCarIds, String eventId, Long batchNo); +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/common/impl/CommonServiceImpl.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/common/impl/CommonServiceImpl.java new file mode 100644 index 0000000..b44176d --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/common/impl/CommonServiceImpl.java @@ -0,0 +1,77 @@ +package com.bnhz.adapter.service.common.impl; + +import com.bnhz.adapter.service.common.CommonService; +import com.bnhz.adapter.util.ThingsModelUtils; +import com.bnhz.common.utils.reflect.ReflectUtils; +import com.bnhz.iot.domain.Device; +import com.bnhz.iot.domain.ThingsModel; +import com.bnhz.iot.model.DataId; +import com.bnhz.iot.service.IDeviceService; +import com.bnhz.iot.service.IThingsModelService; +import com.bnhz.iot.tdengine.service.IColumnModeOperationsService; +import com.bnhz.mq.model.ReportDataBo; +import com.bnhz.mq.service.IDataHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author Leo + * @date 2024/7/11 16:58 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CommonServiceImpl implements CommonService { + + + private final IDeviceService deviceService; + + private final IThingsModelService thingsModelService; + + + private final IDataHandler dataHandler; + + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveEvent(List blackCarIds, String eventId, Long batchNo) { + if (CollectionUtils.isEmpty(blackCarIds)) { + return; + } + ThingsModel query = new ThingsModel(); + query.setIdentifier(eventId); + Set serNumSet = blackCarIds.stream() + .map(item-> item.getSerialNumber().toUpperCase(Locale.ROOT)) + .collect(Collectors.toSet()); + Map deviceMap = deviceService.selectDevicesBySerialNumber(serNumSet) + .stream() + .collect(Collectors.toMap(Device::getSerialNumber, Function.identity())); + ThingsModel thingsModel = thingsModelService.selectSingleThingsModel(query); + blackCarIds.forEach(dataId -> { + String serialNumber = dataId.getSerialNumber(); + Device device = deviceMap.get(serialNumber); + if (ObjectUtils.isEmpty(device)) { + return; + } + ReportDataBo eventReportDataBo; + try { + eventReportDataBo = ThingsModelUtils.toEventReportDataBo(ReflectUtils.getAllFields(dataId), thingsModel.getProductId(), serialNumber, thingsModel.getModelId(), dataId.getDataTime(), device.getDeviceId(), thingsModel.getIdentifier(), batchNo); + } catch (IllegalAccessException e) { + log.error("转换错误", e); + throw new RuntimeException(e); + } + dataHandler.reportEvent(eventReportDataBo); + }); + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/fumes/IFumesService.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/fumes/IFumesService.java new file mode 100644 index 0000000..30c8487 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/fumes/IFumesService.java @@ -0,0 +1,47 @@ +package com.bnhz.adapter.service.fumes; + +import com.bnhz.iot.service.base.SyncDevice; + +/** + * @author Leo + * @date 2024/7/1 15:54 + */ +public interface IFumesService extends SyncDevice { + + /** + * 同步报警信息(一小时一次) + */ + void syncAlarmMsg(); + + /** + * 报警检测(一天一次) + */ + void syncDetector(); + + + /** + * 十分钟数据(10分钟一次) + */ + void syncTenData(); + + /** + * 一分钟数据(5分钟一次) + */ + void syncOneData(); + + /** + * 点位事件(1小时一次) + */ + void syncPointEvent(); + + /** + * 同步减排统计(一天一次) + */ + void syncReduce(); + + /** + * 同步实时数据(5分钟一次) + */ + void syncRealTimeData(); + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/fumes/impl/FumesServiceImpl.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/fumes/impl/FumesServiceImpl.java new file mode 100644 index 0000000..25f31b7 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/fumes/impl/FumesServiceImpl.java @@ -0,0 +1,825 @@ +package com.bnhz.adapter.service.fumes.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSONObject; +import com.bnhz.adapter.model.fumes.*; +import com.bnhz.adapter.service.common.CommonService; +import com.bnhz.adapter.service.fumes.IFumesService; +import com.bnhz.adapter.util.ThingsModelUtils; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.page.PageResult; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.PageUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.json.JsonUtils; +import com.bnhz.common.utils.reflect.ReflectUtils; +import com.bnhz.iot.domain.Device; +import com.bnhz.iot.domain.Product; +import com.bnhz.iot.domain.ThingsModel; +import com.bnhz.iot.enums.DeviceType; +import com.bnhz.iot.model.ThingsModels.ThingsModelEventVO; +import com.bnhz.iot.model.ThingsModels.ThingsModelQuery; +import com.bnhz.iot.service.IDeviceService; +import com.bnhz.iot.service.IProductService; +import com.bnhz.iot.service.IThingsModelService; +import com.bnhz.iot.tdengine.service.IColumnModeOperationsService; +import com.bnhz.mq.model.ReportDataBo; +import com.bnhz.mq.service.IDataHandler; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.bnhz.common.constant.CacheConstants.*; + +/** + * @author Leo + * @date 2024/7/1 15:59 + */ + +@Slf4j +@Service +@Transactional(rollbackFor = Exception.class) +public class FumesServiceImpl implements IFumesService { + + @Value("${fumes.host}") + private String host; + + @Value("${fumes.username}") + private String username; + + @Value("${fumes.password}") + private String password; + + @Value("${fumes.areacode}") + private String areaCode; + + + private final RedisCache redisCache; + + private final IProductService productService; + + private final IDeviceService deviceService; + + private final IThingsModelService thingsModelService; + + private final IColumnModeOperationsService columnModeOperationsService; + + private final IDataHandler dataHandler; + + private final CommonService commonService; + + + private final Executor executor; + + public FumesServiceImpl(RedisCache redisCache, IProductService productService, IDeviceService deviceService, IThingsModelService thingsModelService, IColumnModeOperationsService columnModeOperationsService, IDataHandler dataHandler, CommonService commonService, @Qualifier(value = BnhzConstant.TASK.MESSAGE_CONSUME_TASK) Executor executor) { + this.redisCache = redisCache; + this.productService = productService; + this.deviceService = deviceService; + this.thingsModelService = thingsModelService; + this.columnModeOperationsService = columnModeOperationsService; + this.dataHandler = dataHandler; + this.commonService = commonService; + this.executor = executor; + } + + /** + * 登陆 + */ + private final static String AUTH_URL = "/loginAction"; + + /** + * 监测点信息 + */ + private final static String MONITORING_POINT_URL = "/admin/listLocale"; + + /** + * 消息管理/报警消息 + */ + private final static String ALARM_MSG_URL = "/admin/listAlarmMsg"; + + /** + * 报警管理-监测 + */ + private final static String DETECTOR_DAILY_URL = "/admin/queryDataDetectorDaily2"; + + /** + * 十分钟数据 + */ + private final static String TEN_MIN_URL = "/admin/queryTenMinData"; + + /** + * 一分钟数据 + */ + private final static String ONE_MIN_URL = "/admin/queryDataDetector"; + + /** + * 所有点位事件处理记录 + */ + private final static String POINT_EVENT_URL = "/admin/listEventList"; + + /** + * 监测点位减排统计 + */ + private final static String LOCALE_REDUCE_URL = "/admin/locale_reduce_data"; + + /** + * 实时数据 + */ + private final static String REAL_TIME_URL = "/admin/listDataIntime"; + + /** + * 一天一次同步设备信息 + */ + @Override + public void syncDevice() { + Product product = initProduct(); + initModel(product.getProductId(), product.getProductName()); + + Device queryDevice = new Device(); + queryDevice.setProductId(product.getProductId()); + Map existDeviceMap = deviceService.selectDeviceList(queryDevice) + .stream() + .collect(Collectors.toMap(Device::getSerialNumber, Function.identity())); + loopSyncDevice(product, existDeviceMap); + + } + + @Override + public String getProductName() { + return "餐饮油烟点位设备"; + } + + + @Override + public void syncAlarmMsg() { + int pageNo = 0; + int pageSize = 100; + boolean next; + LocalDateTime lastAlarmTime = getLastAlarmTime(); + LocalDateTime now = LocalDateTime.now(); + do { + PageResult alarmMsgPage = getAlarmMsgPage(pageNo, pageSize, lastAlarmTime, now); + next = PageUtils.hasNextByStartAt(alarmMsgPage.getTotal(), pageNo, pageSize); + List rows = alarmMsgPage.getRows(); + if (!CollectionUtils.isEmpty(rows)) { + rows.forEach(item -> item.setMn(BnhzConstant.FumesBis.INTERFACE_PREFIX + item.getMn())); + } + commonService.saveEvent(rows, BnhzConstant.FumesEvent.ALARM_MSG, DateUtils.getTimestamp()); + pageNo = pageNo + pageSize; + } while (next); + redisCache.setCacheObject(FUMES_ALARM_LAST_TIME, DateUtils.toTimestamp(now)); + } + + private LocalDateTime getLastAlarmTime() { + Object cacheObject = redisCache.getCacheObject(FUMES_ALARM_LAST_TIME); + if (!ObjectUtils.isEmpty(cacheObject)) { + return DateUtils.toLocalDateTime((Long) cacheObject * 1000); + } + return LocalDateTime.now().minusDays(1); + } + + @Override + public void syncDetector() { + + int pageNo = 1; + int pageSize = 100; + boolean next; + do { + PageResult detectorPage = getDetectorDailyPage(pageNo, pageSize, LocalDate.now().minusDays(1)); + next = PageUtils.hasNext(detectorPage.getTotal(), pageNo, pageSize); + List rows = detectorPage.getRows(); + if (!CollectionUtils.isEmpty(rows)) { + rows.forEach(item -> item.setMn(BnhzConstant.FumesBis.INTERFACE_PREFIX + item.getMn())); + } + commonService.saveEvent(rows, BnhzConstant.FumesEvent.DETECTOR, DateUtils.getTimestamp()); + pageNo++; + } while (next); + + } + + @Override + public void syncTenData() { + long lastTenDataTime = getLastTenDataTime(); + Set snSet = getDeviceList() + .stream() + .map(device -> device.getSerialNumber().replace(BnhzConstant.FumesBis.INTERFACE_PREFIX, "")) + .collect(Collectors.toSet()); + List tenMinData = getTenMinData(null, LocalDateTime.now().minusMinutes(12), LocalDateTime.now()) + .stream() + .filter(fumesTenMin -> snSet.contains(fumesTenMin.getSerialNumber())) + .filter(fumesTenMin -> fumesTenMin.getCreateAt() > lastTenDataTime) + .peek(fumesTenMin -> fumesTenMin.setMn(BnhzConstant.FumesBis.INTERFACE_PREFIX + fumesTenMin.getMn())) + .collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(tenMinData)) { + redisCache.setCacheObject(FUMES_TEN_LAST_TIME, tenMinData.iterator().next().getCreateAt()); + commonService.saveEvent(tenMinData, BnhzConstant.FumesEvent.TEN_DATA, DateUtils.getTimestamp()); + } + } + + private long getLastTenDataTime() { + Object cacheObject = redisCache.getCacheObject(FUMES_TEN_LAST_TIME); + if (cacheObject instanceof Long) { + return (long) cacheObject; + } + return 0L; + } + + private List getDeviceList() { + Product query = new Product(); + String transport = BnhzConstant.TRANSPORT.HTTP; + String productName = getProductName(); + query.setTransport(transport); + query.setProductName(productName); + List products = productService.selectProductList(query); + Product product = products.iterator().next(); + Device queryDevice = new Device(); + queryDevice.setProductId(product.getProductId()); + return deviceService.selectDeviceList(queryDevice); + } + + @Override + public void syncOneData() { + LocalDateTime lastOneDataTime = getLastOneDataTime(); + + LocalDateTime now = LocalDateTime.now(); + Set serialNumberSet = getDeviceList().stream() + .map(device -> device.getSerialNumber().replace(BnhzConstant.FumesBis.INTERFACE_PREFIX, "")) + .collect(Collectors.toSet()); + List oneMinData = getOneMinData(null, lastOneDataTime, now) + .stream() + .filter(fumesOneMin -> serialNumberSet.contains(fumesOneMin.getSerialNumber())) + .peek(fumesOneMin -> fumesOneMin.setMn(BnhzConstant.FumesBis.INTERFACE_PREFIX + fumesOneMin.getSerialNumber())) + .collect(Collectors.toList()); + commonService.saveEvent(oneMinData, BnhzConstant.FumesEvent.ONE_DATA, DateUtils.getTimestamp()); + redisCache.setCacheObject(FUMES_ONE_LAST_TIME, DateUtils.toTimestamp(now)); + } + + private LocalDateTime getLastOneDataTime() { + Object cacheObject = redisCache.getCacheObject(FUMES_ONE_LAST_TIME); + if (!ObjectUtils.isEmpty(cacheObject)) { + LocalDateTime lastOneDataTime = DateUtils.toLocalDateTime((Long) cacheObject * 1000); + if (lastOneDataTime.getDayOfYear() != LocalDateTime.now().getDayOfYear()) { + lastOneDataTime = LocalDateTime.now().minusMinutes(5); + } + return timeRangLimit(lastOneDataTime); + } + return LocalDateTime.now().minusMinutes(30); + } + + /** + * 时间范围限制 + * 如果大于10个小时的差值,就取当前时间30分钟前 + * + * @param time 待校验的时间 + * @return 范围内的时间 + */ + private LocalDateTime timeRangLimit(LocalDateTime time) { + long bm = DateUtils.betweenMinute(LocalDateTime.now(), time); + if (bm > 60 * 10) { + return LocalDateTime.now().minusMinutes(30); + } + return time; + } + + @Override + public void syncPointEvent() { + Product query = new Product(); + String transport = BnhzConstant.TRANSPORT.HTTP; + String productName = getProductName(); + query.setTransport(transport); + query.setProductName(productName); + List products = productService.selectProductList(query); + Product product = products.iterator().next(); + ThingsModel modelQuery = new ThingsModel(); + modelQuery.setProductId(product.getProductId()); + modelQuery.setIdentifier(BnhzConstant.FumesEvent.POINT_EVENT); + ThingsModel eventModel = thingsModelService.selectSingleThingsModel(modelQuery); + ThingsModelQuery thingsModelQuery = new ThingsModelQuery(); + thingsModelQuery.setType(BnhzConstant.ModelType.EVENT); + thingsModelQuery.setSize(1); + thingsModelQuery.setProductId(product.getProductId()); + thingsModelQuery.setModelId(eventModel.getModelId()); + List> responseList = columnModeOperationsService.query(thingsModelQuery); + FumesPointEventRecord pointEvent = getPointEvent(); + List onlineEventList = pointEvent.getOnlineEventList(); + if (!CollectionUtils.isEmpty(onlineEventList)) { + long lastPointEventTime = getLastPointEvent(); + onlineEventList = onlineEventList.stream() + .filter(item -> item.getCalculateDate() > lastPointEventTime) + .peek(item -> item.setLocaleMn(BnhzConstant.FumesBis.INTERFACE_PREFIX + item.getLocaleMn())) + .collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(onlineEventList)) { + redisCache.setCacheObject(FUMES_POINT_EVENT_LAST_TIME, onlineEventList.iterator().next().getCalculateDate()); + } + } + if (CollectionUtils.isEmpty(responseList)) { + commonService.saveEvent(onlineEventList, BnhzConstant.FumesEvent.POINT_EVENT, DateUtils.getTimestamp()); + } else { + String timestamp = responseList.stream() + .map(item -> (String) item.get("calculatedate")) + .filter(StringUtils::isNotEmpty) + .findFirst() + .orElseThrow(() -> new ServiceException("同步点位事件报错")); + Long min = Long.valueOf(timestamp); + List pendingSaveList = onlineEventList.stream() + .filter(event -> event.getCalculateDate() - min > 0) + .collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(pendingSaveList)) { + commonService.saveEvent(pendingSaveList, BnhzConstant.FumesEvent.POINT_EVENT, DateUtils.getTimestamp()); + } + + } + } + + private long getLastPointEvent() { + Object cacheObject = redisCache.getCacheObject(FUMES_POINT_EVENT_LAST_TIME); + if (cacheObject instanceof Long) { + return (long) cacheObject; + } + return 0L; + } + + @Override + public void syncReduce() { + int startAt = 0; + int pageSize = 1000; + boolean next; + do { + PageResult reduceEventPage = getReduceEventPage(startAt, pageSize); + next = PageUtils.hasNextByStartAt(reduceEventPage.getTotal(), startAt, reduceEventPage.getRows().size()); + List rows = reduceEventPage.getRows(); + if (!CollectionUtils.isEmpty(rows)) { + rows.forEach(item -> item.setMn(BnhzConstant.FumesBis.INTERFACE_PREFIX + item.getMn())); + } + rows.forEach(fumesReduce -> commonService.saveEvent(rows, BnhzConstant.FumesEvent.REDUCE_EVENT, DateUtils.getTimestamp())); + startAt = startAt + pageSize; + } while (next); + + } + + @Override + public void syncRealTimeData() { + int startAt = 0; + int pageSize = 100; + boolean next; + long lastRealTime = getLastRealTime(); + long maxAcquitAt = 0L; + do { + PageResult reduceEventPage = realTimePage(startAt, pageSize); + next = PageUtils.hasNextByStartAt(reduceEventPage.getTotal(), startAt, reduceEventPage.getRows().size()); + List rows = reduceEventPage.getRows(); + if (!CollectionUtils.isEmpty(rows)) { + rows = rows.stream() + .filter(item -> item.getAcquitAt() > lastRealTime) + .peek(item -> item.setId(BnhzConstant.FumesBis.INTERFACE_PREFIX + item.getId())) + .collect(Collectors.toList()); + if (!CollectionUtil.isEmpty(rows)) { + long acquitAt = rows.iterator().next().getAcquitAt(); + if (acquitAt > maxAcquitAt) { + maxAcquitAt = acquitAt; + } + commonService.saveEvent(unzipRealTimeData(rows), BnhzConstant.FumesEvent.REAL_TIME_EVENT, DateUtils.getTimestamp()); + } + } + startAt = startAt + pageSize; + } while (next); + if (maxAcquitAt > 0) { + redisCache.setCacheObject(FUMES_REAL_TIME, maxAcquitAt); + } + } + + private long getLastRealTime() { + Object cacheObject = redisCache.getCacheObject(FUMES_REAL_TIME); + if (cacheObject instanceof Long) { + return (long) cacheObject; + } + return 0L; + } + + + private void loopSyncDevice(Product product, Map existDeviceMap) { + int startAt = 0; + int pageSize = 500; + boolean next; + do { + log.info("current index:{}", startAt); + PageResult videoDevicePage = getMonitorPointPage(startAt, pageSize); + next = startAt + pageSize < videoDevicePage.getTotal(); + List data = videoDevicePage.getRows(); + if (!CollectionUtils.isEmpty(data)) { + data.forEach(item -> { + if (StringUtils.isNotEmpty(item.getMnLast())) { + item.setMnLast(BnhzConstant.FumesBis.INTERFACE_PREFIX + item.getMnLast()); + } + }); + } + log.info("[餐饮油烟同步设备],同步数量===》{}", data.size()); + executor.execute(() -> data.forEach(fumesMonitorPoint -> { + if (ObjectUtils.isEmpty(fumesMonitorPoint.getMnLast())) { + return; + } + try { + Device existDevice = existDeviceMap.get(fumesMonitorPoint.getMnLast()); + addDevice(fumesMonitorPoint, product, ObjectUtils.isEmpty(existDevice) ? null : existDevice.getDeviceId()); + //2.插入数据属性数据 + ReportDataBo propertyReportDataBo = ThingsModelUtils.toPropertyReportDataBo(ReflectUtils.getAllFields(fumesMonitorPoint), product.getProductId(), fumesMonitorPoint.getMnLast()); + dataHandler.reportData(propertyReportDataBo); + } catch (Exception e) { + log.error("保存设备信息错误", e); + } + })); + startAt = startAt + pageSize; + } while (next); + } + + + private String getToken() { + String token = redisCache.getCacheObject(FUMES_TOKEN); + if (StringUtils.isNotEmpty(token)) { + return token; + } + HashMap paramMap = new HashMap<>(); + paramMap.put("username", username); + paramMap.put("password", password); + paramMap.put("noCode", true); + String response = HttpUtil.createPost(host + AUTH_URL) + .body(JSONObject.toJSONString(paramMap)) + .execute() + .body(); + FumesRes tokenFumesRes = JsonUtils.parseObject(response, new TypeReference>() { + }); + if (tokenFumesRes.getStatus() == 200) { + token = tokenFumesRes.getData().getToken(); + redisCache.setCacheObject(FUMES_TOKEN, token, 24, TimeUnit.HOURS); + return token; + } else { + log.error("[餐饮油烟]获取登陆token失败,返回信息:{}", response); + throw new ServiceException("[餐饮油烟]获取登陆token失败"); + } + + } + + + public PageResult getMonitorPointPage(Integer pageNo, Integer pageSize) { + Map params = new HashMap<>(); + params.put("StartAt", pageNo); + params.put("Size", pageSize); +// Map params2 = new HashMap<>(); +// params2.put("id", "c8t83n56qp1pmvihe0i0"); +// params.put("Param", params2); + + //c8t83n56qp1pmvihe0i0 + String body = HttpUtil.createPost(host + MONITORING_POINT_URL) + .header("auth", getToken()) + .body(JSONObject.toJSONString(params)) + .execute() + .body(); + + FumesRes> response = JsonUtils.parseObject(body, new TypeReference>>() { + }); + FumesPage data = response.getData(); + return new PageResult<>(data.getContent(), data.getTotal()); + } + + /** + * 获取报警信息 + * + * @param pageNo 起始页 + * @param pageSize 每页条数 + * @param begin 开始时间戳(秒) + * @param end 结束时间戳(秒) + * @return PageResult + */ + public PageResult getAlarmMsgPage(Integer pageNo, Integer pageSize, LocalDateTime begin, LocalDateTime end) { + Map params = new HashMap<>(); + Map innerParams = new HashMap<>(); + innerParams.put("begin", DateUtils.toTimestamp(begin)); + innerParams.put("end", DateUtils.toTimestamp(end)); + params.put("StartAt", pageNo); + params.put("Size", pageSize); + params.put("Param", innerParams); + String body = HttpUtil.createPost(host + ALARM_MSG_URL) + .header("auth", getToken()) + .body(JSONObject.toJSONString(params)) + .execute() + .body(); + FumesRes> response = JsonUtils.parseObject(body, new TypeReference>>() { + }); + FumesPage data = response.getData(); + return new PageResult<>(data.getContent(), data.getTotal()); + } + + /** + * 报警管理-监测 + * + * @param pageNo 起始页 + * @param pageSize 每页条数 + * @param date 日期(yyyy-MM-dd) + * @return PageResult + */ + + public PageResult getDetectorDailyPage(Integer pageNo, Integer pageSize, LocalDate date) { + Map params = new HashMap<>(); + params.put("Page", pageNo); + params.put("Perpage", pageSize); + params.put("AcquitDate", DateUtils.toDateStr(date)); + String body = HttpUtil.createPost(host + DETECTOR_DAILY_URL) + .header("auth", getToken()) + .body(JSONObject.toJSONString(params)) + .execute() + .body(); + FumesRes> response = JsonUtils.parseObject(body, new TypeReference>>() { + }); + FumesPage data = response.getData(); + return new PageResult<>(data.getContent(), data.getResultsPageInfo().getTotal()); + } + + public List getTenMinData(String mn, LocalDateTime beginDateTime, LocalDateTime endDateTime) { + Map params = new HashMap<>(); + if (StringUtils.isNotEmpty(mn)) { + params.put("mn", mn); + } + params.put("AcquitAtTimeBegin", DateUtils.toDateTimeStr(beginDateTime)); + params.put("AcquitAtTimeEnd", DateUtils.toDateTimeStr(endDateTime)); + String body = HttpUtil.createPost(host + TEN_MIN_URL) + .header("auth", getToken()) + .body(JSONObject.toJSONString(params)) + .execute() + .body(); + + FumesRes> response = JsonUtils.parseObject(body, new TypeReference>>() { + }); + return response.getData().getData(); + } + + public List getOneMinData(String mn, LocalDateTime beginDate, LocalDateTime endDate) { + Map params = new HashMap<>(); + if (StringUtils.isNotEmpty(mn)) { + params.put("mn", mn); + } + params.put("Begin", DateUtils.toTimestamp(beginDate)); + params.put("End", DateUtils.toTimestamp(endDate)); + String body = HttpUtil.createPost(host + ONE_MIN_URL) + .header("auth", getToken()) + .body(JSONObject.toJSONString(params)) + .execute() + .body(); + + FumesRes> response = JsonUtils.parseObject(body, new TypeReference>>() { + }); + if (ObjectUtils.isEmpty(response.getData())) { + return new ArrayList<>(); + } + return response.getData().getData(); + } + + private FumesPointEventRecord getPointEvent() { + Map params = new HashMap<>(); + params.put("AreaCode", areaCode); + String body = HttpUtil.createPost(host + POINT_EVENT_URL) + .header("auth", getToken()) + .body(JSONObject.toJSONString(params)) + .execute() + .body(); + FumesRes response = JsonUtils.parseObject(body, new TypeReference>() { + }); + return response.getData(); + } + + /** + * 获取减排统计数据 + * + * @param startAt 偏移量 + * @param pageSize 每页条数 + * @return 减排统计数据 + */ + private PageResult getReduceEventPage(Integer startAt, Integer pageSize) { + Map params = new HashMap<>(); + params.put("StartAt", startAt); + params.put("Size", pageSize); + Map params2 = new HashMap<>(); + //获取昨天的数据 + params2.put("acquit_at", DateUtils.toDateStr(LocalDate.now().minusDays(1))); + params.put("Param", params2); + String body = HttpUtil.createGet(host + LOCALE_REDUCE_URL) + .header("auth", getToken()) + .body(JSONObject.toJSONString(params)) + .execute() + .body(); + FumesRes> response = JsonUtils.parseObject(body, new TypeReference>>() { + }); + FumesPage data = response.getData(); + return new PageResult<>(data.getContent(), data.getTotal()); + } + + /** + * 实时数据 + * Id 实时数据的设备编号(中间有“-”连接代表该监测点有多个设备绑定) + * + * @param startAt 偏移量 + * @param pageSize 每页条数 + * @return 实时数据 + */ + + private PageResult realTimePage(Integer startAt, Integer pageSize) { + Map params = new HashMap<>(); + params.put("StartAt", startAt); + params.put("Size", pageSize); + params.put("SortMode", "asc"); + String body = HttpUtil.createPost(host + REAL_TIME_URL) + .header("auth", getToken()) + .body(JSONObject.toJSONString(params)) + .execute() + .body(); + FumesRes> response = JsonUtils.parseObject(body, new TypeReference>>() { + }); + FumesPage data = response.getData(); + return new PageResult<>(data.getContent(), data.getTotal()); + } + + /** + * 解压实时数据 + * + * @param zipFumesRealTimes 压缩的实时数据 + * @return 解压后的实时数据 + */ + private List unzipRealTimeData(List zipFumesRealTimes) { + return zipFumesRealTimes.stream() + .flatMap(fumesRealTime -> { + String id = fumesRealTime.getId(); + if (id.contains("-")) { + String[] split = id.split("-"); + return Arrays.stream(split).map(singleId -> { + FumesRealTime unzipRealTime = new FumesRealTime(); + BeanUtils.copyProperties(fumesRealTime, unzipRealTime); + unzipRealTime.setId(singleId); + return unzipRealTime; + }); + } + return Stream.of(fumesRealTime); + }).collect(Collectors.toList()); + } + + + private Product initProduct() { + Product query = new Product(); + String transport = BnhzConstant.TRANSPORT.HTTP; + String productName = getProductName(); + query.setTransport(transport); + query.setProductName(productName); + List products = productService.selectProductList(query); + Product product; + if (CollectionUtils.isEmpty(products)) { + //初始化产品 + product = new Product(); + product.setTenantId(1L); + product.setProductName(productName); + product.setTransport(transport); + //默认选择其他 + product.setCategoryId(7L); + product.setCategoryName("其他"); + + //直连设备 + product.setDeviceType(DeviceType.DIRECT_DEVICE.getCode()); + product.setLocationWay(3); + //默认以太网 + product.setNetworkMethod(3); + //默认认证方式HTTP + product.setVertificateMethod(3); + product.setProtocolCode(BnhzConstant.TRANSPORT.HTTP); + product.setRemark("系统自动同步"); + productService.insertProduct(product); + } else { + product = products.stream().findFirst().get(); + } + return product; + } + + private Device addDevice(FumesMonitorPoint fumesMonitorPoint, Product product, Long deviceId) { + Device device = new Device(); + device.setDeviceId(deviceId); + device.setProductId(product.getProductId()); + device.setProductName(product.getProductName()); + boolean online = "NORMAL".equals(fumesMonitorPoint.getStatusOfRecord()); + device.setStatus(online ? 3 : 4); + device.setLocationWay(3); + device.setFirmwareVersion(new BigDecimal(1)); + device.setSerialNumber(fumesMonitorPoint.getMnLast()); + device.setDeviceType(DeviceType.DIRECT_DEVICE.getCode()); + device.setIsSimulate(0); + device.setDeviceName(fumesMonitorPoint.getName()); + device.setTenantId(1L); + if (!ObjectUtils.isEmpty(fumesMonitorPoint.getLng())) { + try { + device.setLongitude(new BigDecimal(StringUtils.subByLength(fumesMonitorPoint.getLng(), 10))); + } catch (NumberFormatException e) { + log.error("转换错误,device SN==>{}, lng==>{}", fumesMonitorPoint.getMnLast(), fumesMonitorPoint.getLng()); + } + + } + if (!ObjectUtils.isEmpty(fumesMonitorPoint.getLat())) { + try { + device.setLatitude(new BigDecimal(StringUtils.subByLength(fumesMonitorPoint.getLat(), 10))); + + } catch (NumberFormatException e) { + log.error("转换错误,device SN==>{}, lat==>{}", fumesMonitorPoint.getMnLast(), fumesMonitorPoint.getLat()); + } + } + device.setRemark("系统自动同步"); + if (ObjectUtils.isEmpty(device.getDeviceId())) { + //第一次保存就为激活时间 + device.setActiveTime(new Date()); + deviceService.insertDevice(device); + } else { + deviceService.updateDevice(device); + } + return device; + } + + @SneakyThrows + private void initModel(Long productId, String productName) { + List thingsModelEventVOS = thingsModelService.listEventModeList(productId); + if (CollectionUtils.isEmpty(thingsModelEventVOS)) { + //初始化propertyThingsModel + Map propertyAllFields = ReflectUtils.getAllFields(new FumesMonitorPoint()); + List propertyThingsModel = ThingsModelUtils.toPropertyThingsModel(propertyAllFields, productId, productName); + propertyThingsModel.forEach(thingsModelService::insertThingsModel); + //初始化事件 + //1.报警信息 + Map alarmMsgAllFields = ReflectUtils.getAllFields(new FumesAlarmMsg()); + ThingsModel alarmMsgModel = ThingsModelUtils.toEventThingsModel(alarmMsgAllFields, productId, productName, "消息管理/报警信息", BnhzConstant.FumesEvent.ALARM_MSG); + thingsModelService.insertThingsModel(alarmMsgModel); + //2.报警管理-监测 + Map detectorAllFields = ReflectUtils.getAllFields(new FumesDetectorDaily()); + ThingsModel detectorModel = ThingsModelUtils.toEventThingsModel(detectorAllFields, productId, productName, "报警管理-监测", BnhzConstant.FumesEvent.DETECTOR); + thingsModelService.insertThingsModel(detectorModel); + //3.十分钟数据 + Map tenAllFields = ReflectUtils.getAllFields(new FumesTenMin()); + ThingsModel tenModel = ThingsModelUtils.toEventThingsModel(tenAllFields, productId, productName, "十分钟数据", BnhzConstant.FumesEvent.TEN_DATA); + thingsModelService.insertThingsModel(tenModel); + //4.一分钟数据 + Map oneAllFields = ReflectUtils.getAllFields(new FumesOneMin()); + ThingsModel oneModel = ThingsModelUtils.toEventThingsModel(oneAllFields, productId, productName, "一分钟数据", BnhzConstant.FumesEvent.ONE_DATA); + thingsModelService.insertThingsModel(oneModel); + //5.点位事件 + Map pointEventAllFields = ReflectUtils.getAllFields(new FumesPointEventRecord.Event()); + ThingsModel pointEventModel = ThingsModelUtils.toEventThingsModel(pointEventAllFields, productId, productName, "点位事件", BnhzConstant.FumesEvent.POINT_EVENT); + thingsModelService.insertThingsModel(pointEventModel); + //6.减排统计 + Map reduceEventAllFields = ReflectUtils.getAllFields(new FumesReduce()); + ThingsModel reduceEventModel = ThingsModelUtils.toEventThingsModel(reduceEventAllFields, productId, productName, "监测点位减排统计", BnhzConstant.FumesEvent.REDUCE_EVENT); + thingsModelService.insertThingsModel(reduceEventModel); + + //6.实时数据 + Map realTimeEventAllFields = ReflectUtils.getAllFields(new FumesRealTime()); + ThingsModel realTimeEventModel = ThingsModelUtils.toEventThingsModel(realTimeEventAllFields, productId, productName, "实时数据", BnhzConstant.FumesEvent.REAL_TIME_EVENT); + thingsModelService.insertThingsModel(realTimeEventModel); + + propertyThingsModel.add(alarmMsgModel); + propertyThingsModel.add(detectorModel); + propertyThingsModel.add(tenModel); + propertyThingsModel.add(oneModel); + propertyThingsModel.add(pointEventModel); + propertyThingsModel.add(reduceEventModel); + propertyThingsModel.add(realTimeEventModel); + columnModeOperationsService.ddl(productId, propertyThingsModel); + Product productUpdate = new Product(); + productUpdate.setProductId(productId); + productUpdate.setStatus(2); + productService.updateProduct(productUpdate); + } + } + + + @Data + public static class Token { + + @JsonProperty("Token") + private String token; + } + + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/kacheck/IKaCheckService.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/kacheck/IKaCheckService.java new file mode 100644 index 0000000..cc35651 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/kacheck/IKaCheckService.java @@ -0,0 +1,55 @@ +package com.bnhz.adapter.service.kacheck; + +import com.bnhz.adapter.model.blackcar.*; +import com.bnhz.adapter.model.kacheck.KaCameraInfo; +import com.bnhz.adapter.model.kacheck.KaPoint; +import com.bnhz.adapter.model.kacheck.KaTrafficFlowInfo; +import com.bnhz.adapter.model.kacheck.KaVehicleFlow; + +import java.util.List; + +/** + * @author Leo + * @date 2024/6/28 09:58 + */ +public interface IKaCheckService { + + + /** + * 新增摄像头信息 + * + * @param cameraInfos 摄像头信息 + * @return 结果 + */ + int insertCameraInfo(List cameraInfos); + + + /** + * 新增点位信息 + * 一个点位对应一个设备 + * + * @param points 点位信息 + * @return 结果 + */ + int insertPoint(List points); + + + + /** + * 新增交通流量信息 + * + * @param trafficFlowInfos 交通流量信息 + * @return 结果 + */ + int insertTrafficFlowInfo(List trafficFlowInfos); + + + /** + * 新增车流量 + * + * @param vehicleFlows 车流量 + * @return 结果 + */ + int insertVehicleFlow(List vehicleFlows); + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/kacheck/impl/KaCheckServiceImpl.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/kacheck/impl/KaCheckServiceImpl.java new file mode 100644 index 0000000..b7ee4a8 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/kacheck/impl/KaCheckServiceImpl.java @@ -0,0 +1,216 @@ +package com.bnhz.adapter.service.kacheck.impl; + +import com.bnhz.adapter.model.kacheck.KaCameraInfo; +import com.bnhz.adapter.model.kacheck.KaPoint; +import com.bnhz.adapter.model.kacheck.KaTrafficFlowInfo; +import com.bnhz.adapter.model.kacheck.KaVehicleFlow; +import com.bnhz.adapter.service.common.CommonService; +import com.bnhz.adapter.service.kacheck.IKaCheckService; +import com.bnhz.adapter.util.ThingsModelUtils; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.reflect.ReflectUtils; +import com.bnhz.iot.domain.Device; +import com.bnhz.iot.domain.Product; +import com.bnhz.iot.domain.ThingsModel; +import com.bnhz.iot.enums.DeviceType; +import com.bnhz.iot.model.ThingsModels.ThingsModelEventVO; +import com.bnhz.iot.service.IDeviceService; +import com.bnhz.iot.service.IProductService; +import com.bnhz.iot.service.IThingsModelService; +import com.bnhz.iot.service.base.SyncDevice; +import com.bnhz.iot.tdengine.service.IColumnModeOperationsService; +import com.bnhz.mq.model.ReportDataBo; +import com.bnhz.mq.service.IDataHandler; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author Leo + * @date 2024/6/28 10:00 + */ +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(rollbackFor = Exception.class) +public class KaCheckServiceImpl implements IKaCheckService, SyncDevice { + + private final IDataHandler dataHandler; + + private final IDeviceService deviceService; + + private final IProductService productService; + + private final IThingsModelService thingsModelService; + + private final IColumnModeOperationsService columnModeOperationsService; + + private final CommonService commonService; + + + + @Override + public int insertPoint(List points) { + //1.初始化设备 + Product product = initProduct(); + initModel(new KaPoint(), product.getProductId(), product.getProductName()); + Device queryDevice = new Device(); + queryDevice.setProductId(product.getProductId()); + Map existDeviceMap = deviceService.selectDeviceList(queryDevice) + .stream() + .collect(Collectors.toMap(Device::getSerialNumber, Function.identity())); + points.forEach(point -> { + Device existDevice = existDeviceMap.get(point.getDwbh()); + Device device = addDevice(point, product, ObjectUtils.isEmpty(existDevice) ? null : existDevice.getDeviceId()); + //2.插入数据属性数据 + ReportDataBo propertyReportDataBo; + try { + propertyReportDataBo = ThingsModelUtils.toPropertyReportDataBo(ReflectUtils.getAllFields(point), product.getProductId(), device.getSerialNumber()); + } catch (IllegalAccessException e) { + log.error("转换错误", e); + throw new RuntimeException(e); + } + dataHandler.reportData(propertyReportDataBo); + }); + return points.size(); + } + + + + + @Override + public int insertCameraInfo(List cameraInfos) { + commonService.saveEvent(cameraInfos, BnhzConstant.KaCheckEvent.SXTXX, DateUtils.getTimestamp()); + return cameraInfos.size(); + } + + + @Override + public int insertTrafficFlowInfo(List trafficFlowInfos) { + commonService.saveEvent(trafficFlowInfos, BnhzConstant.KaCheckEvent.JTLL, DateUtils.getTimestamp()); + return trafficFlowInfos.size(); + } + + @Override + public int insertVehicleFlow(List vehicleFlows) { + commonService.saveEvent(vehicleFlows, BnhzConstant.KaCheckEvent.CLL, DateUtils.getTimestamp()); + return vehicleFlows.size(); + } + + @Override + public void syncDevice() { + //黑烟车属于自动推送,入口为insertPoint + } + + @Override + public String getProductName() { + return "卡口点位设备"; + } + + private Product initProduct() { + Product query = new Product(); + String transport = BnhzConstant.TRANSPORT.HTTP; + query.setTransport(transport); + query.setProductName(getProductName()); + List products = productService.selectProductList(query); + Product product; + if (CollectionUtils.isEmpty(products)) { + //初始化产品 + product = new Product(); + product.setTenantId(1L); + product.setProductName(getProductName()); + product.setTransport(transport); + //默认选择其他 + product.setCategoryId(7L); + product.setCategoryName("其他"); + + //直连设备 + product.setDeviceType(DeviceType.DIRECT_DEVICE.getCode()); + product.setLocationWay(3); + //默认以太网 + product.setNetworkMethod(3); + //默认认证方式HTTP + product.setVertificateMethod(3); + product.setProtocolCode(BnhzConstant.TRANSPORT.HTTP); + product.setIsSys(1); + product.setRemark("系统自动同步"); + productService.insertProduct(product); + } else { + product = products.stream().findFirst().get(); + } + return product; + } + + @SneakyThrows + public void initModel(KaPoint point, Long productId, String productName) { + Map allFields = ReflectUtils.getAllFields(point); + List thingsModelEventVOS = thingsModelService.listEventModeList(productId); + if (CollectionUtils.isEmpty(thingsModelEventVOS)) { + //初始化属性 + List propertyThingsModel = ThingsModelUtils.toPropertyThingsModel(allFields, productId, productName); + propertyThingsModel.forEach(thingsModelService::insertThingsModel); + //初始化事件 + //1.交通流量 + Map trafficFlowInfoFields = ReflectUtils.getAllFields(new KaTrafficFlowInfo()); + ThingsModel trafficFlowInfoModel = ThingsModelUtils.toEventThingsModel(trafficFlowInfoFields, productId, productName,"交通流量", BnhzConstant.KaCheckEvent.JTLL); + thingsModelService.insertThingsModel(trafficFlowInfoModel); + //3.车流量 + Map vehicleFlowFields = ReflectUtils.getAllFields(new KaVehicleFlow()); + ThingsModel vehicleFlowModel = ThingsModelUtils.toEventThingsModel(vehicleFlowFields, productId, productName,"车流量", BnhzConstant.KaCheckEvent.CLL); + thingsModelService.insertThingsModel(vehicleFlowModel); + //4.摄像头信息 + Map cameraInfoFields = ReflectUtils.getAllFields(new KaCameraInfo()); + ThingsModel cameraInfoModel = ThingsModelUtils.toEventThingsModel(cameraInfoFields, productId, productName,"摄像头信息", BnhzConstant.KaCheckEvent.SXTXX); + thingsModelService.insertThingsModel(cameraInfoModel); + propertyThingsModel.add(trafficFlowInfoModel); + propertyThingsModel.add(vehicleFlowModel); + propertyThingsModel.add(cameraInfoModel); + columnModeOperationsService.ddl(productId, propertyThingsModel); + Product productUpdate = new Product(); + productUpdate.setProductId(productId); + productUpdate.setStatus(2); + productService.updateProduct(productUpdate); + } + } + + + private Device addDevice(KaPoint point, Product product, Long deviceId) { + Device device = new Device(); + device.setDeviceId(deviceId); + device.setProductId(product.getProductId()); + device.setProductName(product.getProductName()); + boolean online = "1".equals(point.getDwzt()); + device.setStatus(online ? 3 : 4); + device.setLocationWay(3); + device.setFirmwareVersion(new BigDecimal(1)); + device.setSerialNumber(point.getDwbh()); + device.setDeviceType(DeviceType.DIRECT_DEVICE.getCode()); + device.setIsSimulate(0); + device.setDeviceName(point.getDwmc()); + device.setTenantId(1L); + device.setLongitude(point.getDdjd()); + device.setLatitude(point.getDdwd()); + device.setRemark("系统自动同步"); + if (ObjectUtils.isEmpty(device.getDeviceId())) { + //第一次保存就为激活时间 + device.setActiveTime(new Date()); + deviceService.insertDevice(device); + } else { + deviceService.updateDevice(device); + } + return device; + } + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/VideoMonitorService.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/VideoMonitorService.java new file mode 100644 index 0000000..2f9826e --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/VideoMonitorService.java @@ -0,0 +1,46 @@ +package com.bnhz.adapter.service.video; + +import com.bnhz.common.core.page.PageResult; +import com.bnhz.common.utils.PageUtils; +import com.bnhz.iot.model.ext.video.VideoDeviceInfoVO; +import com.bnhz.iot.service.base.SyncDevice; + +import java.util.List; + +/** + * 高空瞭望 + * + * @author Leo + * @date 2024/6/11 15:16 + */ +public interface VideoMonitorService extends SyncDevice { + + PageResult getVideoDevicePage(Integer pageNo, Integer pageSize); + + String getLiveStream(String channelCode, String protocol); + + void syncLiveStream(); + + @Override + default void syncDevice() { + int pageNo = 1; + boolean next = false; + int pageSize = 100; + do { + PageResult videoDevicePage = getVideoDevicePage(pageNo, pageSize); + next = PageUtils.hasNext(videoDevicePage.getTotal(), pageNo, pageSize); + List data = videoDevicePage.getRows(); + register(data); + pageNo++; + } while (next); + } + + /** + * 登记设备到平台 + * + * @param videoDeviceInfoVOS 第三方监控 + */ + void register(List videoDeviceInfoVOS); + + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/impl/BaseVideoServiceImpl.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/impl/BaseVideoServiceImpl.java new file mode 100644 index 0000000..a1c6def --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/impl/BaseVideoServiceImpl.java @@ -0,0 +1,279 @@ +package com.bnhz.adapter.service.video.impl; + +import com.bnhz.adapter.service.common.CommonService; +import com.bnhz.adapter.service.video.VideoMonitorService; +import com.bnhz.adapter.util.ThingsModelUtils; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.reflect.ReflectUtils; +import com.bnhz.iot.domain.Device; +import com.bnhz.iot.domain.Product; +import com.bnhz.iot.domain.ThingsModel; +import com.bnhz.iot.model.ThingsModels.ThingsModelEventVO; +import com.bnhz.iot.model.ext.video.VideoDeviceInfoVO; +import com.bnhz.iot.model.videoMonitor.*; +import com.bnhz.iot.service.IDeviceService; +import com.bnhz.iot.service.IProductService; +import com.bnhz.iot.service.IThingsModelService; +import com.bnhz.iot.tdengine.service.IColumnModeOperationsService; +import com.bnhz.mq.model.ReportDataBo; +import com.bnhz.mq.service.IDataHandler; +import com.bnhz.sip.domain.SipDevice; +import com.bnhz.sip.domain.SipDeviceChannel; +import com.bnhz.sip.service.ISipDeviceChannelService; +import com.bnhz.sip.service.ISipDeviceService; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author Leo + * @date 2024/6/24 15:43 + */ +@Slf4j +@RequiredArgsConstructor +public abstract class BaseVideoServiceImpl implements VideoMonitorService { + + + private final IProductService productService; + + private final IDeviceService deviceService; + + @Getter + private final IThingsModelService thingsModelService; + + private final IDataHandler dataHandler; + + @Getter + private final IColumnModeOperationsService columnModeOperationsService; + + private final ISipDeviceService sipDeviceService; + + private final ISipDeviceChannelService sipDeviceChannelService; + + @Getter + private final CommonService commonService; + + + + abstract String getTransport(); + + @Override + public void syncLiveStream() { + Product product = initProduct(); + SipDeviceChannel querySipDeviceChannel = new SipDeviceChannel(); + querySipDeviceChannel.setProductId(product.getProductId()); + List sipDeviceChannels = sipDeviceChannelService.selectSipDeviceChannelList(querySipDeviceChannel); + List videoStreams = new ArrayList<>(); + sipDeviceChannels.forEach(item -> { + String hslLiveStream = getLiveStream(item.getChannelSipId(), BnhzConstant.VideoProtocol.HLS); + String rtspLiveStream = getLiveStream(item.getChannelSipId(), BnhzConstant.VideoProtocol.RTSP); + VideoStream videoStream = new VideoStream(); + videoStream.setVideoDeviceCode(item.getDeviceSipId()); + videoStream.setChannelNo(item.getChannelSipId()); + videoStream.setGenTime(LocalDateTime.now()); + videoStream.setHlsUrl(hslLiveStream); + videoStream.setRtspUrl(rtspLiveStream); + videoStreams.add(videoStream); + }); + String eventId = product.getProductName().contains(BnhzConstant.SUFFIX.HIGH_OBS) ? BnhzConstant.VideoEvent.HIGH_OBS_PREVIEW_EVENT : BnhzConstant.VideoEvent.VIDEO_MONITOR_PREVIEW_EVENT; + commonService.saveEvent(videoStreams, eventId, DateUtils.getTimestamp()); + } + + @Override + public void register(List videoDeviceInfoVOS) { + Product product = initProduct(); + initModel(product.getProductId()); + Device queryDevice = new Device(); + queryDevice.setProductId(product.getProductId()); + SipDeviceChannel querySipDeviceChannel = new SipDeviceChannel(); + querySipDeviceChannel.setProductId(product.getProductId()); + Map existChannelMap = sipDeviceChannelService.selectSipDeviceChannelList(querySipDeviceChannel) + .stream() + .collect(Collectors.toMap(SipDeviceChannel::getChannelSipId, Function.identity())); + Map existDeviceMap = deviceService.selectDeviceList(queryDevice) + .stream() + .collect(Collectors.toMap(Device::getSerialNumber, Function.identity())); + videoDeviceInfoVOS + .forEach(videoDeviceInfoVO -> { + Device existDevice = existDeviceMap.get(videoDeviceInfoVO.getVideoDeviceCode()); + //添加设备 + Device device = addDevice(videoDeviceInfoVO, product, ObjectUtils.isEmpty(existDevice) ? null : existDevice.getDeviceId()); + //添加通道 + List channelList = videoDeviceInfoVO.getChannelList(); + addChannel(device, channelList, existChannelMap); + + //2.插入数据属性数据 + ReportDataBo propertyReportDataBo; + try { + propertyReportDataBo = ThingsModelUtils.toPropertyReportDataBo(ReflectUtils.getAllFields(videoDeviceInfoVO.getData()), product.getProductId(), videoDeviceInfoVO.getVideoDeviceCode()); + } catch (IllegalAccessException e) { + log.error("转换错误", e); + throw new RuntimeException(e); + } + dataHandler.reportData(propertyReportDataBo); + }); + + + } + + private void addChannel(Device device, List channelList, Map exitChannelMap) { + //新增Sip设备 + SipDevice sipDevice = new SipDevice(); + sipDevice.setProductId(device.getProductId()); + sipDevice.setDeviceSipId(device.getSerialNumber()); + sipDevice.setDeviceId(device.getDeviceId()); + sipDevice.setDeviceName(device.getDeviceName()); + sipDevice.setTransport(getTransport()); + sipDevice.setRegistertime(new Date()); + sipDeviceService.updateDevice(sipDevice); + + channelList.forEach(channel -> { + SipDeviceChannel sipDeviceChannel = new SipDeviceChannel(); + SipDeviceChannel existChannel = exitChannelMap.get((channel.getChannelNo())); + sipDeviceChannel.setId(ObjectUtils.isEmpty(existChannel) ? null : existChannel.getId()); + sipDeviceChannel.setTenantId(device.getTenantId()); + sipDeviceChannel.setTenantName(device.getTenantName()); + sipDeviceChannel.setProductId(device.getProductId()); + sipDeviceChannel.setProductName(device.getProductName()); + sipDeviceChannel.setDeviceSipId(device.getSerialNumber()); + sipDeviceChannel.setChannelSipId(channel.getChannelNo()); + sipDeviceChannel.setChannelName(channel.getChannelName()); + sipDeviceChannel.setStatus(channel.getOnline() ? 2 : 3); + if (ObjectUtils.isEmpty(sipDeviceChannel.getId())) { + sipDeviceChannelService.insertSipDeviceChannel(sipDeviceChannel); + } else { + sipDeviceChannelService.updateSipDeviceChannel(sipDeviceChannel); + } + + }); + + } + + private Device addDevice(VideoDeviceInfoVO videoDeviceInfoVO, Product product, Long deviceId) { + Device device = new Device(); + device.setDeviceId(deviceId); + device.setProductId(product.getProductId()); + device.setProductName(product.getProductName()); + List channelList = videoDeviceInfoVO.getChannelList(); + boolean online = channelList.stream() + .anyMatch(VideoDeviceInfoVO.Channel::getOnline); + device.setStatus(online ? 3 : 4); + device.setLocationWay(3); + device.setFirmwareVersion(new BigDecimal(1)); + device.setSerialNumber(videoDeviceInfoVO.getVideoDeviceCode()); + device.setDeviceType(1); + device.setIsSimulate(0); + device.setDeviceName(videoDeviceInfoVO.getVideoDeviceName()); + device.setTenantId(1L); + device.setRemark("系统自动同步"); + if (ObjectUtils.isEmpty(device.getDeviceId())) { + //第一次保存就为激活时间 + device.setActiveTime(new Date()); + deviceService.insertDevice(device); + } else { + deviceService.updateDevice(device); + } + return device; + } + + /** + * 初始化产品 + * + * @return 产品 + */ + public Product initProduct() { + Product query = new Product(); + query.setTransport(getTransport()); + query.setProductName(getProductName()); + List products = productService.selectProductList(query); + Product product; + if (CollectionUtils.isEmpty(products)) { + //初始化产品 + product = new Product(); + product.setTenantId(1L); + product.setProductName(getProductName()); + product.setTransport(getTransport()); + //默认选择其他 + product.setCategoryId(7L); + product.setCategoryName("其他"); + + //监控设备 + product.setDeviceType(1); + product.setLocationWay(3); + //默认以太网 + product.setNetworkMethod(3); + //默认认证方式HTTP + product.setVertificateMethod(3); + product.setProtocolCode(BnhzConstant.TRANSPORT.HTTP); + product.setRemark("系统自动同步"); + productService.insertProduct(product); + } else { + product = products.stream().findFirst().get(); + } + return product; + } + + @SneakyThrows + private void initModel(Long productId) { + String productName = getProductName(); + List thingsModelEventVOS = thingsModelService.listEventModeList(productId); + if (!CollectionUtils.isEmpty(thingsModelEventVOS)) { + return; + } + //华智 + Map allFields = new HashMap<>(); + ThingsModel streamModel = new ThingsModel(); + Map streamMap = ReflectUtils.getAllFields(new VideoStream()); + if (productName.contains(BnhzConstant.SUFFIX.HZ)) { + allFields = ReflectUtils.getAllFields(new VideoMonitorPointVO()); + //初始化视频预览流事件 + streamModel = ThingsModelUtils.toEventThingsModel(streamMap, productId, productName, "视频预览流", BnhzConstant.VideoEvent.VIDEO_MONITOR_PREVIEW_EVENT); + + } else if (productName.contains(BnhzConstant.SUFFIX.HIGH_OBS)) { + allFields = ReflectUtils.getAllFields(new CameraInfo()); + //初始化事件 + Map fieldInfoMap = ReflectUtils.getAllFields(new FireAlarmHistory()); + ThingsModel fireInfoModel = ThingsModelUtils.toEventThingsModel(fieldInfoMap, productId, productName, "防火组件历史火情列表", BnhzConstant.VideoEvent.HIGH_OBS_FIRE_EVENT); + thingsModelService.insertThingsModel(fireInfoModel); + columnModeOperationsService.ddl(productId, Collections.singletonList(fireInfoModel)); + + Map judgeMap = ReflectUtils.getAllFields(new FireAlarmHistory()); + ThingsModel judgeModel = ThingsModelUtils.toEventThingsModel(judgeMap, productId, productName, "防火组件预警研判列表", BnhzConstant.VideoEvent.HIGH_OBS_JUDGE_EVENT); + thingsModelService.insertThingsModel(judgeModel); + columnModeOperationsService.ddl(productId, Collections.singletonList(judgeModel)); + + + Map playBackMap = ReflectUtils.getAllFields(new HighObsPlayBack()); + ThingsModel playBackModel = ThingsModelUtils.toEventThingsModel(playBackMap, productId, productName, "视频回放数据", BnhzConstant.VideoEvent.HIGH_OBS_PLAY_BACK_EVENT); + thingsModelService.insertThingsModel(playBackModel); + columnModeOperationsService.ddl(productId, Collections.singletonList(playBackModel)); + + //初始化视频预览流事件 + streamModel = ThingsModelUtils.toEventThingsModel(streamMap, productId, productName, "视频预览流", BnhzConstant.VideoEvent.HIGH_OBS_PREVIEW_EVENT); + + } + List propertyThingsModel = ThingsModelUtils.toPropertyThingsModel(allFields, productId, productName); + propertyThingsModel.forEach(thingsModelService::insertThingsModel); + columnModeOperationsService.ddl(productId, propertyThingsModel); + + thingsModelService.insertThingsModel(streamModel); + columnModeOperationsService.ddl(productId, Collections.singletonList(streamModel)); + + + Product productUpdate = new Product(); + productUpdate.setProductId(productId); + productUpdate.setStatus(2); + productService.updateProduct(productUpdate); + } + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/impl/HighObsServiceImpl.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/impl/HighObsServiceImpl.java new file mode 100644 index 0000000..b12b50d --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/impl/HighObsServiceImpl.java @@ -0,0 +1,381 @@ +package com.bnhz.adapter.service.video.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.bnhz.adapter.service.common.CommonService; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.page.PageResult; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.PageUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.json.JsonUtils; +import com.bnhz.iot.domain.Product; +import com.bnhz.iot.domain.ThingsModel; +import com.bnhz.iot.model.ThingsModels.ThingsModelQuery; +import com.bnhz.iot.model.ext.video.VideoDeviceInfoVO; +import com.bnhz.iot.model.videoMonitor.CameraInfo; +import com.bnhz.iot.model.videoMonitor.FireAlarmHistory; +import com.bnhz.iot.model.videoMonitor.HighObsPlayBack; +import com.bnhz.iot.model.videoMonitor.HighObsResult; +import com.bnhz.iot.model.videoMonitor.res.HighObsPlayBackRes; +import com.bnhz.iot.service.IDeviceService; +import com.bnhz.iot.service.IProductService; +import com.bnhz.iot.service.IThingsModelService; +import com.bnhz.iot.tdengine.service.IColumnModeOperationsService; +import com.bnhz.mq.service.IDataHandler; +import com.bnhz.sip.service.ISipDeviceChannelService; +import com.bnhz.sip.service.ISipDeviceService; +import com.fasterxml.jackson.core.type.TypeReference; +import com.hikvision.artemis.sdk.ArtemisHttpUtil; +import com.hikvision.artemis.sdk.config.ArtemisConfig; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Leo + * @date 2024/6/11 15:17 + */ +@Slf4j +@Service("highObsServiceImpl") +@Transactional(rollbackFor = Exception.class) +public class HighObsServiceImpl extends BaseVideoServiceImpl { + + public HighObsServiceImpl(IProductService productService, IDeviceService deviceService, IThingsModelService thingsModelService, IDataHandler dataHandler, IColumnModeOperationsService columnModeOperationsService, ISipDeviceService sipDeviceService, ISipDeviceChannelService sipDeviceChannelService, CommonService commonService) { + super(productService, deviceService, thingsModelService, dataHandler, columnModeOperationsService, sipDeviceService, sipDeviceChannelService, commonService); + } + + @Value("${high-obs.host}") + private String host; + + @Value("${high-obs.appKey}") + private String appKey; + + @Value("${high-obs.appSecret}") + private String appSecret; + + @Value("${high-obs.picUrlPrefix}") + private String picUrlPrefix; + + @Value("${server.domain}") + private String domain; + + + private static final String PIC_SPLIT_SYMBOL = "@@@@"; + + + private static final String ARTEMIS_PATH = "/artemis"; + + + public String callPostApiGetCameraList(Integer pageNo, Integer pageSize) throws Exception { + + ArtemisConfig config = initConfig(); + final String getCamsApi = ARTEMIS_PATH + "/api/resource/v1/cameras"; + Map paramMap = new HashMap<>();// post请求Form表单参数 + paramMap.put("pageNo", pageNo); + paramMap.put("pageSize", pageSize); + String body = JSON.toJSON(paramMap).toString(); + Map path = new HashMap(2) { + { + put("https://", getCamsApi); + } + }; + return ArtemisHttpUtil.doPostStringArtemis(config, path, body, null, null, "application/json"); + } + + @SneakyThrows + public HighObsResult> getFireAlarmHistories(LocalDateTime beginTime, LocalDateTime endTime, Integer pageNo, Integer pageSize) { + ArtemisConfig config = initConfig(); + final String getCamsApi = ARTEMIS_PATH + "/api/bforestfire/v1/historyAlarms"; + Map paramMap = new HashMap<>();// post请求Form表单参数 + paramMap.put("pageNo", pageNo); + paramMap.put("pageSize", pageSize); + if (!ObjectUtils.isEmpty(beginTime)) { + paramMap.put("beginTime", DateUtils.toISOUtc8Time(beginTime)); + } + if (!ObjectUtils.isEmpty(endTime)) { + paramMap.put("endTime", DateUtils.toISOUtc8Time(endTime)); + } + String body = JSON.toJSON(paramMap).toString(); + Map path = new HashMap(2) { + { + put("https://", getCamsApi); + } + }; + String response = ArtemisHttpUtil.doPostStringArtemis(config, path, body, null, null, "application/json"); + + return toFireAlarmHistory(response); + } + + @SneakyThrows + public HighObsResult> getJudgeList(LocalDateTime beginTime, LocalDateTime endTime, Integer pageNo, Integer pageSize) { + ArtemisConfig config = initConfig(); + final String getCamsApi = ARTEMIS_PATH + "/api/bforestfire/v1/alarms/judge/list"; + Map paramMap = new HashMap<>();// post请求Form表单参数 + paramMap.put("pageNo", pageNo); + paramMap.put("pageSize", pageSize); + if (!ObjectUtils.isEmpty(beginTime)) { + paramMap.put("beginTime", DateUtils.toISOUtc8Time(beginTime)); + } + if (!ObjectUtils.isEmpty(endTime)) { + paramMap.put("endTime", DateUtils.toISOUtc8Time(endTime)); + } + String body = JSON.toJSON(paramMap).toString(); + Map path = new HashMap(2) { + { + put("https://", getCamsApi); + } + }; + String response = ArtemisHttpUtil.doPostStringArtemis(config, path, body, null, null, "application/json"); + + return toFireAlarmHistory(response); + } + + private HighObsResult> toFireAlarmHistory(String json) { + HighObsResult> pageHighObsResult = JsonUtils.parseObject(json, new TypeReference>>() { + }); + HighObsResult.Page data = pageHighObsResult.getData(); + if (ObjectUtils.isNotEmpty(data)) { + data.getList().forEach(fireAlarmHistory -> { + String picUrls = fireAlarmHistory.getPicUrls(); + if (StringUtils.isNotEmpty(picUrls) && picUrls.contains(PIC_SPLIT_SYMBOL)) { + String[] split = picUrls.split(PIC_SPLIT_SYMBOL); + fireAlarmHistory.setVisPicUrl(picUrlPrefix + split[0]); + fireAlarmHistory.setThermalPicUrl(picUrlPrefix + split[1]); + } + }); + } + return pageHighObsResult; + } + + private List getPlayBackEventList(List fireAlarmHistories) { + return fireAlarmHistories.stream() + .filter(fireAlarmHistory -> StringUtils.isNotEmpty(fireAlarmHistory.getId())) + .map(fireAlarmHistory -> { + HighObsPlayBack highObsPlayBack = new HighObsPlayBack(); + highObsPlayBack.setCameraId(fireAlarmHistory.getCameraId()); + highObsPlayBack.setFireAlarmId(fireAlarmHistory.getId()); + Date startDate = fireAlarmHistory.getStartTime(); + LocalDateTime startTime = DateUtils.toLocalDateTime(startDate); + if (ObjectUtils.isNotEmpty(startTime)) { + LocalDateTime endTime = startTime.plusMinutes(1); + HighObsResult playBack = getPlayBack(highObsPlayBack.getCameraId(), startTime, endTime); + HighObsPlayBackRes data = playBack.getData(); + if (ObjectUtils.isNotEmpty(data)) { + highObsPlayBack.setUrl(data.getUrl()); + } + highObsPlayBack.setStartTime(startTime); + highObsPlayBack.setEndTime(endTime); + } + return highObsPlayBack; + }).collect(Collectors.toList()); + } + + @SneakyThrows + public HighObsResult getPlayBack(String cameraId, LocalDateTime beginTime, LocalDateTime endTime) { + ArtemisConfig config = initConfig(); + final String getCamsApi = ARTEMIS_PATH + "/api/video/v2/cameras/playbackURLs"; + Map paramMap = new HashMap<>();// post请求Form表单参数 + paramMap.put("cameraIndexCode", cameraId); + paramMap.put("beginTime", DateUtils.toISOUtc8Time(beginTime)); + paramMap.put("endTime", DateUtils.toISOUtc8Time(endTime)); + paramMap.put("protocol", "ws"); + String body = JSON.toJSON(paramMap).toString(); + Map path = new HashMap(2) { + { + put("https://", getCamsApi); + } + }; + String response = ArtemisHttpUtil.doPostStringArtemis(config, path, body, null, null, "application/json"); + + return JsonUtils.parseObject(response, new TypeReference>() { + }); + } + + private ArtemisConfig initConfig() { + ArtemisConfig config = new ArtemisConfig(); + config.setHost(host); // 代理API网关nginx服务器ip端口 + config.setAppKey(appKey); // 秘钥appkey + config.setAppSecret(appSecret);// 秘钥appSecret + return config; + } + + + public String getPreviewUrls(String cameraIndexCode, String protocol) throws Exception { + + ArtemisConfig config = initConfig(); + final String getCamsApi = ARTEMIS_PATH + "/api/vnsc/mls/v1/preview/openApi/getPreviewParam"; + Map paramMap = new HashMap<>();// post请求Form表单参数 + paramMap.put("indexCode", cameraIndexCode); + paramMap.put("netZoneCode", "2"); + paramMap.put("transmode", 1); + paramMap.put("streamType", 0); + paramMap.put("protocol", protocol); + //一小时有效期 单位秒 + paramMap.put("expireTime", 60 * 60); + String body = JSON.toJSON(paramMap).toString(); + Map path = new HashMap(2) { + { + put("https://", getCamsApi); + } + }; + log.info("[High-OBS-getPreviewUrls-request]====>{}", body); + return ArtemisHttpUtil.doPostStringArtemis(config, path, body, null, null, "application/json"); + } + + + @SneakyThrows + @Override + public PageResult getVideoDevicePage(Integer pageNo, Integer pageSize) { + String response = callPostApiGetCameraList(pageNo, pageSize); + HighObsResult> result = JsonUtils.parseObject(response, new TypeReference>>() { + }); + HighObsResult.Page data = result.getData(); + List collect = data.getList().stream() + .map(cameraInfo -> { + VideoDeviceInfoVO videoDeviceInfoVO = new VideoDeviceInfoVO<>(); + videoDeviceInfoVO.setVideoDeviceCode(cameraInfo.getCameraIndexCode()); + videoDeviceInfoVO.setVideoDeviceName(cameraInfo.getCameraName()); + videoDeviceInfoVO.setLongitude(cameraInfo.getLongitude()); + videoDeviceInfoVO.setLatitude(cameraInfo.getLatitude()); + videoDeviceInfoVO.setData(cameraInfo); + VideoDeviceInfoVO.Channel channel = new VideoDeviceInfoVO.Channel(); + channel.setChannelName(cameraInfo.getChannelTypeName()); + channel.setChannelNo(cameraInfo.getCameraIndexCode()); + channel.setOnline(cameraInfo.getStatus() == 1); + videoDeviceInfoVO.setChannelList(Collections.singletonList(channel)); + return videoDeviceInfoVO; + }).collect(Collectors.toList()); + return new PageResult<>(collect, data.getTotal()); + } + + @SneakyThrows + @Override + public String getLiveStream(String channelCode, String protocol) { + + String previewUrlJson = getPreviewUrls(channelCode, protocol); + JSONObject jsonObject = JSONObject.parseObject(previewUrlJson); + String code = jsonObject.getString("code"); + log.info("[High-OBS-getLiveStream-response]====>{}", previewUrlJson); + if ("0".equals(code)) { + JSONObject data = jsonObject.getJSONObject("data"); + if (ObjectUtils.isNotEmpty(data)) { + return data.getString("url"); + } + } + return null; + } + + public void syncFireHistories() { + + Product product = initProduct(); + ThingsModel modelQuery = new ThingsModel(); + modelQuery.setProductId(product.getProductId()); + modelQuery.setIdentifier(BnhzConstant.VideoEvent.HIGH_OBS_FIRE_EVENT); + ThingsModel eventModel = getThingsModelService().selectSingleThingsModel(modelQuery); + ThingsModelQuery thingsModelQuery = new ThingsModelQuery(); + thingsModelQuery.setType(BnhzConstant.ModelType.EVENT); + thingsModelQuery.setSize(1); + thingsModelQuery.setProductId(product.getProductId()); + thingsModelQuery.setModelId(eventModel.getModelId()); + String confirmTimeField = FireAlarmHistory.Fields.confirmTime.toLowerCase(); + thingsModelQuery.setOrderField(confirmTimeField); + + List> responseList = getColumnModeOperationsService().query(thingsModelQuery); + + //获取最近30天的数据 + LocalDateTime beginTime = LocalDateTime.now().minusDays(30); + + //最新的确认时间 + LocalDateTime confirmTime = null; + + //获取最新的一条数据 + if (!CollectionUtils.isEmpty(responseList)) { + Map next = responseList.iterator().next(); + Object date = next.get(confirmTimeField); + if (date instanceof Date) { + confirmTime = DateUtils.toLocalDateTime((Date) date); + } + } + int pageNo = 1; + int pageSize = 500; + boolean next; + long batchNo = DateUtils.getTimestamp(); + do { + HighObsResult> fireAlarmHistories = getFireAlarmHistories(beginTime, null, pageNo, pageSize); + List list = fireAlarmHistories.getData().getList(); + List newData = new ArrayList<>(); + for (FireAlarmHistory fireAlarmHistory : list) { + LocalDateTime newDate = DateUtils.toLocalDateTime(fireAlarmHistory.getConfirmTime()) + .withNano(0); + if (ObjectUtils.isEmpty(confirmTime) || confirmTime.withNano(0).isBefore(newDate)) { + //兼容中台不能以id做为参数 + fireAlarmHistory.setAlarmId(fireAlarmHistory.getId()); + newData.add(fireAlarmHistory); + } + } + getCommonService().saveEvent(newData, BnhzConstant.VideoEvent.HIGH_OBS_FIRE_EVENT, batchNo); + List playBackEventList = getPlayBackEventList(newData); + getCommonService().saveEvent(playBackEventList, BnhzConstant.VideoEvent.HIGH_OBS_PLAY_BACK_EVENT, batchNo); + next = PageUtils.hasNext(fireAlarmHistories.getData().getTotal(), pageNo, pageSize); + pageNo++; + } while (next); + + + } + + + /** + * 同步预警研判列表 + */ + public void syncJudgeList() { + + Product product = initProduct(); + ThingsModel modelQuery = new ThingsModel(); + modelQuery.setProductId(product.getProductId()); + modelQuery.setIdentifier(BnhzConstant.VideoEvent.HIGH_OBS_JUDGE_EVENT); + ThingsModel eventModel = getThingsModelService().selectSingleThingsModel(modelQuery); + ThingsModelQuery thingsModelQuery = new ThingsModelQuery(); + thingsModelQuery.setType(BnhzConstant.ModelType.EVENT); + thingsModelQuery.setSize(1); + thingsModelQuery.setProductId(product.getProductId()); + thingsModelQuery.setModelId(eventModel.getModelId()); + int pageNo = 1; + int pageSize = 500; + boolean next; + long batchNo = DateUtils.getTimestamp(); + do { + HighObsResult> fireAlarmHistories = getJudgeList(null, null, pageNo, pageSize); + List list = fireAlarmHistories.getData().getList(); + list.forEach(fireAlarmHistory -> //兼容中台不能以id做为参数 + fireAlarmHistory.setAlarmId(fireAlarmHistory.getId())); + getCommonService().saveEvent(list, BnhzConstant.VideoEvent.HIGH_OBS_JUDGE_EVENT, batchNo); + List playBackEventList = getPlayBackEventList(list); + getCommonService().saveEvent(playBackEventList, BnhzConstant.VideoEvent.HIGH_OBS_PLAY_BACK_EVENT, batchNo); + next = PageUtils.hasNext(fireAlarmHistories.getData().getTotal(), pageNo, pageSize); + pageNo++; + } while (next); + + + } + + @Override + String getTransport() { + return BnhzConstant.TRANSPORT.HTTP_HIGH_OBS; + } + + @Override + public String getProductName() { + return "高空瞭望" + BnhzConstant.SUFFIX.HIGH_OBS; + } + + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/impl/HuaZhiVideoMonitorServiceImpl.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/impl/HuaZhiVideoMonitorServiceImpl.java new file mode 100644 index 0000000..b52f686 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/service/video/impl/HuaZhiVideoMonitorServiceImpl.java @@ -0,0 +1,229 @@ +package com.bnhz.adapter.service.video.impl; + +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import com.bnhz.adapter.service.common.CommonService; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.page.PageResult; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.json.JsonUtils; +import com.bnhz.iot.model.ext.video.VideoDeviceInfoVO; +import com.bnhz.iot.model.videoMonitor.VideoMonitorLiveStreamQuery; +import com.bnhz.iot.model.videoMonitor.VideoMonitorPage; +import com.bnhz.iot.model.videoMonitor.VideoMonitorPointVO; +import com.bnhz.iot.model.videoMonitor.VideoMonitorResult; +import com.bnhz.iot.service.IDeviceService; +import com.bnhz.iot.service.IProductService; +import com.bnhz.iot.service.IThingsModelService; +import com.bnhz.iot.tdengine.service.IColumnModeOperationsService; +import com.bnhz.mq.service.IDataHandler; +import com.bnhz.sip.service.ISipDeviceChannelService; +import com.bnhz.sip.service.ISipDeviceService; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static com.bnhz.common.constant.CacheConstants.HUAZHI_VIDEO_TOKEN; + +/** + * @author Leo + * @date 2024/6/7 17:20 + */ +@Slf4j +@Service("huaZhiVideoMonitorServiceImpl") +@Transactional(rollbackFor = Exception.class) +public class HuaZhiVideoMonitorServiceImpl extends BaseVideoServiceImpl { + + public HuaZhiVideoMonitorServiceImpl(IProductService productService, IDeviceService deviceService, IThingsModelService thingsModelService, IDataHandler dataHandler, IColumnModeOperationsService columnModeOperationsService, ISipDeviceService sipDeviceService, ISipDeviceChannelService sipDeviceChannelService, CommonService commonService, RedisCache redisCache) { + super(productService, deviceService, thingsModelService, dataHandler, columnModeOperationsService, sipDeviceService, sipDeviceChannelService, commonService); + this.redisCache = redisCache; + } + + @Value("${hua-zhi.url}") + private String domain; + + @Value("${hua-zhi.client_id}") + private String clientId; + + @Value("${hua-zhi.client_secret}") + private String clientSecret; + + @Value("${server.domain}") + private String serverDomain; + + private final RedisCache redisCache; + + /** + * 获取token请求路径 + */ + private static String SSO_URL = "/sso/oauth2.0/accessToken"; + + /** + * 获取设备列表请求路径 + */ + private static String LIST_WITH_DEVICE_URL = "/api/bss/v1/udm/channel/list-with-device"; + + /** + * 获取视频直播地址 + */ + private static String LIVE_VIDEO_URL = "/api/vms/v2/webuas/live/stream/url"; + + + /** + * 在线状态 + */ + private static String ONLINE_STATE = "1"; + + + @Override + public PageResult getVideoDevicePage(Integer pageNo, Integer pageSize) { + + VideoMonitorResult> videoMonitorPageVideoMonitorResult = listWithDevice(pageNo, pageSize); + VideoMonitorPage videoPage = videoMonitorPageVideoMonitorResult.getData(); + List videoMonitorPointVOList = videoPage.getData(); + Map> collect = videoMonitorPointVOList.stream() + .collect(Collectors.groupingBy(VideoMonitorPointVO::getOwnerApsId)); + List videoDeviceList = collect.entrySet().stream() + .map(entry -> { + VideoDeviceInfoVO videoDeviceInfoVO = new VideoDeviceInfoVO<>(); + videoDeviceInfoVO.setVideoDeviceCode(entry.getKey()); + VideoMonitorPointVO next = entry.getValue().iterator().next(); + videoDeviceInfoVO.setVideoDeviceCode(next.getDevApeId()); + videoDeviceInfoVO.setVideoDeviceName(next.getDevName()); + videoDeviceInfoVO.setLongitude(next.getLongitude()); + videoDeviceInfoVO.setLatitude(next.getLatitude()); + videoDeviceInfoVO.setData(next); + List channels = entry.getValue().stream() + .map(videoMonitorPointVO -> { + VideoDeviceInfoVO.Channel channel = new VideoDeviceInfoVO.Channel(); + channel.setChannelNo(videoMonitorPointVO.getApeId()); + channel.setChannelName(videoMonitorPointVO.getName()); + channel.setOnline(ONLINE_STATE.equals(videoMonitorPointVO.getIsOnline())); + return channel; + }).collect(Collectors.toList()); + videoDeviceInfoVO.setChannelList(channels); + return videoDeviceInfoVO; + }).collect(Collectors.toList()); + + return new PageResult<>(videoDeviceList, videoPage.getPaging().getTotalNum()); + } + + @Override + public String getLiveStream(String channelCode, String protocol) { + VideoMonitorLiveStreamQuery videoMonitorLiveStreamQuery = new VideoMonitorLiveStreamQuery(); + videoMonitorLiveStreamQuery.setChannelCode(channelCode); + videoMonitorLiveStreamQuery.setStreamType(0); + int streamMode = 3; + if (BnhzConstant.VideoProtocol.RTSP.equals(protocol)) { + streamMode = 1; + } + videoMonitorLiveStreamQuery.setStreamMode(streamMode); + + return getLiveStream(videoMonitorLiveStreamQuery); + } + + + public VideoMonitorResult> listWithDevice(Integer pageNo, Integer pageSize) { + Map paramMap = new HashMap<>(); + paramMap.put("page_no", pageNo); + paramMap.put("page_size", pageSize); + HttpResponse response = HttpUtil.createGet(domain + LIST_WITH_DEVICE_URL) + .addHeaders(getHeader()) + .form(paramMap) + .execute(); + String body = response.body(); + VideoMonitorResult> videoMonitorResult = JsonUtils.parseObject(body, new TypeReference>>() { + }); + return videoMonitorResult; + } + + + public String getLiveStream(VideoMonitorLiveStreamQuery videoMonitorLiveStreamQuery) { + Map paramMap = new HashMap<>(); + paramMap.put("channel_code", videoMonitorLiveStreamQuery.getChannelCode()); + paramMap.put("stream_type", videoMonitorLiveStreamQuery.getStreamType()); + paramMap.put("stream_mode", videoMonitorLiveStreamQuery.getStreamMode()); + paramMap.put("visit_ip", StringUtils.pickIp(domain)); + HttpResponse response = HttpUtil.createGet(domain + LIVE_VIDEO_URL) + .addHeaders(getHeader()) + .form(paramMap) + .execute(); + String body = response.body(); + VideoMonitorResult videoMonitorResult = JsonUtils.parseObject(body, VideoMonitorResult.class); + return videoMonitorResult.getData(); + } + + + private Map getHeader() { + + Map headerMap = new HashMap<>(); + headerMap.put("Authorization", getToken()); + headerMap.put("User", "usercode:" + clientId); + headerMap.put("Cookie", "usercode=" + clientId); + headerMap.put("Content-Type", "application/json"); + return headerMap; + } + + private String getToken() { + String token = redisCache.getCacheObject(HUAZHI_VIDEO_TOKEN); + if (StringUtils.isNotEmpty(token)) { + return token; + } + HashMap paramMap = new HashMap<>(); + paramMap.put("grant_type", "client_credentials"); + paramMap.put("client_id", clientId); + paramMap.put("client_secret", clientSecret); + paramMap.put("format", "json"); + String response = HttpUtil.get(domain + SSO_URL, paramMap); + + Token tokenData = JsonUtils.parseObject(response, Token.class); + + if (ObjectUtils.isNotEmpty(tokenData)) { + redisCache.setCacheObject(HUAZHI_VIDEO_TOKEN, tokenData.getAccessToken(), tokenData.getExpiresIn(), TimeUnit.SECONDS); + return tokenData.getAccessToken(); + } else { + log.error("[华智视频监控]获取登陆token失败,返回信息:{}", response); + throw new ServiceException("[华智视频监控]获取登陆token失败"); + } + + } + + @Override + String getTransport() { + return BnhzConstant.TRANSPORT.HTTP_HZ; + } + + @Override + public String getProductName() { + return "华智监控" +BnhzConstant.SUFFIX.HZ; + } + + @Data + public static class Token { + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("tokenType") + private String token_type; + + @JsonProperty("expires_in") + private Integer expiresIn; + + @JsonProperty("refresh_token") + private String refreshToken; + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/task/SyncFumesTask.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/task/SyncFumesTask.java new file mode 100644 index 0000000..4fa8ac4 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/task/SyncFumesTask.java @@ -0,0 +1,92 @@ +package com.bnhz.adapter.task; + +import com.bnhz.adapter.service.fumes.IFumesService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Leo + * @date 2024/7/2 11:50 + */ +@Profile("!test") +@Slf4j +@Component("SyncFumes") +@RequiredArgsConstructor +@RestController +@RequestMapping("/sync/fumes") +public class SyncFumesTask { + + private final IFumesService fumesService; + + @PutMapping("/device") + public void syncDevice() { + log.info("=============================Timer-餐饮油烟同步[设备]开始============================="); + fumesService.syncDevice(); + log.info("=============================Timer-餐饮油烟同步[设备]完成============================="); + } + + @PutMapping("/alarm") + public void synAlarmMsg() { + log.info("=============================Timer-餐饮油烟同步[报警信息]开始============================="); + fumesService.syncAlarmMsg(); + log.info("=============================Timer-餐饮油烟同步[报警信息]完成============================="); + } + + /** + * 报警监测(一天一次) + */ + @PutMapping("/detector") + public void syncDetector() { + log.info("=============================Timer-餐饮油烟同步[报警监测]开始============================="); + fumesService.syncDetector(); + log.info("=============================Timer-餐饮油烟同步[报警监测]完成============================="); + } + + /** + * 获取十分钟数据(十分钟执行一次) + */ + @PutMapping("/tenData") + public void syncTenData() { + log.info("=============================Timer-餐饮油烟同步[十分钟数据]开始============================="); + fumesService.syncTenData(); + log.info("=============================Timer-餐饮油烟同步[十分钟数据]完成============================="); + } + + /** + * 五分钟执行一次 + */ + @PutMapping("/oneData") + public void syncOneData() { + log.info("=============================Timer-餐饮油烟同步[一分钟数据]开始============================="); + fumesService.syncOneData(); + log.info("=============================Timer-餐饮油烟同步[一分钟数据]完成============================="); + + } + + @PutMapping("/pointEvent") + public void syncPointEvent() { + log.info("=============================Timer-餐饮油烟同步[点位事件]开始============================="); + fumesService.syncPointEvent(); + log.info("=============================Timer-餐饮油烟同步[点位事件]结束============================="); + } + + @PutMapping("/reduceEvent") + public void syncReduceEvent() { + log.info("=============================Timer-餐饮油烟同步[减排统计]开始============================="); + fumesService.syncReduce(); + log.info("=============================Timer-餐饮油烟同步[减排统计]结束============================="); + } + @PutMapping("/realTimeData") + public void syncRealData() { + log.info("=============================Timer-餐饮油烟同步[实时数据]开始============================="); + fumesService.syncRealTimeData(); + log.info("=============================Timer-餐饮油烟同步[实时数据]结束============================="); + } + + +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/task/SyncVideoDeviceTask.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/task/SyncVideoDeviceTask.java new file mode 100644 index 0000000..a32be8a --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/task/SyncVideoDeviceTask.java @@ -0,0 +1,104 @@ +package com.bnhz.adapter.task; + +import com.bnhz.adapter.service.video.impl.BaseVideoServiceImpl; +import com.bnhz.adapter.service.video.impl.HighObsServiceImpl; +import com.bnhz.framework.util.RedissonLockUtil; +import com.bnhz.iot.service.base.SyncDevice; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +/** + * 系统业务数据初始接口 + * + * @author Leo + * @date 2024/6/25 09:56 + */ +@Profile("!test") +@Slf4j +@Component("SyncVideoDevice") +@RequiredArgsConstructor +public class SyncVideoDeviceTask { + + private final BaseVideoServiceImpl huaZhiVideoMonitorServiceImpl; + + private final HighObsServiceImpl highObsServiceImpl; + + private final RedissonLockUtil redissonLockUtil; + + + public void syncHuaZhiDevice() { + log.info("=============================Timer-Start华智视频同步设备完成============================="); + boolean lock = redissonLockUtil.tryLock("syncHuaZhiDevice", 60L); + if (lock) { + huaZhiVideoMonitorServiceImpl.syncDevice(); + } else { + log.error("华智视频同步设备-已有任务在进行"); + } + log.info("=============================Timer-End华智视频同步设备完成============================="); + } + + + public void syncHuZhiStream() { + log.info("=============================Timer-Start华智视频同步视频流完成============================="); + boolean lock = redissonLockUtil.tryLock("syncHuZhiStream", 60L); + if (lock) { + huaZhiVideoMonitorServiceImpl.syncLiveStream(); + } else { + log.error("华智视频同步视频流-已有任务在进行"); + } + + log.info("=============================Timer-End华智视频同步视频流完成============================="); + } + + + public void syncHighObsDevice() { + log.info("=============================Timer-Start高空瞭望同步设备完成============================="); + boolean lock = redissonLockUtil.tryLock("syncHighObsDevice", 60L); + if (lock) { + highObsServiceImpl.syncDevice(); + } else { + log.error("高空瞭望同步设备-已有任务在进行"); + } + + log.info("=============================Timer-End高空瞭望同步设备完成============================="); + } + + public void syncHighObsStream() { + log.info("=============================Timer-Start高空瞭望同步视频流完成============================="); + boolean lock = redissonLockUtil.tryLock("syncHighObsStream", 60L); + if (lock) { + highObsServiceImpl.syncLiveStream(); + } else { + log.error("高空瞭望同步视频流-已有任务在进行"); + } + + + log.info("=============================Timer-End高空瞭望同步视频流完成============================="); + + } + + public void syncHigObsFireHistories() { + log.info("=============================Timer-Start高空瞭望同步历史火情列表============================="); + boolean lock = redissonLockUtil.tryLock("syncHigObsFireHistories", 60L); + if (lock) { + highObsServiceImpl.syncFireHistories(); + } else { + log.error("高空瞭望同步历史火情列表-已有任务在进行"); + } + log.info("=============================Timer-End高空瞭望同步历史火情列表============================="); + } + + public void syncHigObsJudge() { + log.info("=============================Timer-Start高空瞭望同步研判列表============================="); + boolean lock = redissonLockUtil.tryLock("syncHigObsJudge", 60L); + if (lock){ + highObsServiceImpl.syncJudgeList(); + }else { + log.error("高空瞭望同步研判列表-已有任务在进行"); + } + log.info("=============================Timer-End 高空瞭望同步研判列表============================="); + + } +} diff --git a/bnhz-adapter/src/main/java/com/bnhz/adapter/util/ThingsModelUtils.java b/bnhz-adapter/src/main/java/com/bnhz/adapter/util/ThingsModelUtils.java new file mode 100644 index 0000000..4fb06a8 --- /dev/null +++ b/bnhz-adapter/src/main/java/com/bnhz/adapter/util/ThingsModelUtils.java @@ -0,0 +1,225 @@ +package com.bnhz.adapter.util; + +import com.alibaba.fastjson.JSONObject; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.thingsModel.ThingsModelSimpleItem; +import com.bnhz.common.core.thingsModel.ThingsModelValuesInput; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.reflect.ReflectUtils; +import com.bnhz.iot.domain.ThingsModel; +import com.bnhz.iot.model.ThingsModelItem.Datatype; +import com.bnhz.mq.model.ReportDataBo; +import org.springframework.util.ObjectUtils; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Leo + * @date 2024/6/28 11:33 + */ +public class ThingsModelUtils { + + + public static List toPropertyThingsModel(Map allFields, Long productId, String productName) { + + //移除基础字段 + allFields.remove("id"); + allFields.remove("createTime"); + allFields.remove("updateTime"); + + return allFields.entrySet().stream() + .filter(entry -> { + ReflectUtils.FieldInfo value = entry.getValue(); + //只处理有备注的的属性 + return !ObjectUtils.isEmpty(value.getComment()); + }) + .map(entry -> { + ThingsModel thingsModel = new ThingsModel(); + thingsModel.setIdentifier(entry.getKey()); + ReflectUtils.FieldInfo value = entry.getValue(); + String name = value.getType().getName(); + + thingsModel.setDatatype(toDataType(name)); + thingsModel.setProductId(productId); + thingsModel.setProductName(productName); + thingsModel.setType(BnhzConstant.ModelType.PROPERTY); + thingsModel.setModelName(value.getComment()); + thingsModel.setIsChart(0); + thingsModel.setIsMonitor(0); + thingsModel.setIsHistory(1); + thingsModel.setIsReadonly(0); + thingsModel.setSpecs(toSpec(thingsModel.getDatatype(), value.getLength())); + thingsModel.setTenantId(1L); + thingsModel.setTenantName("System"); + return thingsModel; + }).collect(Collectors.toList()); + + } + + public static ThingsModel toEventThingsModel(Map allFields, Long productId, String productName, String modelName, String identifier) { + ThingsModel thingsModel = new ThingsModel(); + thingsModel.setIdentifier(identifier); + thingsModel.setModelName(modelName); + thingsModel.setProductId(productId); + thingsModel.setProductName(productName); + thingsModel.setType(BnhzConstant.ModelType.EVENT); + thingsModel.setIsChart(0); + thingsModel.setIsMonitor(0); + thingsModel.setIsHistory(1); + thingsModel.setIsReadonly(0); + thingsModel.setTenantId(1L); + thingsModel.setDatatype(BnhzConstant.DataType.TYPE_OBJECT); + List thingsModels = allFields.entrySet() + .stream() + .filter(entry -> { + ReflectUtils.FieldInfo value = entry.getValue(); + //只处理有备注的的属性 + return !ObjectUtils.isEmpty(value.getComment()); + }) + .map(entry -> { + com.bnhz.iot.model.ThingsModelItem.ThingsModel thingsModelInner = new com.bnhz.iot.model.ThingsModelItem.ThingsModel(); + thingsModelInner.setId(identifier + "_" + entry.getKey()); + ReflectUtils.FieldInfo value = entry.getValue(); + String name = value.getType().getName(); + String dataType = toDataType(name); + thingsModelInner.setDatatype(genDatatype(dataType, value.getLength())); + thingsModelInner.setType(BnhzConstant.ModelType.PROPERTY); + thingsModelInner.setName(value.getComment()); + thingsModelInner.setIsChart(0); + thingsModelInner.setIsMonitor(0); + thingsModelInner.setIsHistory(1); + thingsModelInner.setIsReadonly(0); + return thingsModelInner; + }).collect(Collectors.toList()); + Datatype datatype = new Datatype(); + datatype.setType("object"); + datatype.setParams(thingsModels); + thingsModel.setSpecs(JSONObject.toJSONString(datatype)); + return thingsModel; + } + + public static ReportDataBo toPropertyReportDataBo(Map allFields, Long productId, String serialNumber) { + ReportDataBo reportDataBo = new ReportDataBo(); + reportDataBo.setProductId(productId); + reportDataBo.setSerialNumber(serialNumber); + reportDataBo.setType(BnhzConstant.ModelType.PROPERTY); + List thingsModelSimpleItemList = toThingsModelSimpleItemList(allFields); + reportDataBo.setDataList(thingsModelSimpleItemList); + ThingsModelValuesInput thingsModelValuesInput = new ThingsModelValuesInput(); + thingsModelValuesInput.setDataTime(LocalDateTime.now()); + thingsModelValuesInput.setType(BnhzConstant.ModelType.PROPERTY); + thingsModelValuesInput.setThingsModelValueRemarkItem(thingsModelSimpleItemList); + reportDataBo.setValuesInput(thingsModelValuesInput); + reportDataBo.setRuleEngine(true); + return reportDataBo; + } + + public static ReportDataBo toEventReportDataBo(Map allFields + , Long productId + , String serialNumber + , Long modelId + , LocalDateTime dateTime + , Long deviceId + , String topic + , Long batchNo) { + ReportDataBo reportDataBo = new ReportDataBo(); + reportDataBo.setProductId(productId); + reportDataBo.setSerialNumber(serialNumber); + reportDataBo.setType(BnhzConstant.ModelType.EVENT); + List thingsModelSimpleItemList = toThingsModelSimpleItemList(allFields); + reportDataBo.setDataList(thingsModelSimpleItemList); + //reportDataBo.setMessage(JSONObject.toJSONString(thingsModelSimpleItemList)); + ThingsModelValuesInput thingsModelValuesInput = new ThingsModelValuesInput(); + thingsModelValuesInput.setModelId(modelId); + thingsModelValuesInput.setProductId(productId); + thingsModelValuesInput.setDeviceId(deviceId); + thingsModelValuesInput.setDeviceNumber(serialNumber); + if (ObjectUtils.isEmpty(dateTime)) { + thingsModelValuesInput.setDataTime(LocalDateTime.now()); + } else { + thingsModelValuesInput.setDataTime(dateTime); + } + + thingsModelValuesInput.setType(BnhzConstant.ModelType.EVENT); + thingsModelValuesInput.setThingsModelValueRemarkItem(thingsModelSimpleItemList); + thingsModelValuesInput.setTopic(topic); + thingsModelValuesInput.setBatchNo(batchNo); + reportDataBo.setValuesInput(thingsModelValuesInput); + reportDataBo.setRuleEngine(true); + return reportDataBo; + } + + private static List toThingsModelSimpleItemList(Map allFields) { + Date now = new Date(); + return allFields.entrySet() + .stream() + .filter(entry -> { + ReflectUtils.FieldInfo value = entry.getValue(); + //只处理有备注的的属性 + return !ObjectUtils.isEmpty(value.getComment()); + }) + .map(entry -> { + ThingsModelSimpleItem thingsModelSimpleItem = new ThingsModelSimpleItem(); + thingsModelSimpleItem.setId(entry.getKey()); + Object value = entry.getValue().getValue(); + if (value instanceof Date) { + thingsModelSimpleItem.setValue(DateUtils.dateTimeYYYYMDHMS((Date) value)); + } else if (value instanceof LocalDateTime) { + thingsModelSimpleItem.setValue(DateUtils.toDateTimeStr((LocalDateTime) value)); + } else { + thingsModelSimpleItem.setValue(ObjectUtils.isEmpty(value) ? null : String.valueOf(value)); + } + + thingsModelSimpleItem.setTs(now); + return thingsModelSimpleItem; + }).collect(Collectors.toList()); + } + + private static String toDataType(String typeName) { + switch (typeName) { + case "java.lang.String": + return "string"; + case "java.math.BigDecimal": + return "decimal"; + case "java.lang.Integer": + case "int": + return "integer"; + case "java.util.Date": + return "date"; + case "boolean": + return "bool"; + } + return "string"; + } + + private static String toSpec(String dataTypeStr, int length) { + return JSONObject.toJSONString(genDatatype(dataTypeStr, length)); + + } + + + private static Datatype genDatatype(String dataTypeStr, int length) { + Datatype dataType = new Datatype(); + dataType.setType(dataTypeStr); + if ("integer".equals(dataTypeStr) || "decimal".equals(dataTypeStr)) { + dataType.setMin(new BigDecimal(1)); + dataType.setMax(new BigDecimal(1000)); + } + if ("string".equals(dataTypeStr)) { + if (length > 0) { + dataType.setMaxLength(length); + } else { + dataType.setMaxLength(60); + } + + } + return dataType; + } + + +} diff --git a/bnhz-admin/pom.xml b/bnhz-admin/pom.xml new file mode 100644 index 0000000..59d3a6c --- /dev/null +++ b/bnhz-admin/pom.xml @@ -0,0 +1,161 @@ + + + + daqi-back + com.bnhz + 3.8.5 + + 4.0.0 + jar + bnhz-admin + + + web服务入口 + + + + + + + org.springframework.boot + spring-boot-devtools + true + + + + + io.springfox + springfox-boot-starter + + + + + io.swagger + swagger-models + 1.6.2 + + + + + mysql + mysql-connector-java + + + + + com.bnhz + bnhz-framework + + + + + com.bnhz + bnhz-quartz + + + + + com.bnhz + bnhz-generator + + + + com.bnhz + bnhz-oss + + + + + com.bnhz + bnhz-open-api + + + + com.bnhz + boot-strap + + + + com.bnhz + gateway-boot + + + + com.bnhz + sip-server + + + + com.bnhz + bnhz-3rd-api + + + + com.yomahub + liteflow-core + 2.11.3 + + + + + com.bnhz + bnhz-notify-web + + + + com.bnhz + bnhz-notify-core + + + + + com.bnhz + bnhz-oauth + + + + com.bnhz + bnhz-adapter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.1.1.RELEASE + + true + + + + + repackage + + + + + + org.apache.maven.plugins + maven-war-plugin + 3.1.0 + + false + ${project.artifactId} + + + + ${project.artifactId} + + + diff --git a/bnhz-admin/src/main/java/com/bnhz/DaQiApplication.java b/bnhz-admin/src/main/java/com/bnhz/DaQiApplication.java new file mode 100644 index 0000000..c99cfde --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/DaQiApplication.java @@ -0,0 +1,23 @@ +package com.bnhz; + +import com.dtflys.forest.springboot.annotation.ForestScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.kafka.annotation.EnableKafka; + +/** + * 启动程序 + * + * @author ruoyi + */ +@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) +@ForestScan(basePackages = "com.bnhz.api.client") +@EnableKafka +public class DaQiApplication +{ + public static void main(String[] args) + { + SpringApplication.run(DaQiApplication.class, args); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/DaQiServletInitializer.java b/bnhz-admin/src/main/java/com/bnhz/DaQiServletInitializer.java new file mode 100644 index 0000000..9ca076f --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/DaQiServletInitializer.java @@ -0,0 +1,18 @@ +package com.bnhz; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +/** + * web容器中进行部署 + * + * @author ruoyi + */ +public class DaQiServletInitializer extends SpringBootServletInitializer +{ + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) + { + return application.sources(DaQiApplication.class); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/test/kafka/KafkaConsumer.java b/bnhz-admin/src/main/java/com/bnhz/test/kafka/KafkaConsumer.java new file mode 100644 index 0000000..fdc7e38 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/test/kafka/KafkaConsumer.java @@ -0,0 +1,19 @@ +package com.bnhz.test.kafka; + +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.KafkaListener; + +/** + * @author Leo + * @date 2024/7/19 18:01 + */ +@Configuration +public class KafkaConsumer { + + // 指定要监听的 topic + //@KafkaListener(topics = "DEVICE-REPORT-TOPIC") + public void consumeTopic(String msg) { + // 参数: 从topic中收到的 value值 + System.out.println("收到的信息: " + msg); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/test/kafka/ProducerController.java b/bnhz-admin/src/main/java/com/bnhz/test/kafka/ProducerController.java new file mode 100644 index 0000000..be2e576 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/test/kafka/ProducerController.java @@ -0,0 +1,39 @@ +package com.bnhz.test.kafka; + +import com.bnhz.adapter.model.blackcar.Point; +import com.bnhz.common.annotation.Anonymous; +import org.springframework.kafka.core.KafkaTemplate; +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.annotation.Resource; +import java.math.BigDecimal; +import java.util.Date; + +/** + * @author Leo + * @date 2024/7/19 18:03 + */ +@RestController +@RequestMapping("/producer") +public class ProducerController { + + @Resource + private KafkaTemplate kafka; + + @PostMapping + @Anonymous + public String data(@RequestBody String msg) { + + Point point = new Point(); + point.setBisId(123L); + point.setDdjd(new BigDecimal("10.982")); + point.setYxrq(new Date()); + + // 通过Kafka发出数据 + kafka.send("test", point); + return "ok"; + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/test/test/MyClientHandler.java b/bnhz-admin/src/main/java/com/bnhz/test/test/MyClientHandler.java new file mode 100644 index 0000000..d3d85b6 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/test/test/MyClientHandler.java @@ -0,0 +1,41 @@ +package com.bnhz.test.test; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.ReferenceCountUtil; + +/** + * @author Leo + * @date 2024/6/13 17:40 + */ + +public class MyClientHandler extends ChannelInboundHandlerAdapter { + public static String message; + + public MyClientHandler() { + } + + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + try { + ByteBuf byteBuf = (ByteBuf)msg; + byte[] bytes = new byte[byteBuf.readableBytes()]; + byteBuf.readBytes(bytes); + message = new String(bytes); + MyClientNetty.countDownLatch.countDown(); + } finally { + ReferenceCountUtil.release(msg); + } + + } + + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + System.err.println("客户端读取数据完毕"); + ctx.close(); + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + System.err.println("client 读取数据出现异常"); + ctx.close(); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/test/test/MyClientNetty.java b/bnhz-admin/src/main/java/com/bnhz/test/test/MyClientNetty.java new file mode 100644 index 0000000..5e1a182 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/test/test/MyClientNetty.java @@ -0,0 +1,100 @@ +package com.bnhz.test.test; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; + +import java.io.UnsupportedEncodingException; +import java.util.concurrent.CountDownLatch; + +public class MyClientNetty { + public static CountDownLatch countDownLatch = new CountDownLatch(1); + public static CountDownLatch countDownLatch2 = new CountDownLatch(1); + private String ip; + private int port; + private static ChannelFuture cf; + private static EventLoopGroup bossGroup; + + public MyClientNetty(String ip, int port) { + this.ip = ip; + this.port = port; + } + + public String sendRecv(String msg) { + try { + cf.channel().writeAndFlush(Unpooled.copiedBuffer(msg.getBytes())); + MyClientNetty.countDownLatch.await(); + return MyClientHandler.message; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public void connect() throws UnsupportedEncodingException, InterruptedException { + this.action(); + countDownLatch2.await(); + } + + public void close() throws InterruptedException { + cf.channel().closeFuture().sync(); + bossGroup.shutdownGracefully(); + } + + public void action() throws InterruptedException, UnsupportedEncodingException { + bossGroup = new NioEventLoopGroup(); + final Bootstrap bs = new Bootstrap(); + + ((Bootstrap) ((Bootstrap) ((Bootstrap) ((Bootstrap) bs.group(bossGroup)).channel(NioSocketChannel.class)).option(ChannelOption.SO_KEEPALIVE, true)).option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(65535))).handler(new ChannelInitializer() { + protected void initChannel(SocketChannel socketChannel) throws Exception { + socketChannel.pipeline().addLast(new ChannelHandler[]{new MyClientHandler()}); + } + }); + + (new Thread(new Runnable() { + public void run() { + try { + MyClientNetty.cf = bs.connect(MyClientNetty.this.ip, MyClientNetty.this.port).sync(); + MyClientNetty.countDownLatch2.countDown(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + } + })).start(); + } + + public static void main(String[] args) { + + try { +// MyClientNetty myClientNetty = new MyClientNetty("182.148.53.138",3312); +// MyClientNetty myClientNetty = new MyClientNetty("127.0.0.1", 28888); + MyClientNetty myClientNetty = new MyClientNetty("183.223.252.35", 28888); + myClientNetty.connect(); + + //空气微站 +// String result = myClientNetty.sendRecv("##1103QN=20240709100001;ST=22;CN=2031;PW=22;MN=1440-0028-sclw-7280;Flag=5;CP=&&DataTime=20240822120000;LA-Min=0.0,LA-Avg=0.0,LA-Max=0.0,LA-Flag=N;a34002-Min=6.872,a34002-Avg=9.011,a34002-Max=66.66,a34002-Flag=N;a34004-Min=3.464,a34004-Avg=4.605,a34004-Max=6.678,a34004-Flag=N;a01007-Min=0.0,a01007-Avg=0.0,a01007-Max=0.0,a01007-Flag=N;a01008-Min=0.0,a01008-Avg=0.0,a01008-Max=0.0,a01008-Flag=N;a01006-Min=0.000,a01006-Avg=0.000,a01006-Max=0.000,a01006-Flag=N;a01001-Min=0.00,a01001-Avg=0.00,a01001-Max=0.00,a01001-Flag=N;a01002-Min=0.0,a01002-Avg=0.0,a01002-Max=0.0,a01002-Flag=N;a21026-Min=3.858,a21026-Avg=4.450,a21026-Max=4.976,a21026-Flag=N;a21004-Min=8.9989,a21004-Avg=7.842,a21004-Max=10.079,a21004-Flag=N;a21005-Min=0.293,a21005-Avg=0.377,a21005-Max=0.454,a21005-Flag=N;a21006-Min=34.468,a21006-Avg=42.000,a21006-Max=50.231,a21006-Flag=N;a31040-Min=0.000,a31040-Avg=0.000,a31040-Max=0.000,a31040-Flag=N;a19104-Min=0.000,a19104-Avg=0.000,a19104-Max=0.000,a19104-Flag=N;a19105-Min=0.000,a19105-Avg=0.000,a19105-Max=0.000,a19105-Flag=N;a19106-Min=0.000,a19106-Avg=0.000,a19106-Max=0.000,a19106-Flag=N&&81C1"); + //TVOC + //String result = myClientNetty.sendRecv("##0948QN=20240826000010189;ST=22;CN=2031;PW=123456;MN=ZHKCEAMS2408060002;Flag=5;CP=&&DataTime=20240825000000;a34004-Avg=33.496,a34004-orignalRtd=0.000,a34004-Flag=N;a34002-Avg=35.499,a34002-orignalRtd=0.000,a34002-Flag=N;a21026-Avg=0.088,a21026-orignalRtd=21.973,a21026-Flag=N;a21005-Avg=0.000,a21005-orignalRtd=66.229,a21005-Flag=N;a21004-Avg=47.552,a21004-orignalRtd=51.611,a21004-Flag=N;a05024-Avg=138.814,a05024-orignalRtd=71.681,a05024-Flag=N;a24088-Avg=215.130,a24088-orignalRtd=78.448,a24088-Flag=N;a01001-Avg=1.000,a01001-orignalRtd=1.000,a01001-Flag=N;a21003-Avg=30.012,a21003-orignalRtd=0.000,a21003-Flag=N;a21002-Avg=29.969,a21002-orignalRtd=0.000,a21002-Flag=N;a01001-Avg=33.671,a01001-orignalRtd=0.000,a01001-Flag=D;a01002-Avg=54.172,a01002-orignalRtd=0.000,a01002-Flag=D;a01006-Avg=94.197,a01006-orignalRtd=0.000,a01006-Flag=D;a01007-Avg=1.350,a01007-orignalRtd=0.000,a01007-Flag=D;a01008-Avg=176.111,a01008-orignalRtd=0.000,a01008-Flag=D&&06C0"); + + //VOCs + String result = myClientNetty.sendRecv("##1305QN=20240904154505299;ST=31;CN=2031;PW=123456;MN=12345678901234hl9999;Flag=5;CP=&&DataTime=20240901154400;a19001-Min=19.78,a19001-Max=19.8,a19001-Avg=19.784,a19001-Flag=N;a01012-Min=93,a01012-Max=93.47,a01012-Avg=93.168,a01012-Flag=N;a01013-Min=-0.112,a01013-Max=-0.104,a01013-Avg=-0.108,a01013-Flag=N;a01011-Min=17.58,a01011-Max=18.38,a01011-Avg=18.059,a01011-Flag=N;a01014-Min=0.66,a01014-Max=0.67,a01014-Avg=0.669,a01014-Flag=N;a00000-Min=13,a00000-Max=13.6,a00000-Avg=13.362,a00000-Cou=801.7,a00000-Flag=N;a05002-Min=0.735,a05002-Max=0.735,a05002-Avg=0.735,a05002-Cou=0.001,a05002-Flag=N;a24087-Min=74.05,a24087-Max=74.06,a24087-Avg=74.059,a24087-Cou=0.059,a24087-Flag=N;a24088-Min=73.31,a24088-Max=73.32,a24088-Avg=73.319,a24088-Cou=0.059,a24088-Flag=N;a25002-Min=8.76,a25002-Max=8.76,a25002-Avg=8.76,a25002-Cou=0.007,a25002-Flag=N;a25003-Min=1.16,a25003-Max=1.16,a25003-Avg=1.16,a25003-Cou=0.001,a25003-Flag=N;a25005-Min=0.16,a25005-Max=0.16,a25005-Avg=0.16,a25005-Cou=0,a25005-Flag=N;a01001-Min=36.3,a01001-Max=36.4,a01001-Avg=36.317,a01001-Flag=N;a01002-Min=45.6,a01002-Max=46.3,a01002-Avg=45.867,a01002-Flag=N;a01007-Min=0.9,a01007-Max=1.7,a01007-Avg=1.433,a01007-Flag=N;a01006-Min=93.98,a01006-Max=93.99,a01006-Avg=93.983,a01006-Flag=N;a01008-Min=189,a01008-Max=224,a01008-Avg=209.167,a01008-Flag=N&&3C00\n"); +// String result = myClientNetty.sendRecv("7e81ZHKCEAMS24080500017e"); + System.out.println("返回信息:" + result); + + myClientNetty.close(); + + + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + } +} + + diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/common/CaptchaController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/common/CaptchaController.java new file mode 100644 index 0000000..d8741c0 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/common/CaptchaController.java @@ -0,0 +1,99 @@ +package com.bnhz.web.controller.common; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import javax.annotation.Resource; +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletResponse; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.FastByteArrayOutputStream; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import com.google.code.kaptcha.Producer; +import com.bnhz.common.config.DaQiConfig; +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.utils.sign.Base64; +import com.bnhz.common.utils.uuid.IdUtils; +import com.bnhz.system.service.ISysConfigService; + +/** + * 验证码操作处理 + * + * @author ruoyi + */ +@Api(tags = "验证码操作") +@RestController +public class CaptchaController +{ + @Resource(name = "captchaProducer") + private Producer captchaProducer; + + @Resource(name = "captchaProducerMath") + private Producer captchaProducerMath; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysConfigService configService; + /** + * 生成验证码 + */ + @ApiOperation("获取验证码") + @GetMapping("/captchaImage") + public AjaxResult getCode(HttpServletResponse response) throws IOException + { + AjaxResult ajax = AjaxResult.success(); + boolean captchaEnabled = configService.selectCaptchaEnabled(); + ajax.put("captchaEnabled", captchaEnabled); + if (!captchaEnabled) + { + return ajax; + } + + // 保存验证码信息 + String uuid = IdUtils.simpleUUID(); + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid; + + String capStr = null, code = null; + BufferedImage image = null; + + // 生成验证码 + String captchaType = DaQiConfig.getCaptchaType(); + if ("math".equals(captchaType)) + { + String capText = captchaProducerMath.createText(); + capStr = capText.substring(0, capText.lastIndexOf("@")); + code = capText.substring(capText.lastIndexOf("@") + 1); + image = captchaProducerMath.createImage(capStr); + } + else if ("char".equals(captchaType)) + { + capStr = code = captchaProducer.createText(); + image = captchaProducer.createImage(capStr); + } + + redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); + // 转换流信息写出 + FastByteArrayOutputStream os = new FastByteArrayOutputStream(); + try + { + ImageIO.write(image, "jpg", os); + } + catch (IOException e) + { + return AjaxResult.error(e.getMessage()); + } + + ajax.put("uuid", uuid); + ajax.put("img", Base64.encode(os.toByteArray())); + return ajax; + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/common/CommonController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/common/CommonController.java new file mode 100644 index 0000000..a395727 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/common/CommonController.java @@ -0,0 +1,171 @@ +package com.bnhz.web.controller.common; + +import java.util.ArrayList; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.bnhz.common.config.DaQiConfig; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.file.FileUploadUtils; +import com.bnhz.common.utils.file.FileUtils; +import com.bnhz.framework.config.ServerConfig; + +/** + * 通用请求处理 + * + * @author ruoyi + */ +@Api(tags = "通用请求处理") +@RestController +@RequestMapping("/common") +public class CommonController +{ + private static final Logger log = LoggerFactory.getLogger(CommonController.class); + + @Autowired + private ServerConfig serverConfig; + + private static final String FILE_DELIMETER = ","; + + /** + * 通用下载请求 + * + * @param fileName 文件名称 + * @param delete 是否删除 + */ + @ApiOperation("文件下载") + @GetMapping("/download") + public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) + { + try + { + if (!FileUtils.checkAllowDownload(fileName)) + { + throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName)); + } + String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); + String filePath = DaQiConfig.getDownloadPath() + fileName; + + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, realFileName); + FileUtils.writeBytes(filePath, response.getOutputStream()); + if (delete) + { + FileUtils.deleteFile(filePath); + } + } + catch (Exception e) + { + log.error("下载文件失败", e); + } + } + + /** + * 通用上传请求(单个) + */ + @ApiOperation("单个文件上传") + @PostMapping("/upload") + public AjaxResult uploadFile(MultipartFile file) throws Exception + { + try + { + // 上传文件路径 + String filePath = DaQiConfig.getUploadPath(); + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + AjaxResult ajax = AjaxResult.success(); + ajax.put("url", url); + ajax.put("fileName", fileName); + ajax.put("newFileName", FileUtils.getName(fileName)); + ajax.put("originalFilename", file.getOriginalFilename()); + return ajax; + } + catch (Exception e) + { + return AjaxResult.error(e.getMessage()); + } + } + + /** + * 通用上传请求(多个) + */ + @ApiOperation("多个文件上传") + @PostMapping("/uploads") + public AjaxResult uploadFiles(List files) throws Exception + { + try + { + // 上传文件路径 + String filePath = DaQiConfig.getUploadPath(); + List urls = new ArrayList(); + List fileNames = new ArrayList(); + List newFileNames = new ArrayList(); + List originalFilenames = new ArrayList(); + for (MultipartFile file : files) + { + // 上传并返回新文件名称 + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + urls.add(url); + fileNames.add(fileName); + newFileNames.add(FileUtils.getName(fileName)); + originalFilenames.add(file.getOriginalFilename()); + } + AjaxResult ajax = AjaxResult.success(); + ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER)); + ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER)); + ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER)); + ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER)); + return ajax; + } + catch (Exception e) + { + return AjaxResult.error(e.getMessage()); + } + } + + /** + * 本地资源通用下载 + */ + @GetMapping("/download/resource") + @ApiOperation("本地资源通用下载") + public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response) + throws Exception + { + try + { + if (!FileUtils.checkAllowDownload(resource)) + { + throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource)); + } + // 本地资源路径 + String localPath = DaQiConfig.getProfile(); + // 数据库资源地址 + String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); + // 下载名称 + String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, downloadName); + FileUtils.writeBytes(downloadPath, response.getOutputStream()); + } + catch (Exception e) + { + log.error("下载文件失败", e); + } + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/CacheController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/CacheController.java new file mode 100644 index 0000000..d2737a6 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/CacheController.java @@ -0,0 +1,131 @@ +package com.bnhz.web.controller.monitor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.system.domain.SysCache; + +/** + * 缓存监控 + * + * @author ruoyi + */ +@Api(tags = "缓存监控") +@RestController +@RequestMapping("/monitor/cache") +public class CacheController +{ + @Autowired + private RedisTemplate redisTemplate; + + private final static List caches = new ArrayList(); + { + caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息")); + caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息")); + caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典")); + caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码")); + caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交")); + caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理")); + caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数")); + } + + @ApiOperation("获取缓存信息") + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping() + public AjaxResult getInfo() throws Exception + { + Properties info = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info()); + Properties commandStats = (Properties) redisTemplate.execute((RedisCallback) connection -> connection.info("commandstats")); + Object dbSize = redisTemplate.execute((RedisCallback) connection -> connection.dbSize()); + + Map result = new HashMap<>(3); + result.put("info", info); + result.put("dbSize", dbSize); + + List> pieList = new ArrayList<>(); + commandStats.stringPropertyNames().forEach(key -> { + Map data = new HashMap<>(2); + String property = commandStats.getProperty(key); + data.put("name", StringUtils.removeStart(key, "cmdstat_")); + data.put("value", StringUtils.substringBetween(property, "calls=", ",usec")); + pieList.add(data); + }); + result.put("commandStats", pieList); + return AjaxResult.success(result); + } + + @ApiOperation("缓存列表") + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getNames") + public AjaxResult cache() + { + return AjaxResult.success(caches); + } + + @ApiOperation("键名列表") + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getKeys/{cacheName}") + public AjaxResult getCacheKeys(@PathVariable String cacheName) + { + Set cacheKeys = redisTemplate.keys(cacheName + "*"); + return AjaxResult.success(cacheKeys); + } + + @ApiOperation("缓存内容") + @PreAuthorize("@ss.hasPermi('monitor:cache:list')") + @GetMapping("/getValue/{cacheName}/{cacheKey}") + public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey) + { + String cacheValue = redisTemplate.opsForValue().get(cacheKey); + SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue); + return AjaxResult.success(sysCache); + } + + @ApiOperation("清理缓存名称") + @PreAuthorize("@ss.hasPermi('monitor:cache:remove')") + @DeleteMapping("/clearCacheName/{cacheName}") + public AjaxResult clearCacheName(@PathVariable String cacheName) + { + Collection cacheKeys = redisTemplate.keys(cacheName + "*"); + redisTemplate.delete(cacheKeys); + return AjaxResult.success(); + } + + @ApiOperation("清理缓存键名") + @PreAuthorize("@ss.hasPermi('monitor:cache:remove')") + @DeleteMapping("/clearCacheKey/{cacheKey}") + public AjaxResult clearCacheKey(@PathVariable String cacheKey) + { + redisTemplate.delete(cacheKey); + return AjaxResult.success(); + } + + @ApiOperation("清理所有缓存内容") + @PreAuthorize("@ss.hasPermi('monitor:cache:remove')") + @DeleteMapping("/clearCacheAll") + public AjaxResult clearCacheAll() + { + Collection cacheKeys = redisTemplate.keys("*"); + redisTemplate.delete(cacheKeys); + return AjaxResult.success(); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/ServerController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/ServerController.java new file mode 100644 index 0000000..5453def --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/ServerController.java @@ -0,0 +1,31 @@ +package com.bnhz.web.controller.monitor; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.framework.web.domain.Server; + +/** + * 服务器监控 + * + * @author ruoyi + */ +@Api(tags = "服务器监控") +@RestController +@RequestMapping("/monitor/server") +public class ServerController +{ + @ApiOperation("获取服务器信息") + @PreAuthorize("@ss.hasPermi('monitor:server:list')") + @GetMapping() + public AjaxResult getInfo() throws Exception + { + Server server = new Server(); + server.copyTo(); + return AjaxResult.success(server); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/SysLogininforController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/SysLogininforController.java new file mode 100644 index 0000000..5d3e49b --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/SysLogininforController.java @@ -0,0 +1,91 @@ +package com.bnhz.web.controller.monitor; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.framework.web.service.SysPasswordService; +import com.bnhz.system.domain.SysLogininfor; +import com.bnhz.system.service.ISysLogininforService; + +/** + * 系统访问记录 + * + * @author ruoyi + */ +@Api(tags = "日志管理:登录日志") +@RestController +@RequestMapping("/monitor/logininfor") +public class SysLogininforController extends BaseController +{ + @Autowired + private ISysLogininforService logininforService; + + @Autowired + private SysPasswordService passwordService; + + @ApiOperation("获取列表登录信息") + @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')") + @GetMapping("/list") + public TableDataInfo list(SysLogininfor logininfor) + { + startPage(); + List list = logininforService.selectLogininforList(logininfor); + return getDataTable(list); + } + + @ApiOperation("导出登录日志列表") + @Log(title = "登录日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysLogininfor logininfor) + { + List list = logininforService.selectLogininforList(logininfor); + ExcelUtil util = new ExcelUtil(SysLogininfor.class); + util.exportExcel(response, list, "登录日志"); + } + + @ApiOperation("批量删除登录日志") + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{infoIds}") + public AjaxResult remove(@PathVariable Long[] infoIds) + { + return toAjax(logininforService.deleteLogininforByIds(infoIds)); + } + + @ApiOperation("清空登录日志信息") + @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')") + @Log(title = "登录日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public AjaxResult clean() + { + logininforService.cleanLogininfor(); + return success(); + } + + @ApiOperation("账户解锁") + @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')") + @Log(title = "账户解锁", businessType = BusinessType.OTHER) + @GetMapping("/unlock/{userName}") + public AjaxResult unlock(@PathVariable("userName") String userName) + { + passwordService.clearLoginRecordCache(userName); + return success(); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/SysOperlogController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/SysOperlogController.java new file mode 100644 index 0000000..486fb78 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/SysOperlogController.java @@ -0,0 +1,77 @@ +package com.bnhz.web.controller.monitor; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.system.domain.SysOperLog; +import com.bnhz.system.service.ISysOperLogService; + +/** + * 操作日志记录 + * + * @author ruoyi + */ +@Api(tags = "日志管理:操作日志") +@RestController +@RequestMapping("/monitor/operlog") +public class SysOperlogController extends BaseController +{ + @Autowired + private ISysOperLogService operLogService; + + @ApiOperation("获取操作日志列表") + @PreAuthorize("@ss.hasPermi('monitor:operlog:list')") + @GetMapping("/list") + public TableDataInfo list(SysOperLog operLog) + { + startPage(); + List list = operLogService.selectOperLogList(operLog); + return getDataTable(list); + } + + @ApiOperation("导出操作日志") + @Log(title = "操作日志", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('monitor:operlog:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysOperLog operLog) + { + List list = operLogService.selectOperLogList(operLog); + ExcelUtil util = new ExcelUtil(SysOperLog.class); + util.exportExcel(response, list, "操作日志"); + } + + @ApiOperation("批量删除操作日志") + @Log(title = "操作日志", businessType = BusinessType.DELETE) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/{operIds}") + public AjaxResult remove(@PathVariable Long[] operIds) + { + return toAjax(operLogService.deleteOperLogByIds(operIds)); + } + + @ApiOperation("清空操作日志") + @Log(title = "操作日志", businessType = BusinessType.CLEAN) + @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')") + @DeleteMapping("/clean") + public AjaxResult clean() + { + operLogService.cleanOperLog(); + return success(); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/SysUserOnlineController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/SysUserOnlineController.java new file mode 100644 index 0000000..a52c9e9 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/monitor/SysUserOnlineController.java @@ -0,0 +1,98 @@ +package com.bnhz.web.controller.monitor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.system.domain.SysUserOnline; +import com.bnhz.system.service.ISysUserOnlineService; + +/** + * 在线用户监控 + * + * @author ruoyi + */ +@Api(tags = "在线用户监控") +@RestController +@RequestMapping("/monitor/online") +public class SysUserOnlineController extends BaseController +{ + @Autowired + private ISysUserOnlineService userOnlineService; + + @Autowired + private RedisCache redisCache; + + @ApiOperation("获取在线用户列表") + @PreAuthorize("@ss.hasPermi('monitor:online:list')") + @GetMapping("/list") + public TableDataInfo list(String ipaddr, String userName) + { + Collection keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*"); + List userOnlineList = new ArrayList(); + for (String key : keys) + { + LoginUser user = redisCache.getCacheObject(key); + if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName)) + { + if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername())) + { + userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user)); + } + } + else if (StringUtils.isNotEmpty(ipaddr)) + { + if (StringUtils.equals(ipaddr, user.getIpaddr())) + { + userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user)); + } + } + else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser())) + { + if (StringUtils.equals(userName, user.getUsername())) + { + userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user)); + } + } + else + { + userOnlineList.add(userOnlineService.loginUserToUserOnline(user)); + } + } + Collections.reverse(userOnlineList); + userOnlineList.removeAll(Collections.singleton(null)); + return getDataTable(userOnlineList); + } + + /** + * 强退用户 + */ + @ApiOperation("强制退出在线用户") + @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')") + @Log(title = "在线用户", businessType = BusinessType.FORCE) + @DeleteMapping("/{tokenId}") + public AjaxResult forceLogout(@PathVariable String tokenId) + { + redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId); + return success(); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysConfigController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysConfigController.java new file mode 100644 index 0000000..9ec26be --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysConfigController.java @@ -0,0 +1,146 @@ +package com.bnhz.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.constant.UserConstants; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.system.domain.SysConfig; +import com.bnhz.system.service.ISysConfigService; + +/** + * 参数配置 信息操作处理 + * + * @author ruoyi + */ +@Api(tags = "参数设置") +@RestController +@RequestMapping("/system/config") +public class SysConfigController extends BaseController +{ + @Autowired + private ISysConfigService configService; + + /** + * 获取参数配置列表 + */ + @ApiOperation("获取参数配置列表") + @PreAuthorize("@ss.hasPermi('system:config:list')") + @GetMapping("/list") + public TableDataInfo list(SysConfig config) + { + startPage(); + List list = configService.selectConfigList(config); + return getDataTable(list); + } + + @ApiOperation("导出参数配置列表") + @Log(title = "参数管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:config:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysConfig config) + { + List list = configService.selectConfigList(config); + ExcelUtil util = new ExcelUtil(SysConfig.class); + util.exportExcel(response, list, "参数数据"); + } + + /** + * 根据参数编号获取详细信息 + */ + @ApiOperation("根据参数编号获取详细信息") + @PreAuthorize("@ss.hasPermi('system:config:query')") + @GetMapping(value = "/{configId}") + public AjaxResult getInfo(@PathVariable Long configId) + { + return success(configService.selectConfigById(configId)); + } + + /** + * 根据参数键名查询参数值 + */ + @ApiOperation("根据参数键名查询参数值") + @GetMapping(value = "/configKey/{configKey}") + public AjaxResult getConfigKey(@PathVariable String configKey) + { + return success(configService.selectConfigByKey(configKey)); + } + + /** + * 新增参数配置 + */ + @ApiOperation("新增参数配置") + @PreAuthorize("@ss.hasPermi('system:config:add')") + @Log(title = "参数管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysConfig config) + { + if (UserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config))) + { + return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setCreateBy(getUsername()); + return toAjax(configService.insertConfig(config)); + } + + /** + * 修改参数配置 + */ + @ApiOperation("修改参数配置") + @PreAuthorize("@ss.hasPermi('system:config:edit')") + @Log(title = "参数管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysConfig config) + { + if (UserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config))) + { + return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在"); + } + config.setUpdateBy(getUsername()); + return toAjax(configService.updateConfig(config)); + } + + /** + * 删除参数配置 + */ + @ApiOperation("批量删除参数配置") + @PreAuthorize("@ss.hasPermi('system:config:remove')") + @Log(title = "参数管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{configIds}") + public AjaxResult remove(@PathVariable Long[] configIds) + { + configService.deleteConfigByIds(configIds); + return success(); + } + + /** + * 刷新参数缓存 + */ + @ApiOperation("刷新参数缓存") + @PreAuthorize("@ss.hasPermi('system:config:refresh')") + @Log(title = "参数管理", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + configService.resetConfigCache(); + return success(); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysDeptController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysDeptController.java new file mode 100644 index 0000000..539eb85 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysDeptController.java @@ -0,0 +1,286 @@ +package com.bnhz.web.controller.system; + +import cn.hutool.core.util.ObjectUtil; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.constant.UserConstants; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.*; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.iot.mapper.DeviceMapper; +import com.bnhz.iot.model.RegisterUserInput; +import com.bnhz.iot.model.RegisterUserOutput; +import com.bnhz.iot.service.IToolService; +import com.bnhz.system.domain.vo.SysDeptTypeVO; +import com.bnhz.system.mapper.SysRoleDeptMapper; +import com.bnhz.system.service.*; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 机构信息 + * + * @author ruoyi + */ +@Api(tags = "机构管理") +@RestController +@RequestMapping("/system/dept") +public class SysDeptController extends BaseController +{ + @Autowired + private ISysDeptService deptService; + + @Resource + private IToolService toolService; + + @Resource + private ISysUserService sysUserService; + + @Resource + private SysRoleDeptMapper sysRoleDeptMapper; + + @Resource + private ISysDictDataService sysDictDataService; + + @Resource + private ISysRoleService sysRoleService; + + @Resource + private ISysMenuService sysMenuService; + + @Resource + private DeviceMapper deviceMapper; + + /** + * 获取机构列表 + */ + @ApiOperation("获取机构列表") + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list") + public AjaxResult list(SysDept dept) + { + List depts = deptService.selectDeptList(dept); + return success(depts); + } + + /** + * 查询机构列表(排除节点) + */ + @ApiOperation("查询机构列表(排除节点)") + @PreAuthorize("@ss.hasPermi('system:dept:list')") + @GetMapping("/list/exclude/{deptId}") + public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) + { + List depts = deptService.selectDeptList(new SysDept()); + depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + "")); + return success(depts); + } + + /** + * 根据机构编号获取详细信息 + */ + @ApiOperation("根据机构编号获取详细信息") + @PreAuthorize("@ss.hasPermi('system:dept:query')") + @Transactional(rollbackFor = Exception.class) + @GetMapping(value = "/{deptId}") + public AjaxResult getInfo(@PathVariable Long deptId) + { + deptService.checkDeptDataScope(deptId); + SysDept sysDept = deptService.selectDeptById(deptId); + if (null != sysDept && null != sysDept.getDeptUserId()) { + SysUser sysUser = sysUserService.selectUserById(sysDept.getDeptUserId()); + sysDept.setUserName(sysUser.getUserName()); + sysDept.setPhone(sysUser.getPhonenumber()); + } + return success(sysDept); + } + + /** + * 新增机构 + */ + @ApiOperation("新增机构") + @PreAuthorize("@ss.hasPermi('system:dept:add')") + @Log(title = "机构管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDept dept) + { + if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) + { + return error("新增机构'" + dept.getDeptName() + "'失败,机构名称已存在"); + } + dept.setCreateBy(getUsername()); + // 校验系统账号信息 + if (StringUtils.isNotEmpty(dept.getUserName())) { + SysUser sysUser = sysUserService.selectUserByUserName(dept.getUserName()); + if (ObjectUtil.isNotNull(sysUser)) { + throw new ServiceException("系统账号名称已存在,请修改后重试"); + } + if (!dept.getPassword().equals(dept.getConfirmPassword())) { + throw new ServiceException("两次密码不一致,请重新输入"); + } + } + int result = deptService.insertDept(dept); + // 新增机构关联系统账号 + if (result > 0) { + // 添加管理员角色,给所有权限 + // 查询所有权限 +// List sysMenuList = sysMenuService.selectMenuList(new SysMenu(), 1L); + SysDept sysDept = deptService.selectDeptById(dept.getParentId()); + List sysMenuList = sysMenuService.selectMenuList(new SysMenu(), sysDept.getDeptUserId()); + Long[] menuIdList = sysMenuList.stream().map(SysMenu::getMenuId).toArray(Long[]::new); + SysRole sysRole = new SysRole(); + sysRole.setRoleName("管理员"); + sysRole.setRoleKey("manager"); + sysRole.setRoleSort(1); + sysRole.setStatus("0"); + sysRole.setDeptId(dept.getDeptId()); + sysRole.setMenuIds(menuIdList); + sysRoleService.insertRole(sysRole); + + // 注册机构管理员用户 + RegisterUserInput registerUserInput = new RegisterUserInput(); + registerUserInput.setUsername(dept.getUserName()); + registerUserInput.setPassword(dept.getPassword()); + registerUserInput.setPhonenumber(dept.getPhone()); + registerUserInput.setDeptId(dept.getDeptId()); + registerUserInput.setRoleIds(new Long[]{sysRole.getRoleId()}); + RegisterUserOutput registerUserOutput = toolService.registerNoCaptcha(registerUserInput); + if (StringUtils.isNotEmpty(registerUserOutput.getMsg())) { + deptService.deleteDeptById(dept.getDeptId()); + sysRoleService.deleteRoleById(sysRole.getRoleId()); + return AjaxResult.error(registerUserOutput.getMsg()); + } + // 更新机构管理员角色绑定信息 + deptService.updateDeptUserId(dept.getDeptId(), registerUserOutput.getSysUserId()); + } + return toAjax(result); + } + + /** + * 修改机构 + */ + @ApiOperation("修改机构") + @PreAuthorize("@ss.hasPermi('system:dept:edit')") + @Log(title = "机构管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDept dept) + { + Long deptId = dept.getDeptId(); + deptService.checkDeptDataScope(deptId); + if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) + { + return error("修改机构'" + dept.getDeptName() + "'失败,机构名称已存在"); + } + else if (dept.getParentId().equals(deptId)) + { + return error("修改机构'" + dept.getDeptName() + "'失败,上级机构不能是自己"); + } + else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0) + { + return error("该机构包含未停用的子机构!"); + } + dept.setUpdateBy(getUsername()); + return toAjax(deptService.updateDept(dept)); + } + + /** + * 删除机构 + */ + @ApiOperation("根据机构编号删除机构") + @PreAuthorize("@ss.hasPermi('system:dept:remove')") + @Log(title = "机构管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{deptId}") + public AjaxResult remove(@PathVariable Long deptId) + { + if (deptService.hasChildByDeptId(deptId)) + { + return warn("存在下级机构,不允许删除"); + } +// if (deptService.checkDeptExistUser(deptId)) +// { +// return warn("机构存在用户,不允许删除"); +// } + // 有设备不允许删除 + SysDept sysDept = deptService.selectDeptById(deptId); + int deviceCount = deviceMapper.countByTenantId(sysDept.getDeptUserId()); + if (deviceCount > 0) { + return warn("请先把该机构设备回收或删除后重试!"); + } + deptService.checkDeptDataScope(deptId); + // 删除机构绑定角色和用户 + List roleIdList = sysRoleDeptMapper.selectByDeptId(deptId); + if (!org.springframework.util.CollectionUtils.isEmpty(roleIdList)) { + sysRoleService.deleteRoleByIds(roleIdList.toArray(new Long[roleIdList.size()])); + sysUserService.deleteUserByDeptID(deptId); + } + return toAjax(deptService.deleteDeptById(deptId)); + } + + /** + * 获取机构类型 + * @param deptType 父级类型 + * @return com.bnhz.common.core.domain.AjaxResult + */ + @GetMapping("/getDeptType") + public AjaxResult getDeptType(Integer deptType, Boolean showOwner) { + SysDictData sysDictData = new SysDictData(); + sysDictData.setDictType("department_type"); + List sysDictDataList = sysDictDataService.selectDictDataList(sysDictData); + if (CollectionUtils.isEmpty(sysDictDataList)) { + return success(); + } + List result = new ArrayList<>(); + for (SysDictData dictData : sysDictDataList) { + SysDeptTypeVO sysDeptTypeVO = new SysDeptTypeVO(); + sysDeptTypeVO.setDeptType(Integer.valueOf(dictData.getDictValue())); + sysDeptTypeVO.setDeptTypeName(dictData.getDictLabel()); + sysDeptTypeVO.setAncestors(dictData.getRemark()); + result.add(sysDeptTypeVO); + } + if (null == deptType) { + return success(result); + } + SysDeptTypeVO sysDeptTypeVO = result.stream().filter(d -> deptType.equals(d.getDeptType())).findFirst().orElse(null); + if (ObjectUtil.isNull(sysDeptTypeVO)) { + return success(new ArrayList<>()); + } + String ancestors = sysDeptTypeVO.getAncestors(); + result = result.stream().filter(d -> ancestors.contains(d.getDeptType().toString())).collect(Collectors.toList()); + if (showOwner) { + List newResult = new ArrayList<>(); + newResult.add(sysDeptTypeVO); + newResult.addAll(result); + return success(newResult); + } + return success(result); + } + + /** + * 获取机构角色 + * @param deptId 机构id + * @return com.bnhz.common.core.domain.AjaxResult + */ + @GetMapping("/getRole") + public AjaxResult getRole(Long deptId) { + AjaxResult success = AjaxResult.success(); + List sysRoleList = deptService.getRole(deptId); + success.put("roles", sysRoleList); + success.put("roleIds", sysRoleList.stream().map(SysRole::getRoleId).collect(Collectors.toList())); + return success; + } + +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysDictDataController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysDictDataController.java new file mode 100644 index 0000000..f18aac9 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysDictDataController.java @@ -0,0 +1,121 @@ +package com.bnhz.web.controller.system; + +import java.util.ArrayList; +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysDictData; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.system.service.ISysDictDataService; +import com.bnhz.system.service.ISysDictTypeService; + +/** + * 数据字典信息 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/system/dict/data") +public class SysDictDataController extends BaseController +{ + @Autowired + private ISysDictDataService dictDataService; + + @Autowired + private ISysDictTypeService dictTypeService; + + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictData dictData) + { + startPage(); + List list = dictDataService.selectDictDataList(dictData); + return getDataTable(list); + } + + @Log(title = "字典数据", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictData dictData) + { + List list = dictDataService.selectDictDataList(dictData); + ExcelUtil util = new ExcelUtil(SysDictData.class); + util.exportExcel(response, list, "字典数据"); + } + + /** + * 查询字典数据详细 + */ + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictCode}") + public AjaxResult getInfo(@PathVariable Long dictCode) + { + return success(dictDataService.selectDictDataById(dictCode)); + } + + /** + * 根据字典类型查询字典数据信息 + */ + @GetMapping(value = "/type/{dictType}") + public AjaxResult dictType(@PathVariable String dictType) + { + List data = dictTypeService.selectDictDataByType(dictType); + if (StringUtils.isNull(data)) + { + data = new ArrayList(); + } + return success(data); + } + + /** + * 新增字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典数据", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictData dict) + { + dict.setCreateBy(getUsername()); + return toAjax(dictDataService.insertDictData(dict)); + } + + /** + * 修改保存字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典数据", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictData dict) + { + dict.setUpdateBy(getUsername()); + return toAjax(dictDataService.updateDictData(dict)); + } + + /** + * 删除字典类型 + */ + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictCodes}") + public AjaxResult remove(@PathVariable Long[] dictCodes) + { + dictDataService.deleteDictDataByIds(dictCodes); + return success(); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysDictTypeController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysDictTypeController.java new file mode 100644 index 0000000..c80edff --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysDictTypeController.java @@ -0,0 +1,144 @@ +package com.bnhz.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.constant.UserConstants; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysDictType; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.system.service.ISysDictTypeService; + +/** + * 数据字典信息 + * + * @author ruoyi + */ +@Api(tags = "字典管理") +@RestController +@RequestMapping("/system/dict/type") +public class SysDictTypeController extends BaseController +{ + @Autowired + private ISysDictTypeService dictTypeService; + + @ApiOperation("获取字典分页列表") + @PreAuthorize("@ss.hasPermi('system:dict:list')") + @GetMapping("/list") + public TableDataInfo list(SysDictType dictType) + { + startPage(); + List list = dictTypeService.selectDictTypeList(dictType); + return getDataTable(list); + } + + @ApiOperation("导出字典列表") + @Log(title = "字典类型", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:dict:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysDictType dictType) + { + List list = dictTypeService.selectDictTypeList(dictType); + ExcelUtil util = new ExcelUtil(SysDictType.class); + util.exportExcel(response, list, "字典类型"); + } + + /** + * 查询字典类型详细 + */ + @ApiOperation("查询字典类型详细") + @PreAuthorize("@ss.hasPermi('system:dict:query')") + @GetMapping(value = "/{dictId}") + public AjaxResult getInfo(@PathVariable Long dictId) + { + return success(dictTypeService.selectDictTypeById(dictId)); + } + + /** + * 新增字典类型 + */ + @ApiOperation("新增字典类型") + @PreAuthorize("@ss.hasPermi('system:dict:add')") + @Log(title = "字典类型", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysDictType dict) + { + if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) + { + return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setCreateBy(getUsername()); + return toAjax(dictTypeService.insertDictType(dict)); + } + + /** + * 修改字典类型 + */ + @ApiOperation("新增字典类型") + @PreAuthorize("@ss.hasPermi('system:dict:edit')") + @Log(title = "字典类型", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysDictType dict) + { + if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) + { + return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在"); + } + dict.setUpdateBy(getUsername()); + return toAjax(dictTypeService.updateDictType(dict)); + } + + /** + * 删除字典类型 + */ + @ApiOperation("删除字典类型") + @PreAuthorize("@ss.hasPermi('system:dict:remove')") + @Log(title = "字典类型", businessType = BusinessType.DELETE) + @DeleteMapping("/{dictIds}") + public AjaxResult remove(@PathVariable Long[] dictIds) + { + dictTypeService.deleteDictTypeByIds(dictIds); + return success(); + } + + /** + * 刷新字典缓存 + */ + @ApiOperation("刷新字典缓存") + @PreAuthorize("@ss.hasPermi('system:dict:refresh')") + @Log(title = "字典类型", businessType = BusinessType.CLEAN) + @DeleteMapping("/refreshCache") + public AjaxResult refreshCache() + { + dictTypeService.resetDictCache(); + return success(); + } + + /** + * 获取字典选择框列表 + */ + @ApiOperation("获取字典选择框列表") + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List dictTypes = dictTypeService.selectDictTypeAll(); + return success(dictTypes); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysIndexController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysIndexController.java new file mode 100644 index 0000000..8606622 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysIndexController.java @@ -0,0 +1,29 @@ +package com.bnhz.web.controller.system; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.config.DaQiConfig; +import com.bnhz.common.utils.StringUtils; + +/** + * 首页 + * + * @author ruoyi + */ +@RestController +public class SysIndexController +{ + /** 系统基础配置 */ + @Autowired + private DaQiConfig daQiConfig; + + /** + * 访问首页,提示语 + */ + @RequestMapping("/") + public String index() + { + return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", daQiConfig.getName(), daQiConfig.getVersion()); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysLoginController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysLoginController.java new file mode 100644 index 0000000..eb8efe3 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysLoginController.java @@ -0,0 +1,108 @@ +package com.bnhz.web.controller.system; + +import java.util.List; +import java.util.Set; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysMenu; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.domain.model.LoginBody; +import com.bnhz.common.utils.SecurityUtils; +import com.bnhz.framework.web.service.SysLoginService; +import com.bnhz.framework.web.service.SysPermissionService; +import com.bnhz.system.service.ISysMenuService; + +/** + * 登录验证 + * + * @author ruoyi + */ +@Api(tags = "登录验证") +@RestController +public class SysLoginController +{ + @Autowired + private SysLoginService loginService; + + @Autowired + private ISysMenuService menuService; + + @Autowired + private SysPermissionService permissionService; + @Value("${server.broker.enabled}") + private Boolean enabled; + + /** + * 登录方法 + * + * @param loginBody 登录信息 + * @return 结果 + */ + @ApiOperation("用户登录") + @PostMapping("/login") + public AjaxResult login(@RequestBody LoginBody loginBody) + { + AjaxResult ajax = AjaxResult.success(); + // 生成令牌 + String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), + loginBody.getUuid(), loginBody.getSourceType()); + ajax.put(Constants.TOKEN, token); + return ajax; + } + @ApiOperation("卡口用户登录") + @PostMapping("/KC/login") + public AjaxResult kaCheckUserLogin(@RequestBody LoginBody loginBody) + { + AjaxResult ajax = AjaxResult.success(); + // 生成令牌 + String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), + loginBody.getUuid(), loginBody.getSourceType()); + ajax.put(Constants.TOKEN, token); + return ajax; + } + + /** + * 获取用户信息 + * + * @return 用户信息 + */ + @ApiOperation("获取用户信息") + @GetMapping("getInfo") + public AjaxResult getInfo() + { + SysUser user = SecurityUtils.getLoginUser().getUser(); + // 角色集合 + Set roles = permissionService.getRolePermission(user); + // 权限集合 + Set permissions = permissionService.getMenuPermission(user); + AjaxResult ajax = AjaxResult.success(); + ajax.put("user", user); + ajax.put("roles", roles); + ajax.put("permissions", permissions); + ajax.put("mqtt",enabled); + return ajax; + } + + /** + * 获取路由信息 + * + * @return 路由信息 + */ + @ApiOperation("获取路由信息") + @GetMapping("getRouters") + public AjaxResult getRouters() + { + Long userId = SecurityUtils.getUserId(); + List menus = menuService.selectMenuTreeByUserId(userId); + return AjaxResult.success(menuService.buildMenus(menus)); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysMenuController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysMenuController.java new file mode 100644 index 0000000..ea418a2 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysMenuController.java @@ -0,0 +1,158 @@ +package com.bnhz.web.controller.system; + +import java.util.List; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.constant.UserConstants; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysMenu; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.system.service.ISysMenuService; + +/** + * 菜单信息 + * + * @author ruoyi + */ +@Api(tags = "菜单管理") +@RestController +@RequestMapping("/system/menu") +public class SysMenuController extends BaseController +{ + @Autowired + private ISysMenuService menuService; + + /** + * 获取菜单列表 + */ + @ApiOperation("获取菜单列表") + @PreAuthorize("@ss.hasPermi('system:menu:list')") + @GetMapping("/list") + public AjaxResult list(SysMenu menu) + { + List menus = menuService.selectMenuList(menu, getUserId()); + return success(menus); + } + + /** + * 根据菜单编号获取详细信息 + */ + @ApiOperation("根据菜单编号获取详细信息") + @PreAuthorize("@ss.hasPermi('system:menu:query')") + @GetMapping(value = "/{menuId}") + public AjaxResult getInfo(@PathVariable Long menuId) + { + return success(menuService.selectMenuById(menuId)); + } + + /** + * 获取菜单下拉树列表 + */ + @ApiOperation("获取菜单下拉树列表") + @GetMapping("/treeselect") + public AjaxResult treeselect(SysMenu menu) + { + List menus = menuService.selectMenuList(menu, getUserId()); + return success(menuService.buildMenuTreeSelect(menus)); + } + + /** + * 加载对应角色菜单列表树 + */ + @ApiOperation("加载对应角色菜单列表树") + @GetMapping(value = "/roleMenuTreeselect") + public AjaxResult roleMenuTreeselect(@RequestParam Long roleId, @RequestParam Long deptId) + { + List menus = menuService.deptRoleMenuTreeselect(deptId, roleId); +// List menus = menuService.selectMenuList(getUserId()); + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId)); + ajax.put("menus", menuService.buildMenuTreeSelect(menus)); + return ajax; + } + + /** + * 新增菜单 + */ + @ApiOperation("新增菜单") + @PreAuthorize("@ss.hasPermi('system:menu:add')") + @Log(title = "菜单管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysMenu menu) + { + if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) + { + return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) + { + return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + menu.setCreateBy(getUsername()); + return toAjax(menuService.insertMenu(menu)); + } + + /** + * 修改菜单 + */ + @ApiOperation("修改菜单") + @PreAuthorize("@ss.hasPermi('system:menu:edit')") + @Log(title = "菜单管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysMenu menu) + { + if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); + } + else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); + } + else if (menu.getMenuId().equals(menu.getParentId())) + { + return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己"); + } + menu.setUpdateBy(getUsername()); + return toAjax(menuService.updateMenu(menu)); + } + + /** + * 删除菜单 + */ + @ApiOperation("删除菜单") + @PreAuthorize("@ss.hasPermi('system:menu:remove')") + @Log(title = "菜单管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{menuId}") + public AjaxResult remove(@PathVariable("menuId") Long menuId) + { + if (menuService.hasChildByMenuId(menuId)) + { + return warn("存在子菜单,不允许删除"); + } + if (menuService.checkMenuExistRole(menuId)) + { + return warn("菜单已分配,不允许删除"); + } + return toAjax(menuService.deleteMenuById(menuId)); + } + + /** + * 加载对应部门菜单列表树 + */ + @ApiOperation("加载对应部门菜单列表树") + @GetMapping(value = "/deptMenuTreeselect/{deptId}") + public AjaxResult deptMenuTreeselect(@PathVariable("deptId") Long deptId) + { + List menus = menuService.deptMenuTreeselect(deptId); + return success(menuService.buildMenuTreeSelect(menus)); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysNoticeController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysNoticeController.java new file mode 100644 index 0000000..b1c5c5c --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysNoticeController.java @@ -0,0 +1,100 @@ +package com.bnhz.web.controller.system; + +import java.util.List; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.system.domain.SysNotice; +import com.bnhz.system.service.ISysNoticeService; + +/** + * 公告 信息操作处理 + * + * @author ruoyi + */ +@Api(tags = "通知公告") +@RestController +@RequestMapping("/system/notice") +public class SysNoticeController extends BaseController +{ + @Autowired + private ISysNoticeService noticeService; + + /** + * 获取通知公告列表 + */ + @ApiOperation("获取通知公告列表") + @PreAuthorize("@ss.hasPermi('system:notice:list')") + @GetMapping("/list") + public TableDataInfo list(SysNotice notice) + { + startPage(); + List list = noticeService.selectNoticeList(notice); + return getDataTable(list); + } + + /** + * 根据通知公告编号获取详细信息 + */ + @ApiOperation("根据通知公告编号获取详细信息") + @PreAuthorize("@ss.hasPermi('system:notice:query')") + @GetMapping(value = "/{noticeId}") + public AjaxResult getInfo(@PathVariable Long noticeId) + { + return success(noticeService.selectNoticeById(noticeId)); + } + + /** + * 新增通知公告 + */ + @ApiOperation("新增通知公告") + @PreAuthorize("@ss.hasPermi('system:notice:add')") + @Log(title = "通知公告", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysNotice notice) + { + notice.setCreateBy(getUsername()); + return toAjax(noticeService.insertNotice(notice)); + } + + /** + * 修改通知公告 + */ + @ApiOperation("修改通知公告") + @PreAuthorize("@ss.hasPermi('system:notice:edit')") + @Log(title = "通知公告", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysNotice notice) + { + notice.setUpdateBy(getUsername()); + return toAjax(noticeService.updateNotice(notice)); + } + + /** + * 删除通知公告 + */ + @ApiOperation("删除通知公告") + @PreAuthorize("@ss.hasPermi('system:notice:remove')") + @Log(title = "通知公告", businessType = BusinessType.DELETE) + @DeleteMapping("/{noticeIds}") + public AjaxResult remove(@PathVariable Long[] noticeIds) + { + return toAjax(noticeService.deleteNoticeByIds(noticeIds)); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysPostController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysPostController.java new file mode 100644 index 0000000..324ed74 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysPostController.java @@ -0,0 +1,141 @@ +package com.bnhz.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.constant.UserConstants; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.system.domain.SysPost; +import com.bnhz.system.service.ISysPostService; + +/** + * 岗位信息操作处理 + * + * @author ruoyi + */ +@Api(tags = "岗位管理") +@RestController +@RequestMapping("/system/post") +public class SysPostController extends BaseController +{ + @Autowired + private ISysPostService postService; + + /** + * 获取岗位列表 + */ + @ApiOperation("获取岗位列表") + @PreAuthorize("@ss.hasPermi('system:post:list')") + @GetMapping("/list") + public TableDataInfo list(SysPost post) + { + startPage(); + List list = postService.selectPostList(post); + return getDataTable(list); + } + + @ApiOperation("导出岗位列表") + @Log(title = "岗位管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:post:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysPost post) + { + List list = postService.selectPostList(post); + ExcelUtil util = new ExcelUtil(SysPost.class); + util.exportExcel(response, list, "岗位数据"); + } + + /** + * 根据岗位编号获取详细信息 + */ + @ApiOperation("根据岗位编号获取详细信息") + @PreAuthorize("@ss.hasPermi('system:post:query')") + @GetMapping(value = "/{postId}") + public AjaxResult getInfo(@PathVariable Long postId) + { + return success(postService.selectPostById(postId)); + } + + /** + * 新增岗位 + */ + @ApiOperation("新增岗位") + @PreAuthorize("@ss.hasPermi('system:post:add')") + @Log(title = "岗位管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysPost post) + { + if (UserConstants.NOT_UNIQUE.equals(postService.checkPostNameUnique(post))) + { + return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (UserConstants.NOT_UNIQUE.equals(postService.checkPostCodeUnique(post))) + { + return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setCreateBy(getUsername()); + return toAjax(postService.insertPost(post)); + } + + /** + * 修改岗位 + */ + @ApiOperation("修改岗位") + @PreAuthorize("@ss.hasPermi('system:post:edit')") + @Log(title = "岗位管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysPost post) + { + if (UserConstants.NOT_UNIQUE.equals(postService.checkPostNameUnique(post))) + { + return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在"); + } + else if (UserConstants.NOT_UNIQUE.equals(postService.checkPostCodeUnique(post))) + { + return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在"); + } + post.setUpdateBy(getUsername()); + return toAjax(postService.updatePost(post)); + } + + /** + * 删除岗位 + */ + @ApiOperation("删除岗位") + @PreAuthorize("@ss.hasPermi('system:post:remove')") + @Log(title = "岗位管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{postIds}") + public AjaxResult remove(@PathVariable Long[] postIds) + { + return toAjax(postService.deletePostByIds(postIds)); + } + + /** + * 获取岗位选择框列表 + */ + @ApiOperation("获取岗位选择框列表") + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + List posts = postService.selectPostAll(); + return success(posts); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysProfileController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysProfileController.java new file mode 100644 index 0000000..8ebeac4 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysProfileController.java @@ -0,0 +1,163 @@ +package com.bnhz.web.controller.system; + +import com.bnhz.common.annotation.Log; +import com.bnhz.common.config.DaQiConfig; +import com.bnhz.common.constant.UserConstants; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.enums.SocialPlatformType; +import com.bnhz.common.utils.SecurityUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.file.FileUploadUtils; +import com.bnhz.common.utils.file.MimeTypeUtils; +import com.bnhz.framework.web.service.TokenService; +import com.bnhz.iot.domain.UserSocialProfile; +import com.bnhz.iot.service.IUserSocialProfileService; +import com.bnhz.system.service.ISysUserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +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.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 个人信息 业务处理 + * + * @author ruoyi + */ +@Api(tags = "个人中心") +@RestController +@RequestMapping("/system/user/profile") +public class SysProfileController extends BaseController +{ + @Autowired + private ISysUserService userService; + + @Autowired + private TokenService tokenService; + + @Autowired + private IUserSocialProfileService iUserSocialProfileService; + + /** + * 个人信息 + */ + @ApiOperation("获取个人信息") + @GetMapping + public AjaxResult profile() + { + LoginUser loginUser = getLoginUser(); + SysUser user = loginUser.getUser(); + AjaxResult ajax = AjaxResult.success(user); + ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername())); + ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername())); + List socialProfileList = iUserSocialProfileService.selectUserSocialProfile(loginUser.getUserId()); + UserSocialProfile userSocialProfile = socialProfileList.stream().filter(s -> SocialPlatformType.listWechatPlatform.contains(s.getSourceClient()) && "1".equals(s.getStatus())).findFirst().orElse(null); + ajax.put("socialGroup", socialProfileList); + ajax.put("wxBind", userSocialProfile != null); + return ajax; + } + + /** + * 修改用户 + */ + @ApiOperation("修改个人信息") + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult updateProfile(@RequestBody SysUser user) + { + LoginUser loginUser = getLoginUser(); + SysUser sysUser = loginUser.getUser(); + user.setUserName(sysUser.getUserName()); + if (StringUtils.isNotEmpty(user.getPhonenumber()) + && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) + { + return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + if (StringUtils.isNotEmpty(user.getEmail()) + && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) + { + return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setUserId(sysUser.getUserId()); + user.setPassword(null); + user.setAvatar(null); + user.setDeptId(sysUser.getDeptId()); + if (userService.updateUserProfile(user) > 0) + { + // 更新缓存用户信息 + sysUser.setNickName(user.getNickName()); + sysUser.setPhonenumber(user.getPhonenumber()); + sysUser.setEmail(user.getEmail()); + sysUser.setSex(user.getSex()); + tokenService.setLoginUser(loginUser); + return success(); + } + return error("修改个人信息异常,请联系管理员"); + } + + /** + * 重置密码 + */ + @ApiOperation("重置密码") + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping("/updatePwd") + public AjaxResult updatePwd(String oldPassword, String newPassword) + { + LoginUser loginUser = getLoginUser(); + String userName = loginUser.getUsername(); + String password = loginUser.getPassword(); + if (!SecurityUtils.matchesPassword(oldPassword, password)) + { + return error("修改密码失败,旧密码错误"); + } + if (SecurityUtils.matchesPassword(newPassword, password)) + { + return error("新密码不能与旧密码相同"); + } + if (userService.resetUserPwd(userName, SecurityUtils.encryptPassword(newPassword)) > 0) + { + // 更新缓存用户密码 + loginUser.getUser().setPassword(SecurityUtils.encryptPassword(newPassword)); + tokenService.setLoginUser(loginUser); + return success(); + } + return error("修改密码异常,请联系管理员"); + } + + /** + * 头像上传 + */ + @ApiOperation("头像上传") + @Log(title = "用户头像", businessType = BusinessType.UPDATE) + @PostMapping("/avatar") + public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception + { + if (!file.isEmpty()) + { + LoginUser loginUser = getLoginUser(); + String avatar = FileUploadUtils.upload(DaQiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION); + if (userService.updateUserAvatar(loginUser.getUsername(), avatar)) + { + AjaxResult ajax = AjaxResult.success(); + ajax.put("imgUrl", avatar); + // 更新缓存用户头像 + loginUser.getUser().setAvatar(avatar); + tokenService.setLoginUser(loginUser); + return ajax; + } + } + return error("上传图片异常,请联系管理员"); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysRegisterController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysRegisterController.java new file mode 100644 index 0000000..257c28c --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysRegisterController.java @@ -0,0 +1,42 @@ +package com.bnhz.web.controller.system; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +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.RestController; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.model.RegisterBody; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.framework.web.service.SysRegisterService; +import com.bnhz.system.service.ISysConfigService; + +/** + * 注册验证 + * + * @author ruoyi + */ +@Api(tags = "注册账号") +@RestController +public class SysRegisterController extends BaseController +{ + @Autowired + private SysRegisterService registerService; + + @Autowired + private ISysConfigService configService; + + @ApiOperation("注册账号") + @PostMapping("/register") + public AjaxResult register(@RequestBody RegisterBody user) + { + if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) + { + return error("当前系统没有开启注册功能!"); + } + String msg = registerService.register(user); + return StringUtils.isEmpty(msg) ? success() : error(msg); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysRoleController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysRoleController.java new file mode 100644 index 0000000..432cf32 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysRoleController.java @@ -0,0 +1,298 @@ +package com.bnhz.web.controller.system; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.bnhz.common.constant.HttpStatus; +import com.bnhz.common.utils.collection.CollectionUtils; +import com.github.pagehelper.PageInfo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysDept; +import com.bnhz.common.core.domain.entity.SysRole; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.framework.web.service.SysPermissionService; +import com.bnhz.framework.web.service.TokenService; +import com.bnhz.system.domain.SysUserRole; +import com.bnhz.system.service.ISysDeptService; +import com.bnhz.system.service.ISysRoleService; +import com.bnhz.system.service.ISysUserService; + +/** + * 角色信息 + * + * @author ruoyi + */ +@Api(tags = "角色管理") +@RestController +@RequestMapping("/system/role") +public class SysRoleController extends BaseController +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private TokenService tokenService; + + @Autowired + private SysPermissionService permissionService; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysDeptService deptService; + + @ApiOperation("获取角色分页列表") + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/list") + public TableDataInfo list(SysRole role, Integer pageNum, Integer pageSize) + { +// startPage(); + List list = roleService.selectRoleList(role); + TableDataInfo tableDataInfo = new TableDataInfo(); + tableDataInfo.setCode(HttpStatus.SUCCESS); + tableDataInfo.setMsg("查询成功"); + if (org.apache.commons.collections4.CollectionUtils.isEmpty(list)) { + tableDataInfo.setRows(list); + tableDataInfo.setTotal(0); + } else { + List list1 = CollectionUtils.startPage(list, pageNum, pageSize); + tableDataInfo.setRows(list1); + tableDataInfo.setTotal(new PageInfo(list).getTotal()); + } + return tableDataInfo; + } + + @ApiOperation("导出角色列表") + @Log(title = "角色管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:role:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysRole role) + { + List list = roleService.selectRoleList(role); + ExcelUtil util = new ExcelUtil(SysRole.class); + util.exportExcel(response, list, "角色数据"); + } + + /** + * 根据角色编号获取详细信息 + */ + @ApiOperation("根据角色编号获取详细信息") + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping(value = "/{roleId}") + public AjaxResult getInfo(@PathVariable Long roleId) + { + roleService.checkRoleDataScope(roleId); + return success(roleService.selectRoleById(roleId)); + } + + /** + * 新增角色 + */ + @ApiOperation("新增角色") + @PreAuthorize("@ss.hasPermi('system:role:add')") + @Log(title = "角色管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysRole role) + { +// if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role))) +// { +// return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在"); +// } +// else if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role))) +// { +// return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在"); +// } + if ("manager".equals(role.getRoleKey())) { + return error("不允许设置管理员角色标识"); + } + role.setCreateBy(getUsername()); + return toAjax(roleService.insertRole(role)); + + } + + /** + * 修改保存角色 + */ + @ApiOperation("修改角色") + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); +// if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role))) +// { +// return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在"); +// } +// else if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role))) +// { +// return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在"); +// } + role.setUpdateBy(getUsername()); + + if (roleService.updateRole(role) > 0) + { + // 更新缓存用户权限 + LoginUser loginUser = getLoginUser(); + if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin()) + { + loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser())); + loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName())); + tokenService.setLoginUser(loginUser); + } + return success(); + } + return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员"); + } + + /** + * 修改保存数据权限 + */ + @ApiOperation("修改保存数据权限") + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/dataScope") + public AjaxResult dataScope(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + return toAjax(roleService.authDataScope(role)); + } + + /** + * 状态修改 + */ + @ApiOperation("修改角色状态") + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysRole role) + { + roleService.checkRoleAllowed(role); + roleService.checkRoleDataScope(role.getRoleId()); + role.setUpdateBy(getUsername()); + return toAjax(roleService.updateRoleStatus(role)); + } + + /** + * 删除角色 + */ + @ApiOperation("删除角色") + @PreAuthorize("@ss.hasPermi('system:role:remove')") + @Log(title = "角色管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{roleIds}") + public AjaxResult remove(@PathVariable Long[] roleIds) + { + return toAjax(roleService.deleteRoleByIds(roleIds)); + } + + /** + * 获取角色选择框列表 + */ + @ApiOperation("获取角色选择框列表") + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping("/optionselect") + public AjaxResult optionselect() + { + return success(roleService.selectRoleAll()); + } + + /** + * 查询已分配用户角色列表 + */ + @ApiOperation("查询已分配用户角色列表") + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/authUser/allocatedList") + public TableDataInfo allocatedList(SysUser user) + { + startPage(); + List list = userService.selectAllocatedList(user); + return getDataTable(list); + } + + /** + * 查询未分配用户角色列表 + */ + @ApiOperation("查询未分配用户角色列表") + @PreAuthorize("@ss.hasPermi('system:role:list')") + @GetMapping("/authUser/unallocatedList") + public TableDataInfo unallocatedList(SysUser user) + { + startPage(); + List list = userService.selectUnallocatedList(user); + return getDataTable(list); + } + + /** + * 取消授权用户 + */ + @ApiOperation("取消授权用户") + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancel") + public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole) + { + return toAjax(roleService.deleteAuthUser(userRole)); + } + + /** + * 批量取消授权用户 + */ + @ApiOperation("批量取消授权用户") + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/cancelAll") + public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds) + { + return toAjax(roleService.deleteAuthUsers(roleId, userIds)); + } + + /** + * 批量选择用户授权 + */ + @ApiOperation("批量选择用户授权") + @PreAuthorize("@ss.hasPermi('system:role:edit')") + @Log(title = "角色管理", businessType = BusinessType.GRANT) + @PutMapping("/authUser/selectAll") + public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds) + { + roleService.checkRoleDataScope(roleId); + return toAjax(roleService.insertAuthUsers(roleId, userIds)); + } + + /** + * 获取对应角色部门树列表 + */ + @ApiOperation("获取对应角色部门树列表") + @PreAuthorize("@ss.hasPermi('system:role:query')") + @GetMapping(value = "/deptTree/{roleId}") + public AjaxResult deptTree(@PathVariable("roleId") Long roleId) + { + AjaxResult ajax = AjaxResult.success(); + ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId)); + ajax.put("depts", deptService.selectDeptTreeList(new SysDept())); + return ajax; + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysUserController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysUserController.java new file mode 100644 index 0000000..e9244cc --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/system/SysUserController.java @@ -0,0 +1,301 @@ +package com.bnhz.web.controller.system; + +import java.util.List; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletResponse; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.constant.UserConstants; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysDept; +import com.bnhz.common.core.domain.entity.SysRole; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.SecurityUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.system.service.ISysDeptService; +import com.bnhz.system.service.ISysPostService; +import com.bnhz.system.service.ISysRoleService; +import com.bnhz.system.service.ISysUserService; + +/** + * 用户信息 + * + * @author ruoyi + */ +@Api(tags = "用户管理") +@RestController +@RequestMapping("/system/user") +public class SysUserController extends BaseController +{ + @Autowired + private ISysUserService userService; + + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysDeptService deptService; + + @Autowired + private ISysPostService postService; + + /** + * 获取用户列表 + */ + @ApiOperation("获取用户分页列表") + @PreAuthorize("@ss.hasPermi('system:user:list')") + @GetMapping("/list") + public TableDataInfo list(SysUser user) + { + startPage(); + if (null == user.getDeptId()) { + user.setDeptId(getLoginUser().getDeptId()); + } + List list = userService.selectUserList(user); + return getDataTable(list); + } + + @ApiOperation("导出用户列表") + @Log(title = "用户管理", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('system:user:export')") + @PostMapping("/export") + public void export(HttpServletResponse response, SysUser user) + { + if (null == user.getDeptId()) { + user.setDeptId(getLoginUser().getDeptId()); + } + List list = userService.selectUserList(user); + ExcelUtil util = new ExcelUtil(SysUser.class); + util.exportExcel(response, list, "用户数据"); + } + + @ApiOperation("批量导入用户") + @Log(title = "用户管理", businessType = BusinessType.IMPORT) + @PreAuthorize("@ss.hasPermi('system:user:import')") + @PostMapping("/importData") + public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception + { + ExcelUtil util = new ExcelUtil(SysUser.class); + List userList = util.importExcel(file.getInputStream()); + String operName = getUsername(); + String message = userService.importUser(userList, updateSupport, operName); + return success(message); + } + + + @ApiOperation("下载用户导入模板") + @PostMapping("/importTemplate") + public void importTemplate(HttpServletResponse response) + { + ExcelUtil util = new ExcelUtil(SysUser.class); + util.importTemplateExcel(response, "用户数据"); + } + + /** + * 根据用户编号获取详细信息 + */ + @ApiOperation("根据用户编号获取详细信息") + @PreAuthorize("@ss.hasPermi('system:user:query')") + @GetMapping(value = { "/", "/{userId}" }) + public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId) + { + userService.checkUserDataScope(userId); + AjaxResult ajax = AjaxResult.success(); +// List roles = roleService.selectRoleAll(); +// ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + ajax.put("posts", postService.selectPostAll()); + if (StringUtils.isNotNull(userId)) + { + SysUser sysUser = userService.selectUserById(userId); + ajax.put(AjaxResult.DATA_TAG, sysUser); + ajax.put("postIds", postService.selectPostListByUserId(userId)); + SysRole sysRole = new SysRole(); + sysRole.setDeptId(sysUser.getDeptId()); + sysRole.setShowChild(false); + List sysRoleList = roleService.selectRoleList(sysRole); + ajax.put("roles", sysRoleList); + ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList())); + } + return ajax; + } + + /** + * 新增用户 + */ + @ApiOperation("新增用户") + @PreAuthorize("@ss.hasPermi('system:user:add')") + @Log(title = "用户管理", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@Validated @RequestBody SysUser user) + { + if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(user))) + { + return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在"); + } + else if (StringUtils.isNotEmpty(user.getPhonenumber()) + && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) + { + return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + else if (StringUtils.isNotEmpty(user.getEmail()) + && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) + { + return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setCreateBy(getUsername()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + return toAjax(userService.insertUser(user)); + } + + /** + * 修改用户 + */ + @ApiOperation("修改用户") + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@Validated @RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(user))) + { + return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在"); + } + else if (StringUtils.isNotEmpty(user.getPhonenumber()) + && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) + { + return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); + } + else if (StringUtils.isNotEmpty(user.getEmail()) + && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) + { + return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); + } + user.setUpdateBy(getUsername()); + return toAjax(userService.updateUser(user)); + } + + /** + * 删除用户 + */ + @ApiOperation("删除用户") + @PreAuthorize("@ss.hasPermi('system:user:remove')") + @Log(title = "用户管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{userIds}") + public AjaxResult remove(@PathVariable Long[] userIds) + { + if (ArrayUtils.contains(userIds, getUserId())) + { + return error("当前用户不能删除"); + } + return toAjax(userService.deleteUserByIds(userIds)); + } + + /** + * 重置密码 + */ + @ApiOperation("重置用户密码") + @PreAuthorize("@ss.hasPermi('system:user:resetPwd')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/resetPwd") + public AjaxResult resetPwd(@RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword())); + user.setUpdateBy(getUsername()); + return toAjax(userService.resetPwd(user)); + } + + /** + * 状态修改 + */ + @ApiOperation("修改用户状态") + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody SysUser user) + { + userService.checkUserAllowed(user); + userService.checkUserDataScope(user.getUserId()); + user.setUpdateBy(getUsername()); + return toAjax(userService.updateUserStatus(user)); + } + + /** + * 根据用户编号获取授权角色 + */ + @ApiOperation("根据用户编号获取授权角色") + @PreAuthorize("@ss.hasPermi('system:user:query')") + @GetMapping("/authRole/{userId}") + public AjaxResult authRole(@PathVariable("userId") Long userId) + { + AjaxResult ajax = AjaxResult.success(); + SysUser user = userService.selectUserById(userId); + List roles = roleService.selectRolesByUserId(userId); + ajax.put("user", user); + ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList())); + return ajax; + } + + /** + * 用户授权角色 + */ + @ApiOperation("为用户授权角色") + @PreAuthorize("@ss.hasPermi('system:user:edit')") + @Log(title = "用户管理", businessType = BusinessType.GRANT) + @PutMapping("/authRole") + public AjaxResult insertAuthRole(Long userId, Long[] roleIds) + { + userService.checkUserDataScope(userId); + userService.insertUserAuth(userId, roleIds); + return success(); + } + + /** + * 获取部门树列表 + */ + @ApiOperation("获取部门树列表") + @PreAuthorize("@ss.hasPermi('system:user:list')") + @GetMapping("/deptTree") + public AjaxResult deptTree(SysDept dept) + { + return success(deptService.selectDeptTreeList(dept)); + } + + /** + * 获取终端用户列表 + * @param user 用户信息 + * @return com.bnhz.common.core.page.TableDataInfo + */ + @ApiOperation("获取用户分页列表") + @PreAuthorize("@ss.hasPermi('system:user:list')") + @GetMapping("/listTerminal") + public TableDataInfo listTerminal(SysUser user) + { + startPage(); + List list = userService.listTerminal(user); + return getDataTable(list); + } + +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/controller/tool/SwaggerController.java b/bnhz-admin/src/main/java/com/bnhz/web/controller/tool/SwaggerController.java new file mode 100644 index 0000000..a6d852c --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/controller/tool/SwaggerController.java @@ -0,0 +1,24 @@ +package com.bnhz.web.controller.tool; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import com.bnhz.common.core.controller.BaseController; + +/** + * swagger 接口 + * + * @author ruoyi + */ +@Controller +@RequestMapping("/tool/swagger") +public class SwaggerController extends BaseController +{ + @PreAuthorize("@ss.hasPermi('tool:swagger:view')") + @GetMapping() + public String index() + { + return redirect("/swagger-ui.html"); + } +} diff --git a/bnhz-admin/src/main/java/com/bnhz/web/core/config/SwaggerConfig.java b/bnhz-admin/src/main/java/com/bnhz/web/core/config/SwaggerConfig.java new file mode 100644 index 0000000..09624c8 --- /dev/null +++ b/bnhz-admin/src/main/java/com/bnhz/web/core/config/SwaggerConfig.java @@ -0,0 +1,127 @@ +package com.bnhz.web.core.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.bnhz.common.config.DaQiConfig; +import io.swagger.annotations.ApiOperation; +import io.swagger.models.auth.In; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.ApiKey; +import springfox.documentation.service.AuthorizationScope; +import springfox.documentation.service.Contact; +import springfox.documentation.service.SecurityReference; +import springfox.documentation.service.SecurityScheme; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; + +/** + * Swagger2的接口配置 + * + * @author ruoyi + */ +@Configuration +public class SwaggerConfig +{ + /** 系统基础配置 */ + @Autowired + private DaQiConfig daQiConfig; + + /** 是否开启swagger */ + @Value("${swagger.enabled}") + private boolean enabled; + + /** 设置请求的统一前缀 */ + @Value("${swagger.pathMapping}") + private String pathMapping; + + /** + * 创建API + */ + @Bean + public Docket createRestApi() + { + return new Docket(DocumentationType.OAS_30) + // 是否启用Swagger + .enable(enabled) + // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息) + .apiInfo(apiInfo()) + // 设置哪些接口暴露给Swagger展示 + .select() + // 扫描所有有注解的api,用这种方式更灵活 +// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiAdd.class)) + .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) + // 扫描指定包中的swagger注解 + // .apis(RequestHandlerSelectors.basePackage("com.bnhz.project.tool.swagger")) + // 扫描所有 .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.any()) + .build() + /* 设置安全模式,swagger可以设置访问token */ + .securitySchemes(securitySchemes()) + .securityContexts(securityContexts()) + .pathMapping(pathMapping); + } + + /** + * 安全模式,这里指定token通过Authorization头请求头传递 + */ + private List securitySchemes() + { + List apiKeyList = new ArrayList(); + apiKeyList.add(new ApiKey("Authorization", "Authorization", In.HEADER.toValue())); + return apiKeyList; + } + + /** + * 安全上下文 + */ + private List securityContexts() + { + List securityContexts = new ArrayList<>(); + securityContexts.add( + SecurityContext.builder() + .securityReferences(defaultAuth()) + .operationSelector(o -> o.requestMappingPattern().matches("/.*")) + .build()); + return securityContexts; + } + + /** + * 默认的安全上引用 + */ + private List defaultAuth() + { + AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; + authorizationScopes[0] = authorizationScope; + List securityReferences = new ArrayList<>(); + securityReferences.add(new SecurityReference("Authorization", authorizationScopes)); + return securityReferences; + } + + /** + * 添加摘要信息 + */ + private ApiInfo apiInfo() + { + // 用ApiInfoBuilder进行定制 + return new ApiInfoBuilder() + // 设置标题 + .title("大气环保物联网平台接口文档") + // 描述 + .description("描述:大气环保物联网平台") + // 作者信息 + .contact(new Contact(daQiConfig.getName(), null, null)) + // 版本 + .version("版本号:" + daQiConfig.getVersion()) + .build(); + } +} diff --git a/bnhz-admin/src/main/resources/META-INF/spring-devtools.properties b/bnhz-admin/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..37e7b58 --- /dev/null +++ b/bnhz-admin/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.json=/com.alibaba.fastjson2.*.jar \ No newline at end of file diff --git a/bnhz-admin/src/main/resources/application-dev.yml b/bnhz-admin/src/main/resources/application-dev.yml new file mode 100644 index 0000000..af9bcbc --- /dev/null +++ b/bnhz-admin/src/main/resources/application-dev.yml @@ -0,0 +1,173 @@ +# 数据源配置 +spring: + kafka: + bootstrap-servers: home.com:9092 + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.apache.kafka.common.serialization.StringSerializer + consumer: + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer + #指定消费者组的 group_id + group-id: bnhz-dev + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driverClassName: com.mysql.cj.jdbc.Driver + druid: + # 主库数据源 + master: + url: jdbc:mysql://49.234.239.14:3306/atmosphere?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: mmdev + password: Mmdev8848# +# url: jdbc:mysql://182.148.53.138:3313/atmosphere?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 +# username: root +# password: bnhz8866.@ +# url: jdbc:mysql://home.com:3306/atmosphere?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 +# username: leo +# password: ljc123456 + # 从库数据源 + slave: + enabled: false # 从数据源开关/默认关闭 + url: + username: + password: + # TDengine数据库 + tdengine-server: + enabled: true # 默认不启用TDengine,true=启用,false=不启用 + driverClassName: com.taosdata.jdbc.TSDBDriver + url: jdbc:TAOS://test-td1:6030/daqi_log?timezone=UTC-8&charset=utf-8 + username: daqi + password: daqi8866 + dbName: daqi_log + + initialSize: 5 # 初始连接数 + minIdle: 10 # 最小连接池数量 + maxActive: 20 # 最大连接池数量 + maxWait: 60000 # 配置获取连接等待超时的时间 + timeBetweenEvictionRunsMillis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最小生存的时间,单位是毫秒 + maxEvictableIdleTimeMillis: 900000 # 配置一个连接在池中最大生存的时间,单位是毫秒 + validationQuery: SELECT 1 FROM DUAL # 配置检测连接是否有效 + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + webStatFilter: + enabled: true + statViewServlet: + enabled: true + # 设置白名单,不填则允许所有访问 + allow: + url-pattern: /druid/* + # 控制台管理用户名和密码 + login-username: bnhz + login-password: bnhz + filter: + stat: + enabled: true + # 慢SQL记录 + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + # redis 配置 + redis: + host: localhost # 地址 + port: 6379 # 端口,默认为6379 + database: 1 # 数据库索引 + password: # 密码 + timeout: 10s # 连接超时时间 + lettuce: + pool: + min-idle: 0 # 连接池中的最小空闲连接 + max-idle: 8 # 连接池中的最大空闲连接 + max-active: 8 # 连接池的最大数据库连接数 + max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制) + # mqtt 配置 + mqtt: + username: bnhz # 账号 + password: bnhz # 密码 + host-url: tcp://localhost:1883 # mqtt连接tcp地址 + client-id: ${random.int} # 客户端Id,不能相同,采用随机数 ${random.value} + default-topic: test # 默认主题 + timeout: 30 # 超时时间 + keepalive: 30 # 保持连接 + clearSession: true # 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息) + +# sip 配置 +sip: + enabled: false # 是否启用视频监控SIP,true为启用 + ## 本地调试时,绑定网卡局域网IP,设备在同一局域网,设备接入IP填写绑定IP + ## 部署服务端时,默认绑定容器IP,设备接入IP填写服务器公网IP + ip: 177.7.0.13 + port: 5061 # SIP端口(保持默认) + domain: 3402000000 # 由省级、市级、区级、基层编号组成 + id: 34020000002000000001 # 同上,另外增加编号,(可保持默认) + password: 12345678 # 监控设备接入的密码 + +# 日志配置 +logging: + level: + com.bnhz: debug + com.yomahub: debug + org.dromara: warn + org.springframework: warn + +# Swagger配置 +swagger: + enabled: true # 是否开启swagger + pathMapping: # 请求前缀 + +liteflow: + #FlowExecutor的execute2Future的线程数,默认为64 + main-executor-works: 64 + #FlowExecutor的execute2Future的自定义线程池Builder + main-executor-class: com.bnhz.mq.ruleEngine.MainExecutorBuilder + #并行节点的线程池Builder + thread-executor-class: com.bnhz.mq.ruleEngine.WhenExecutorBuilder + rule-source-ext-data-map: + # 应用名称,规则链和脚本组件名称需要一致,不要修改 + applicationName: bnhz + #是否开启SQL日志 + sqlLogEnabled: true + # 规则多时,启用快速加载模式 + fast-load: false + #是否开启SQL数据轮询自动刷新机制 默认不开启 + pollingEnabled: false + pollingIntervalSeconds: 60 + pollingStartSeconds: 60 + #以下是chain表的配置 + chainTableName: iot_scene + chainApplicationNameField: application_name + chainNameField: chain_name + elDataField: el_data + chainEnableField: enable + #以下是script表的配置 + scriptTableName: iot_script + scriptApplicationNameField: application_name + scriptIdField: script_id + scriptNameField: script_name + scriptDataField: script_data + scriptTypeField: script_type + scriptLanguageField: script_language + scriptEnableField: enable +hua-zhi: + url: http://182.148.53.138:11125 + client_id: lvsejixiaotest + client_secret: huanzhi123 +high-obs: + host: 183.221.119.74:1443 + appKey: 27429972 + appSecret: YiJycH8a4bdjyuoHyWcS + picUrlPrefix: https://172.100.0.141:1443/bforestfire/v1/picture/preview?picUrl= +fumes: + host: http://116.62.234.187:8120 + username: gaoxin_sjjk + password: gxqjg@123 + areacode: gxnq + +server: + domain: http://demo.youfans.cn + + diff --git a/bnhz-admin/src/main/resources/application-prod.yml b/bnhz-admin/src/main/resources/application-prod.yml new file mode 100644 index 0000000..a81dddd --- /dev/null +++ b/bnhz-admin/src/main/resources/application-prod.yml @@ -0,0 +1,165 @@ +# 数据源配置 +spring: + kafka: + bootstrap-servers: 192.168.0.20:9092 + # 生产者 key value的序列化方式 + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer + #指定消费者组的 group_id + group-id: bnhz-prod + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driverClassName: com.mysql.cj.jdbc.Driver + druid: + # 主库数据源 + master: + url: jdbc:mysql://49.234.239.14:3306/atmosphere?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: mmdev + password: Mmdev8848# + # 从库数据源 + slave: + enabled: false # 从数据源开关/默认关闭 + url: + username: + password: + # TDengine数据库 + tdengine-server: + enabled: false # 默认不启用TDengine,true=启用,false=不启用 + driverClassName: com.taosdata.jdbc.TSDBDriver + url: jdbc:TAOS://bnhz:6030/bnhz_log?timezone=Asia/Beijing&charset=utf-8 + username: root + password: taosdata + dbName: bnhz_log + + initialSize: 5 # 初始连接数 + minIdle: 10 # 最小连接池数量 + maxActive: 20 # 最大连接池数量 + maxWait: 60000 # 配置获取连接等待超时的时间 + timeBetweenEvictionRunsMillis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最小生存的时间,单位是毫秒 + maxEvictableIdleTimeMillis: 900000 # 配置一个连接在池中最大生存的时间,单位是毫秒 + validationQuery: SELECT 1 FROM DUAL # 配置检测连接是否有效 + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + webStatFilter: + enabled: true + statViewServlet: + enabled: true + # 设置白名单,不填则允许所有访问 + allow: + url-pattern: /druid/* + # 控制台管理用户名和密码 + login-username: bnhz + login-password: bnhz + filter: + stat: + enabled: true + # 慢SQL记录 + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + # redis 配置 + redis: + host: localhost # 地址 + port: 6379 # 端口,默认为6379 + database: 0 # 数据库索引 + password: bnhz # 密码 + timeout: 10s # 连接超时时间 + lettuce: + pool: + min-idle: 0 # 连接池中的最小空闲连接 + max-idle: 8 # 连接池中的最大空闲连接 + max-active: 8 # 连接池的最大数据库连接数 + max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制) + # mqtt 配置 + mqtt: + username: bnhz # 账号(仅用于后端自认证) + password: bnhz # 密码(仅用于后端自认证) + host-url: tcp://177.7.0.12:1883 # 连接 Emqx 消息服务器地址 + # host-url: tcp://177.7.0.13:1883 # 内置netty mqtt broker地址 + client-id: ${random.int} # 客户端Id,不能相同,采用随机数 ${random.value} + default-topic: test # 默认主题 + timeout: 30 # 超时时间 + keepalive: 30 # 保持连接 + clearSession: true # 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息) + +# sip 配置 +sip: + enabled: false # 是否启用视频监控SIP,true为启用 + ## 本地调试时,绑定网卡局域网IP,设备在同一局域网,设备接入IP填写绑定IP + ## 部署服务端时,默认绑定容器IP,设备接入IP填写服务器公网IP + ip: 177.7.0.13 + port: 5061 # SIP端口(保持默认) + domain: 3402000000 # 由省级、市级、区级、基层编号组成 + id: 34020000002000000001 # 同上,另外增加编号,(可保持默认) + password: 12345678 # 监控设备接入的密码 + +# 日志配置 +logging: + level: + com.bnhz: error + com.yomahub: warn + org.dromara: warn + org.springframework: warn + +# Swagger配置 +swagger: + enabled: true # 是否开启swagger + pathMapping: /prod-api # 请求前缀 + + +liteflow: + #FlowExecutor的execute2Future的线程数 + main-executor-works: 128 + #FlowExecutor的execute2Future的自定义线程池Builder + main-executor-class: com.bnhz.mq.ruleEngine.MainExecutorBuilder + #并行节点的线程池Builder + thread-executor-class: com.bnhz.mq.ruleEngine.WhenExecutorBuilder + rule-source-ext-data-map: + # 应用名称,规则链和脚本组件名称需要一致,不要修改 + applicationName: bnhz + #是否开启SQL日志 + sqlLogEnabled: true + # 规则多时,启用快速加载模式 + fast-load: false + #是否开启SQL数据轮询自动刷新机制 默认不开启 + pollingEnabled: false + pollingIntervalSeconds: 60 + pollingStartSeconds: 60 + #以下是chain表的配置 + chainTableName: iot_scene + chainApplicationNameField: application_name + chainNameField: chain_name + elDataField: el_data + chainEnableField: enable + #以下是script表的配置 + scriptTableName: iot_script + scriptApplicationNameField: application_name + scriptIdField: script_id + scriptNameField: script_name + scriptDataField: script_data + scriptTypeField: script_type + scriptLanguageField: script_language + scriptEnableField: enable +huazhi: + url: http://182.148.53.138:11125 + client_id: lvsejixiaotest + client_secret: huanzhi123 +high-obs: + host: 183.221.119.74:1443 + appKey: 27429972 + appSecret: YiJycH8a4bdjyuoHyWcS + picUrlPrefix: https://172.100.0.141:1443/bforestfire/v1/picture/preview?picUrl= +fumes: + host: http://116.62.234.187:8120 + username: gaoxin_sjjk + password: gxqjg@123 + areacode: gxnq diff --git a/bnhz-admin/src/main/resources/application-test.yml b/bnhz-admin/src/main/resources/application-test.yml new file mode 100644 index 0000000..4346b33 --- /dev/null +++ b/bnhz-admin/src/main/resources/application-test.yml @@ -0,0 +1,177 @@ +server: + domain: http://123 +# 数据源配置 +spring: + kafka: + bootstrap-servers: 192.168.0.20:9092 + # 生产者 key value的序列化方式 + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + # 消费者 key value的反序列化方式(当前项目暂时不需要消费) + consumer: + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring: + json: + trusted: + packages: "com.bnhz.adapter.model.blackcar" + value: + default: + type: "com.bnhz.adapter.model.blackcar.Point" + #指定消费者组的 group_id + group-id: bnhz-test + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driverClassName: com.mysql.cj.jdbc.Driver + druid: + # 主库数据源 + master: + url: jdbc:mysql://49.234.239.14:3306/atmosphere?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: mmdev + password: Mmdev8848# + # 从库数据源 + slave: + enabled: false # 从数据源开关/默认关闭 + url: + username: + password: + # TDengine数据库 + tdengine-server: + enabled: true # 默认不启用TDengine,true=启用,false=不启用 + driverClassName: com.taosdata.jdbc.TSDBDriver +# url: jdbc:TAOS://test-td1:6030/daqi_log?timezone=UTC-8&charset=utf-8 + url: jdbc:TAOS://49.234.239.14:6030/daqi_log?timezone=UTC-8&charset=utf-8 + username: daqi + password: daqi8866 + dbName: daqi_log + + initialSize: 5 # 初始连接数 + minIdle: 10 # 最小连接池数量 + maxActive: 20 # 最大连接池数量 + maxWait: 60000 # 配置获取连接等待超时的时间 + timeBetweenEvictionRunsMillis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 + minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最小生存的时间,单位是毫秒 + maxEvictableIdleTimeMillis: 900000 # 配置一个连接在池中最大生存的时间,单位是毫秒 + validationQuery: SELECT 1 FROM DUAL # 配置检测连接是否有效 + testWhileIdle: true + testOnBorrow: false + testOnReturn: false + webStatFilter: + enabled: true + statViewServlet: + enabled: true + # 设置白名单,不填则允许所有访问 + allow: + url-pattern: /druid/* + # 控制台管理用户名和密码 + login-username: bnhz + login-password: bnhz + filter: + stat: + enabled: true + # 慢SQL记录 + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + # redis 配置 + redis: + host: localhost # 地址 + port: 6379 # 端口,默认为6379 + database: 1 # 数据库索引 + password: # 密码 + timeout: 10s # 连接超时时间 + lettuce: + pool: + min-idle: 0 # 连接池中的最小空闲连接 + max-idle: 8 # 连接池中的最大空闲连接 + max-active: 8 # 连接池的最大数据库连接数 + max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制) + # mqtt 配置 + mqtt: + username: bnhz # 账号 + password: bnhz # 密码 + host-url: tcp://localhost:1883 # mqtt连接tcp地址 + client-id: ${random.int} # 客户端Id,不能相同,采用随机数 ${random.value} + default-topic: test # 默认主题 + timeout: 30 # 超时时间 + keepalive: 30 # 保持连接 + clearSession: true # 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息) + +# sip 配置 +sip: + enabled: false # 是否启用视频监控SIP,true为启用 + ## 本地调试时,绑定网卡局域网IP,设备在同一局域网,设备接入IP填写绑定IP + ## 部署服务端时,默认绑定容器IP,设备接入IP填写服务器公网IP + ip: 177.7.0.13 + port: 5061 # SIP端口(保持默认) + domain: 3402000000 # 由省级、市级、区级、基层编号组成 + id: 34020000002000000001 # 同上,另外增加编号,(可保持默认) + password: 12345678 # 监控设备接入的密码 + +# 日志配置 +logging: + level: + com.bnhz: debug + com.yomahub: debug + org.dromara: warn + org.springframework: warn + +# Swagger配置 +swagger: + enabled: true # 是否开启swagger + pathMapping: # 请求前缀 + +liteflow: + #FlowExecutor的execute2Future的线程数,默认为64 + main-executor-works: 64 + #FlowExecutor的execute2Future的自定义线程池Builder + main-executor-class: com.bnhz.mq.ruleEngine.MainExecutorBuilder + #并行节点的线程池Builder + thread-executor-class: com.bnhz.mq.ruleEngine.WhenExecutorBuilder + rule-source-ext-data-map: + # 应用名称,规则链和脚本组件名称需要一致,不要修改 + applicationName: bnhz + #是否开启SQL日志 + sqlLogEnabled: true + # 规则多时,启用快速加载模式 + fast-load: false + #是否开启SQL数据轮询自动刷新机制 默认不开启 + pollingEnabled: false + pollingIntervalSeconds: 60 + pollingStartSeconds: 60 + #以下是chain表的配置 + chainTableName: iot_scene + chainApplicationNameField: application_name + chainNameField: chain_name + elDataField: el_data + chainEnableField: enable + #以下是script表的配置 + scriptTableName: iot_script + scriptApplicationNameField: application_name + scriptIdField: script_id + scriptNameField: script_name + scriptDataField: script_data + scriptTypeField: script_type + scriptLanguageField: script_language + scriptEnableField: enable +hua-zhi: + url: http://182.148.53.138:11125 + client_id: lvsejixiaotest + client_secret: huanzhi123 +high-obs: + host: 183.221.119.74:1443 + appKey: 27429972 + appSecret: YiJycH8a4bdjyuoHyWcS + picUrlPrefix: https://172.100.0.141:1443/bforestfire/v1/picture/preview?picUrl= + +fumes: + host: http://116.62.234.187:8120 + username: gaoxin_sjjk + password: gxqjg@123 + areacode: gxnq + diff --git a/bnhz-admin/src/main/resources/application.yml b/bnhz-admin/src/main/resources/application.yml new file mode 100644 index 0000000..6c0fbb5 --- /dev/null +++ b/bnhz-admin/src/main/resources/application.yml @@ -0,0 +1,120 @@ +# 项目相关配置 +bnhz: + name: bnhz # 名称 + version: 3.8.5 # 版本 + copyrightYear: 2023 # 版权年份 + demoEnabled: true # 实例演示开关 + # 文件路径,以uploadPath结尾 示例( Windows配置 D:/uploadPath,Linux配置 /uploadPath) + profile: /uploadPath + addressEnabled: true # 获取ip地址开关 + captchaType: math # 验证码类型 math 数组计算 char 字符验证 + workId: 1 + distributed: false + +# 开发环境配置 +server: + port: 8080 # 服务器的HTTP端口,默认为8080 + servlet: + context-path: / # 应用的访问路径 + tomcat: + uri-encoding: UTF-8 # tomcat的URI编码 + accept-count: 1000 # 连接数满后的排队数,默认为100 + threads: + max: 800 # tomcat最大线程数,默认为200 + min-spare: 100 # Tomcat启动初始化的线程数,默认值10 + # 基于netty的服务器 + broker: + enabled: true # mqttBroker类型选择, true: 基于netty的mqttBroker和webSocket false: emq的mqttBroker + broker-node: node1 # 服务器集群节点 + port: 1883 + openws: true # 控制webSocket是否开启 + websocket-port: 8083 + websocket-path: /mqtt + keep-alive: 70 # 默认的全部客户端心跳上传时间 + #TCP服务端口 + tcp: + enabled: true # 控制tcp端口是否开启 + port: 8888 + keep-alive: 70 + delimiter: 0x7e + udp: + enabled: false # 控制udp端口是否开启 + port: 8889 + read-idle: 300 # udp保活时间 默认5分钟 + #平台判断离线时间 是心跳时间的2倍 + device: + platform: + expried: 120 + +# Spring配置 +spring: + # 环境配置,dev=开发环境,prod=生产环境 + profiles: + active: dev # 环境配置,dev=开发环境,prod=生产环境 + # 资源信息 + messages: + # 国际化资源文件路径 + basename: i18n/messages + # 文件上传 + servlet: + multipart: + max-file-size: 10MB # 单个文件大小 + max-request-size: 20MB # 设置总上传的文件大小 + # 服务模块 + devtools: + restart: + enabled: true # 热部署开关 + task: + execution: + pool: + core-size: 20 # 最小连接数 + max-size: 200 # 最大连接数 + queue-capacity: 3000 # 最大容量 + keep-alive: 60 + +#集群配置 +cluster: + enable: true + type: redis + + +# 用户配置 +user: + password: + maxRetryCount: 5 # 密码最大错误次数 + lockTime: 10 # 密码锁定时间(默认10分钟) + +# token配置 +token: + header: Authorization # 令牌自定义标识 + secret: abcdefghijklfastbeesmartrstuvwxyz # 令牌密钥 + expireTime: 1440 # 令牌有效期(默认30分钟)1440为一天 + +# MyBatis配置 +#mybatis: +# typeAliasesPackage: com.bnhz.**.domain # 搜索指定包别名 +# mapperLocations: classpath*:mapper/**/*Mapper.xml # 配置mapper的扫描,找到所有的mapper.xml映射文件 +# configLocation: classpath:mybatis/mybatis-config.xml # 加载全局的配置文件 + +# mybatis-plus配置 +mybatis-plus: + typeAliasesPackage: com.bnhz.**.domain # 搜索指定包别名 + mapperLocations: classpath*:mapper/**/*Mapper.xml # 配置mapper的扫描,找到所有的mapper.xml映射文件 + configLocation: classpath:mybatis/mybatis-config.xml # 加载全局的配置文件 + global-config: + db-config: + id-type: AUTO # 自增 ID + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + +# PageHelper分页插件 +pagehelper: + helperDialect: mysql + supportMethodsArguments: true + params: count=countSql + +# 防止XSS攻击 +xss: + enabled: true # 过滤开关 + excludes: /system/notice # 排除链接(多个用逗号分隔) + urlPatterns: /system/*,/monitor/*,/tool/* # 匹配链接 diff --git a/bnhz-admin/src/main/resources/banner.txt b/bnhz-admin/src/main/resources/banner.txt new file mode 100644 index 0000000..e425a31 --- /dev/null +++ b/bnhz-admin/src/main/resources/banner.txt @@ -0,0 +1,2 @@ +Application Version: ${bnhz.version} +Spring Boot Version: ${spring-boot.version} diff --git a/bnhz-admin/src/main/resources/i18n/messages.properties b/bnhz-admin/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..4098fc9 --- /dev/null +++ b/bnhz-admin/src/main/resources/i18n/messages.properties @@ -0,0 +1,37 @@ +#错误消息 +not.null=* 必须填写 +user.jcaptcha.error=验证码错误 +user.jcaptcha.expire=验证码已失效 +user.not.exists=用户不存在/密码错误 +user.password.not.match=用户不存在/密码错误 +user.password.retry.limit.count=密码输入错误{0}次 +user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 +user.password.delete=对不起,您的账号已被删除 +user.blocked=用户已封禁,请联系管理员 +role.blocked=角色已封禁,请联系管理员 +user.logout.success=退出成功 + +length.not.valid=长度必须在{min}到{max}个字符之间 + +user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头 +user.password.not.valid=* 5-50个字符 + +user.email.not.valid=邮箱格式错误 +user.mobile.phone.number.not.valid=手机号格式错误 +user.login.success=登录成功 +user.register.success=注册成功 +user.notfound=请重新登录 +user.forcelogout=管理员强制退出,请重新登录 +user.unknown.error=未知错误,请重新登录 + +##文件上传消息 +upload.exceed.maxSize=上传的文件大小超出限制的文件大小!
允许的文件最大大小是:{0}MB! +upload.filename.exceed.length=上传的文件名最长{0}个字符 + +##权限 +no.permission=您没有数据的权限,请联系管理员添加权限 [{0}] +no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}] +no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}] +no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}] +no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}] +no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}] diff --git a/bnhz-admin/src/main/resources/logback.xml b/bnhz-admin/src/main/resources/logback.xml new file mode 100644 index 0000000..d8f2c01 --- /dev/null +++ b/bnhz-admin/src/main/resources/logback.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/sys-console.log + + + + ${log.path}/sys-console.%d{yyyy-MM-dd}.log + + 7 + + + ${log.pattern} + + + + + + ACCEPT + + DENY + + + + + + + ${log.path}/sys-debug.log + + + + ${log.path}/sys-debug.%d{yyyy-MM-dd}.log + + 10 + + + ${log.pattern} + + + + DEBUG + + ACCEPT + + DENY + + + + + + ${log.path}/sys-info.log + + + + ${log.path}/sys-info.%d{yyyy-MM-dd}.log + + 100 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/sys-error.log + + + + ${log.path}/sys-error.%d{yyyy-MM-dd}.log + + 100 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + ${log.path}/sys-user.log + + + ${log.path}/sys-user.%d{yyyy-MM-dd}.log + + 100 + + + ${log.pattern} + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bnhz-admin/src/main/resources/mybatis/mybatis-config.xml b/bnhz-admin/src/main/resources/mybatis/mybatis-config.xml new file mode 100644 index 0000000..ac47c03 --- /dev/null +++ b/bnhz-admin/src/main/resources/mybatis/mybatis-config.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/bnhz-admin/src/test/java/com/bnhz/BlackCarTest.java b/bnhz-admin/src/test/java/com/bnhz/BlackCarTest.java new file mode 100644 index 0000000..6596e44 --- /dev/null +++ b/bnhz-admin/src/test/java/com/bnhz/BlackCarTest.java @@ -0,0 +1,29 @@ +package com.bnhz; + +import com.bnhz.adapter.model.blackcar.Point; +import com.bnhz.adapter.service.blackcar.impl.BlackCarServiceImpl; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ActiveProfiles; + +/** + * @author Leo + * @date 2024/7/3 17:29 + */ +@ActiveProfiles("test") +@SpringBootTest +@Disabled +public class BlackCarTest { + + @Autowired + private BlackCarServiceImpl blackCarService; + + @Test + @Rollback + public void initModelTest() { + blackCarService.initModel(new Point(), 999L, "测试模型导入"); + } +} diff --git a/bnhz-admin/src/test/java/com/bnhz/FumesTest.java b/bnhz-admin/src/test/java/com/bnhz/FumesTest.java new file mode 100644 index 0000000..6975364 --- /dev/null +++ b/bnhz-admin/src/test/java/com/bnhz/FumesTest.java @@ -0,0 +1,117 @@ +package com.bnhz; + +import com.bnhz.adapter.model.fumes.FumesOneMin; +import com.bnhz.adapter.service.fumes.IFumesService; +import com.bnhz.adapter.util.ThingsModelUtils; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.utils.reflect.ReflectUtils; +import com.bnhz.iot.domain.ThingsModel; +import com.bnhz.iot.model.ThingsModels.ThingsModelEventVO; +import com.bnhz.iot.model.ThingsModels.ThingsModelQuery; +import com.bnhz.iot.service.IEventLogService; +import com.bnhz.iot.service.IThingsModelService; +import com.bnhz.iot.tdengine.service.IColumnModeOperationsService; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * @author Leo + * @date 2024/7/2 14:00 + */ +@ActiveProfiles("test") +@SpringBootTest +@Disabled +public class FumesTest { + + @Autowired + private IFumesService fumesService; + + @Autowired + private IEventLogService eventLogService; + + @Autowired + private IThingsModelService thingsModelService; + + @Autowired + private IColumnModeOperationsService columnModeOperationsService; + + + @Test + public void syncDeviceTest() { + fumesService.syncDevice(); + } + + @Test + public void syncAlarmMsgTest() { + fumesService.syncAlarmMsg(); + } + + @Test + public void syncDetectorTest() { + fumesService.syncDetector(); + } + + @Test + public void syncTenDataTest() { + fumesService.syncTenData(); + } + + @Test + public void syncOneDataTest() { + fumesService.syncOneData(); + } + + @Test + public void syncPointEventTest() { + fumesService.syncPointEvent(); + } + + @Test + public void syncReduceTest() { + fumesService.syncReduce(); + } + + @Test + public void syncRealTimeDataTest() { + fumesService.syncRealTimeData(); + } + + + + @Test + public void listColumnEventTest() { + ThingsModelQuery thingsModelQuery = new ThingsModelQuery(); + thingsModelQuery.setProductId(163L); + thingsModelQuery.setModelId(1313L); + thingsModelQuery.setDeviceId(437L); + thingsModelQuery.setStartDateTime(LocalDateTime.now().minusDays(5)); + thingsModelQuery.setEndDateTime(LocalDateTime.now()); + List> maps = eventLogService.listColumnEvent(thingsModelQuery); + System.out.println(maps.size()); + } + + @Test + public void listEventModeListTest() { + List thingsModelEventVOS = thingsModelService.listEventModeList(163L); + System.out.println(thingsModelEventVOS); + } + + @SneakyThrows + @Test + public void initOneDataTest() { + Map oneAllFields = ReflectUtils.getAllFields(new FumesOneMin()); + ThingsModel oneModel = ThingsModelUtils.toEventThingsModel(oneAllFields, 198L, "餐饮油烟点位设备", "一分钟数据", BnhzConstant.FumesEvent.ONE_DATA); + thingsModelService.insertThingsModel(oneModel); + columnModeOperationsService.ddl(198L, Arrays.asList(oneModel)); + } + +} diff --git a/bnhz-admin/src/test/java/com/bnhz/TDengineTest.java b/bnhz-admin/src/test/java/com/bnhz/TDengineTest.java new file mode 100644 index 0000000..318a7a4 --- /dev/null +++ b/bnhz-admin/src/test/java/com/bnhz/TDengineTest.java @@ -0,0 +1,121 @@ +package com.bnhz; + +import com.alibaba.fastjson.JSONObject; +import com.bnhz.adapter.model.blackcar.Point; +import com.bnhz.adapter.model.fumes.FumesAlarmMsg; +import com.bnhz.adapter.util.ThingsModelUtils; +import com.bnhz.common.core.thingsModel.ThingsModelSimpleItem; +import com.bnhz.common.core.thingsModel.ThingsModelValuesInput; +import com.bnhz.common.utils.reflect.ReflectUtils; +import com.bnhz.iot.domain.ThingsModel; +import com.bnhz.iot.model.ThingsModels.ThingsModelQuery; +import com.bnhz.iot.model.dto.ColumnDto; +import com.bnhz.iot.tdengine.dao.TDDynamicDAO; +import com.bnhz.iot.tdengine.service.IColumnModeOperationsService; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Leo + * @date 2024/6/27 11:24 + */ +@ActiveProfiles("test") +@SpringBootTest +@Disabled +public class TDengineTest { + + @Autowired + private IColumnModeOperationsService columnModeOperationsService; + + @Autowired + private TDDynamicDAO tdDynamicDAO; + + + @Test + public void ddlTest() { + String json = "{\"templateId\":null,\"templateName\":\"测试DDL\",\"userId\":null,\"userName\":null,\"tenantId\":null,\"tenantName\":null,\"identifier\":\"abc\",\"modelOrder\":0,\"type\":\"3\",\"datatype\":\"object\",\"isSys\":null,\"isChart\":0,\"isHistory\":1,\"isMonitor\":0,\"isReadonly\":1,\"isSharePerm\":0,\"delFlag\":null,\"createBy\":\"admin\",\"createTime\":null,\"updateBy\":null,\"updateTime\":null,\"remark\":null,\"owner\":null,\"specs\":\"{\\\"type\\\":\\\"object\\\",\\\"params\\\":[{\\\"name\\\":\\\"二硫化碳天累计值\\\",\\\"id\\\":\\\"abc_a99051-Cou-D\\\",\\\"order\\\":1,\\\"datatype\\\":{\\\"type\\\":\\\"decimal\\\",\\\"min\\\":0,\\\"max\\\":1000,\\\"unit\\\":\\\"毫克/立方米\\\",\\\"step\\\":1},\\\"isChart\\\":0,\\\"isHistory\\\":1,\\\"isSharePerm\\\":0,\\\"isMonitor\\\":1,\\\"isReadonly\\\":1},{\\\"name\\\":\\\"二硫化碳时累计值\\\",\\\"id\\\":\\\"abc_a99051-Cou-H\\\",\\\"order\\\":1,\\\"datatype\\\":{\\\"type\\\":\\\"decimal\\\",\\\"min\\\":0,\\\"max\\\":1000,\\\"unit\\\":\\\"毫克/立方米\\\",\\\"step\\\":1},\\\"isChart\\\":0,\\\"isHistory\\\":1,\\\"isSharePerm\\\":0,\\\"isMonitor\\\":1,\\\"isReadonly\\\":1},{\\\"name\\\":\\\"丙烯腈时累计值\\\",\\\"id\\\":\\\"abc_a99010-Cou-H\\\",\\\"order\\\":1,\\\"datatype\\\":{\\\"type\\\":\\\"decimal\\\",\\\"min\\\":0,\\\"max\\\":1000,\\\"unit\\\":\\\"毫克/立方米\\\",\\\"step\\\":1},\\\"isChart\\\":0,\\\"isHistory\\\":1,\\\"isSharePerm\\\":0,\\\"isMonitor\\\":1,\\\"isReadonly\\\":1}]}\"}"; + ThingsModel thingsModel = JSONObject.parseObject(json, ThingsModel.class); + thingsModel.setProductId(1L); + thingsModel.setModelId(1000L); + columnModeOperationsService.ddl(thingsModel.getProductId(), Arrays.asList(thingsModel)); + + columnModeOperationsService.save(mockThingsModelValuesInput()); + } + + @Test + public void ddlEntityTest() throws IllegalAccessException { + Map allFields = ReflectUtils.getAllFields(new Point()); + Long productId = 1L; + List propertyThingsModel = ThingsModelUtils.toPropertyThingsModel(allFields, productId, "测试产品2"); + columnModeOperationsService.ddl(productId, propertyThingsModel); + + } + + + + @Test + public void queryTest() { + //columnModeOperationsService.save(mockThingsModelValuesInput()); + ThingsModelQuery thingsModelQuery = new ThingsModelQuery(); + thingsModelQuery.setType(3); + thingsModelQuery.setProductId(164L); + thingsModelQuery.setDeviceId(1L); + thingsModelQuery.setModelId(1514L); + thingsModelQuery.setStartDateTime(LocalDateTime.now().minusDays(2)); + thingsModelQuery.setEndDateTime(LocalDateTime.now()); + thingsModelQuery.setOrderField("create_time"); + List> query = columnModeOperationsService.query(thingsModelQuery); + System.out.println(query.size()); + } + + @Test + public void columnNameTest() { + Set columnNames = tdDynamicDAO.queryColumnName("daqi_log", "abc"); + System.out.println(columnNames); + } + + @Test + public void tableNameTest() { + Set tableList = tdDynamicDAO.queryTableList("daqi_log"); + System.out.println(tableList); + } + + @Test + public void addColumnTest() { + ColumnDto columnDto = new ColumnDto(); + columnDto.setColumnName("a_b_c"); + columnDto.setType("string"); + tdDynamicDAO.addColumn("daqi_log", "event_999_1473", columnDto); + } + + private ThingsModelValuesInput mockThingsModelValuesInput() { + ThingsModelValuesInput thingsModelValuesInput = new ThingsModelValuesInput(); + thingsModelValuesInput.setDataTime(LocalDateTime.now().minusDays(1)); + thingsModelValuesInput.setType(3); + thingsModelValuesInput.setProductId(1L); + thingsModelValuesInput.setDeviceId(1L); + thingsModelValuesInput.setModelId(1000L); + thingsModelValuesInput.setDeviceNumber("S001"); + ThingsModelSimpleItem thingsModelSimpleItem1 = new ThingsModelSimpleItem(); + thingsModelSimpleItem1.setId("a99010_cou_h"); + thingsModelSimpleItem1.setValue("10.2"); + ThingsModelSimpleItem thingsModelSimpleItem2 = new ThingsModelSimpleItem(); + thingsModelSimpleItem2.setId("a99051_cou_d"); + thingsModelSimpleItem2.setValue("10.3"); + ThingsModelSimpleItem thingsModelSimpleItem3 = new ThingsModelSimpleItem(); + thingsModelSimpleItem3.setId("a99051_cou_h"); + thingsModelSimpleItem3.setValue("10.4"); + thingsModelValuesInput.setThingsModelValueRemarkItem(Arrays.asList(thingsModelSimpleItem1, thingsModelSimpleItem2, thingsModelSimpleItem3)); + return thingsModelValuesInput; + } + +} diff --git a/bnhz-admin/src/test/java/com/bnhz/VideoMonitorTest.java b/bnhz-admin/src/test/java/com/bnhz/VideoMonitorTest.java new file mode 100644 index 0000000..df656a4 --- /dev/null +++ b/bnhz-admin/src/test/java/com/bnhz/VideoMonitorTest.java @@ -0,0 +1,111 @@ +package com.bnhz; + +import com.bnhz.adapter.service.video.impl.HighObsServiceImpl; +import com.bnhz.adapter.service.video.impl.HuaZhiVideoMonitorServiceImpl; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.page.PageResult; +import com.bnhz.iot.model.ext.video.VideoDeviceInfoVO; +import com.bnhz.iot.model.videoMonitor.*; +import com.bnhz.iot.model.videoMonitor.res.HighObsPlayBackRes; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDateTime; + +/** + * @author Leo + * @date 2024/6/11 11:32 + */ +@ActiveProfiles("test") +@SpringBootTest +@Disabled +public class VideoMonitorTest { + + @Autowired + private HuaZhiVideoMonitorServiceImpl huaZhiVideoMonitorServiceImpl; + + @Autowired + private HighObsServiceImpl highObsServiceImpl; + + + @Test + public void listWithDeviceTest() { + VideoMonitorResult> videoMonitorPageVideoMonitorResult = huaZhiVideoMonitorServiceImpl.listWithDevice(1, 100); + System.out.println(videoMonitorPageVideoMonitorResult.getErrorCode()); + } + + @Test + public void getLiveStreamTest() { + VideoMonitorLiveStreamQuery videoMonitorLiveStreamQuery = new VideoMonitorLiveStreamQuery(); + videoMonitorLiveStreamQuery.setChannelCode("10000000001311000070"); + videoMonitorLiveStreamQuery.setStreamType(0); + videoMonitorLiveStreamQuery.setStreamMode(5); + String liveStream = huaZhiVideoMonitorServiceImpl.getLiveStream(videoMonitorLiveStreamQuery); + System.out.println(liveStream); + } + + @Test + public void getVideoDevicePageHuaZhiTest() { + PageResult videoDevicePage = huaZhiVideoMonitorServiceImpl.getVideoDevicePage(1, 100); + System.out.println(videoDevicePage.getTotal()); + } + + @Test + public void getLiveStreamHuzZhiTest() { + String liveStream = huaZhiVideoMonitorServiceImpl.getLiveStream("10000000001311000070", "hsl"); + System.out.println(liveStream); + } + + @Test + public void getVideoDevicePageHighObsTest() { + PageResult videoDevicePage = highObsServiceImpl.getVideoDevicePage(1, 100); + System.out.println(videoDevicePage.getTotal()); + } + + @Test + public void getLiveStreamHighObsTest() { + String liveStream = highObsServiceImpl.getLiveStream("107203ff6b48480b97b8dda4dc1d108e", BnhzConstant.VideoProtocol.RTSP); + System.out.println(liveStream); + } + + @Test + public void huaZhiSyncTest() { + huaZhiVideoMonitorServiceImpl.syncDevice(); + } + + @Test + public void highObsSyncTest() { + highObsServiceImpl.syncDevice(); + } + + @Test + public void getFireAlarmHistoriesTest() { + HighObsResult> fireAlarmHistories = highObsServiceImpl.getFireAlarmHistories(null, null, 2, 1000); + System.out.println(fireAlarmHistories.getData()); + } + + @Test + public void syncFireHistoriesTest() { + highObsServiceImpl.syncFireHistories(); + } + + @Test + public void syncJudgeTest() { + highObsServiceImpl.syncJudgeList(); + } + + @Test + public void getJudgeList() { + HighObsResult> fireAlarmHistories = highObsServiceImpl.getJudgeList(null, null, 1, 1000); + System.out.println(fireAlarmHistories.getData()); + } + + @Test + public void getPlayBackTest() { + HighObsResult playBack = highObsServiceImpl.getPlayBack("f670c60e4c3e4e72b86ef15e44be5828", LocalDateTime.of(2024, 7, 3, 6, 20,0), LocalDateTime.of(2024, 7, 3, 6, 30,0)); + System.out.println(playBack); + } +} diff --git a/bnhz-admin/src/test/java/com/bnhz/common/RedissonTest.java b/bnhz-admin/src/test/java/com/bnhz/common/RedissonTest.java new file mode 100644 index 0000000..1143646 --- /dev/null +++ b/bnhz-admin/src/test/java/com/bnhz/common/RedissonTest.java @@ -0,0 +1,37 @@ +package com.bnhz.common; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.redisson.api.RBloomFilter; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +/** + * @author Leo + * @date 2024/9/9 16:29 + */ +@ActiveProfiles("test") +@SpringBootTest +@Disabled +public class RedissonTest { + + + @Autowired + private RedissonClient redissonClient; + + @Test + public void bloomFilterTest() { + RBloomFilter bloomFilter = redissonClient.getBloomFilter("phoneList"); + //初始化布隆过滤器:预计元素为100000000L,误差率为3% + bloomFilter.tryInit(5000000L, 0.03); + //将号码10086插入到布隆过滤器中 + bloomFilter.add("10086"); + + //判断下面号码是否在布隆过滤器中 + System.out.println(bloomFilter.contains("123456"));//false + System.out.println(bloomFilter.contains("10086"));//true + } + +} diff --git a/bnhz-common/pom.xml b/bnhz-common/pom.xml new file mode 100644 index 0000000..dc04834 --- /dev/null +++ b/bnhz-common/pom.xml @@ -0,0 +1,200 @@ + + + + daqi-back + com.bnhz + 3.8.5 + + 4.0.0 + + bnhz-common + + + common通用工具 + + + + + + + org.springframework + spring-context-support + + + + + org.springframework + spring-web + + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.baomidou + mybatis-plus-boot-starter + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus-generator.version} + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.apache.commons + commons-lang3 + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.baomidou + dynamic-datasource-spring-boot-starter + 3.5.2 + + + + + com.alibaba.fastjson2 + fastjson2 + + + + + commons-io + commons-io + + + + + commons-fileupload + commons-fileupload + + + + + org.apache.poi + poi-ooxml + + + + + org.yaml + snakeyaml + + + + + io.jsonwebtoken + jjwt + + + + + javax.xml.bind + jaxb-api + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.apache.commons + commons-pool2 + + + + + eu.bitwalker + UserAgentUtils + + + + + javax.servlet + javax.servlet-api + + + + org.projectlombok + lombok + + + + io.swagger + swagger-annotations + 1.6.2 + compile + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.5 + compile + + + + cn.hutool + hutool-all + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + com.google.guava + guava + + + + + + + + + com.alibaba + easyexcel-core + + + + + org.dromara.sms4j + sms4j-spring-boot-starter + 3.0.4 + + + + + diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/Anonymous.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/Anonymous.java new file mode 100644 index 0000000..457a0c1 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/Anonymous.java @@ -0,0 +1,19 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 匿名访问不鉴权注解 + * + * @author ruoyi + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Anonymous +{ +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/ApiAdd.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/ApiAdd.java new file mode 100644 index 0000000..047a3a6 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/ApiAdd.java @@ -0,0 +1,18 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 此注解下表示新增的api接口 + * + * @author Leo + * @date 2024/7/2 18:00 + */ + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiAdd { +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/DataScope.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/DataScope.java new file mode 100644 index 0000000..c0ca9a7 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/DataScope.java @@ -0,0 +1,33 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 数据权限过滤注解 + * + * @author ruoyi + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataScope +{ + /** + * 部门表的别名 + */ + public String deptAlias() default ""; + + /** + * 用户表的别名 + */ + public String userAlias() default ""; + + /** + * 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来 + */ + public String permission() default ""; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/DataSource.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/DataSource.java new file mode 100644 index 0000000..404f803 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/DataSource.java @@ -0,0 +1,28 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.bnhz.common.enums.DataSourceType; + +/** + * 自定义多数据源切换注解 + * + * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准 + * + * @author ruoyi + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface DataSource +{ + /** + * 切换数据源名称 + */ + public DataSourceType value() default DataSourceType.MASTER; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/DictFormat.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/DictFormat.java new file mode 100644 index 0000000..09594e3 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/DictFormat.java @@ -0,0 +1,22 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.*; + +/** + * 字典格式化 + * + * 实现将字典数据的值,格式化成字典数据的标签 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface DictFormat { + + /** + * 例如说,SysDictTypeConstants、InfDictTypeConstants + * + * @return 字典类型 + */ + String value(); + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/Excel.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/Excel.java new file mode 100644 index 0000000..474b298 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/Excel.java @@ -0,0 +1,187 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.BigDecimal; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import com.bnhz.common.utils.poi.ExcelHandlerAdapter; + +/** + * 自定义导出Excel数据注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Excel +{ + /** + * 导出时在excel中排序 + */ + public int sort() default Integer.MAX_VALUE; + + /** + * 导出到Excel中的名字. + */ + public String name() default ""; + + /** + * 日期格式, 如: yyyy-MM-dd + */ + public String dateFormat() default ""; + + /** + * 如果是字典类型,请设置字典的type值 (如: sys_user_sex) + */ + public String dictType() default ""; + + /** + * 读取内容转表达式 (如: 0=男,1=女,2=未知) + */ + public String readConverterExp() default ""; + + /** + * 分隔符,读取字符串组内容 + */ + public String separator() default ","; + + /** + * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化) + */ + public int scale() default -1; + + /** + * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN + */ + public int roundingMode() default BigDecimal.ROUND_HALF_EVEN; + + /** + * 导出时在excel中每个列的高度 单位为字符 + */ + public double height() default 14; + + /** + * 导出时在excel中每个列的宽 单位为字符 + */ + public double width() default 16; + + /** + * 文字后缀,如% 90 变成90% + */ + public String suffix() default ""; + + /** + * 当值为空时,字段的默认值 + */ + public String defaultValue() default ""; + + /** + * 提示信息 + */ + public String prompt() default ""; + + /** + * 设置只能选择不能输入的列内容. + */ + public String[] combo() default {}; + + /** + * 是否需要纵向合并单元格,应对需求:含有list集合单元格) + */ + public boolean needMerge() default false; + + /** + * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写. + */ + public boolean isExport() default true; + + /** + * 另一个类中的属性名称,支持多级获取,以小数点隔开 + */ + public String targetAttr() default ""; + + /** + * 是否自动统计数据,在最后追加一行统计数据总和 + */ + public boolean isStatistics() default false; + + /** + * 导出类型(0数字 1字符串 2图片) + */ + public ColumnType cellType() default ColumnType.STRING; + + /** + * 导出列头背景色 + */ + public IndexedColors headerBackgroundColor() default IndexedColors.GREY_50_PERCENT; + + /** + * 导出列头字体颜色 + */ + public IndexedColors headerColor() default IndexedColors.WHITE; + + /** + * 导出单元格背景色 + */ + public IndexedColors backgroundColor() default IndexedColors.WHITE; + + /** + * 导出单元格字体颜色 + */ + public IndexedColors color() default IndexedColors.BLACK; + + /** + * 导出字段对齐方式 + */ + public HorizontalAlignment align() default HorizontalAlignment.CENTER; + + /** + * 自定义数据处理器 + */ + public Class handler() default ExcelHandlerAdapter.class; + + /** + * 自定义数据处理器参数 + */ + public String[] args() default {}; + + /** + * 字段类型(0:导出导入;1:仅导出;2:仅导入) + */ + Type type() default Type.ALL; + + public enum Type + { + ALL(0), EXPORT(1), IMPORT(2); + private final int value; + + Type(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } + + public enum ColumnType + { + NUMERIC(0), STRING(1), IMAGE(2); + private final int value; + + ColumnType(int value) + { + this.value = value; + } + + public int value() + { + return this.value; + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/Excels.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/Excels.java new file mode 100644 index 0000000..a6713a5 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/Excels.java @@ -0,0 +1,18 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Excel注解集 + * + * @author ruoyi + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Excels +{ + public Excel[] value(); +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/Length.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/Length.java new file mode 100644 index 0000000..61a8b2a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/Length.java @@ -0,0 +1,18 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Leo + * @date 2024/7/5 15:40 + */ + +@Target({ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Length { + + int value() default 0; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/Log.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/Log.java new file mode 100644 index 0000000..661dda6 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/Log.java @@ -0,0 +1,46 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.enums.OperatorType; + +/** + * 自定义操作日志记录注解 + * + * @author ruoyi + * + */ +@Target({ ElementType.PARAMETER, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log +{ + /** + * 模块 + */ + public String title() default ""; + + /** + * 功能 + */ + public BusinessType businessType() default BusinessType.OTHER; + + /** + * 操作人类别 + */ + public OperatorType operatorType() default OperatorType.MANAGE; + + /** + * 是否保存请求的参数 + */ + public boolean isSaveRequestData() default true; + + /** + * 是否保存响应的参数 + */ + public boolean isSaveResponseData() default true; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/RateLimiter.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/RateLimiter.java new file mode 100644 index 0000000..e7edac3 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/RateLimiter.java @@ -0,0 +1,40 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.enums.LimitType; + +/** + * 限流注解 + * + * @author ruoyi + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RateLimiter +{ + /** + * 限流key + */ + public String key() default CacheConstants.RATE_LIMIT_KEY; + + /** + * 限流时间,单位秒 + */ + public int time() default 60; + + /** + * 限流次数 + */ + public int count() default 100; + + /** + * 限流类型 + */ + public LimitType limitType() default LimitType.DEFAULT; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/RepeatSubmit.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/RepeatSubmit.java new file mode 100644 index 0000000..c965953 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/RepeatSubmit.java @@ -0,0 +1,31 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义注解防止表单重复提交 + * + * @author ruoyi + * + */ +@Inherited +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepeatSubmit +{ + /** + * 间隔时间(ms),小于此时间视为重复提交 + */ + public int interval() default 5000; + + /** + * 提示消息 + */ + public String message() default "不允许重复提交,请稍候再试"; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/annotation/SysProtocol.java b/bnhz-common/src/main/java/com/bnhz/common/annotation/SysProtocol.java new file mode 100644 index 0000000..f0ca3e6 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/annotation/SysProtocol.java @@ -0,0 +1,21 @@ +package com.bnhz.common.annotation; + +import java.lang.annotation.*; + +/** + * 表示系统内部协议解析器 + * @author gsb + * @date 2022/10/24 10:33 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SysProtocol { + + /*协议名*/ + String name() default ""; + /*协议编码*/ + String protocolCode() default ""; + //协议描述 + String description() default ""; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/config/DaQiConfig.java b/bnhz-common/src/main/java/com/bnhz/common/config/DaQiConfig.java new file mode 100644 index 0000000..839debf --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/config/DaQiConfig.java @@ -0,0 +1,135 @@ +package com.bnhz.common.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 读取项目相关配置 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "bnhz") +public class DaQiConfig +{ + /** 项目名称 */ + private String name; + + /** 版本 */ + private String version; + + /** 版权年份 */ + private String copyrightYear; + + /** 实例演示开关 */ + private boolean demoEnabled; + + /** 上传路径 */ + private static String profile; + + /** 获取地址开关 */ + private static boolean addressEnabled; + + /** 验证码类型 */ + private static String captchaType; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + + public String getCopyrightYear() + { + return copyrightYear; + } + + public void setCopyrightYear(String copyrightYear) + { + this.copyrightYear = copyrightYear; + } + + public boolean isDemoEnabled() + { + return demoEnabled; + } + + public void setDemoEnabled(boolean demoEnabled) + { + this.demoEnabled = demoEnabled; + } + + public static String getProfile() + { + return profile; + } + + public void setProfile(String profile) + { + DaQiConfig.profile = profile; + } + + public static boolean isAddressEnabled() + { + return addressEnabled; + } + + public void setAddressEnabled(boolean addressEnabled) + { + DaQiConfig.addressEnabled = addressEnabled; + } + + public static String getCaptchaType() { + return captchaType; + } + + public void setCaptchaType(String captchaType) { + DaQiConfig.captchaType = captchaType; + } + + /** + * 获取导入上传路径 + */ + public static String getImportPath() + { + return getProfile() + "/import"; + } + + /** + * 获取头像上传路径 + */ + public static String getAvatarPath() + { + return getProfile() + "/avatar"; + } + + /** + * 获取下载路径 + */ + public static String getDownloadPath() + { + return getProfile() + "/download/"; + } + + /** + * 获取上传路径 + */ + public static String getUploadPath() + { + return getProfile() + "/upload"; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/config/DeviceTask.java b/bnhz-common/src/main/java/com/bnhz/common/config/DeviceTask.java new file mode 100644 index 0000000..560ce5b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/config/DeviceTask.java @@ -0,0 +1,109 @@ +package com.bnhz.common.config; + +import com.bnhz.common.constant.BnhzConstant; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 设备报文处理线程池 + * @author bill + */ +@Configuration +@EnableAsync +@ConfigurationProperties(prefix = "spring.task.execution.pool") +@Data +public class DeviceTask { + + private int coreSize; + + private int maxSize; + + private int queueCapacity; + + private int keepAlive; + + /*设备状态池*/ + @Bean(BnhzConstant.TASK.DEVICE_STATUS_TASK) + public Executor deviceStatusTaskExecutor() { + return builder(BnhzConstant.TASK.DEVICE_STATUS_TASK); + } + + /*平台自动获取线程池(例如定时获取设备信息)*/ + @Bean(BnhzConstant.TASK.DEVICE_FETCH_PROP_TASK) + public Executor deviceFetchTaskExecutor() { + return builder(BnhzConstant.TASK.DEVICE_FETCH_PROP_TASK); + } + + /*设备回调信息(下发指令(服务)设备应答信息)*/ + @Bean(BnhzConstant.TASK.DEVICE_REPLY_MESSAGE_TASK) + public Executor deviceReplyTaskExecutor() { + return builder(BnhzConstant.TASK.DEVICE_REPLY_MESSAGE_TASK); + } + + /*设备主动上报(设备数据有变化主动上报)*/ + @Bean(BnhzConstant.TASK.DEVICE_UP_MESSAGE_TASK) + public Executor deviceUpMessageTaskExecutor() { + return builder(BnhzConstant.TASK.DEVICE_UP_MESSAGE_TASK); + } + /*设备主动上报日志*/ + @Bean(BnhzConstant.TASK.DEVICE_UP_MESSAGE_LOG_TASK) + public Executor deviceUpMessageLogTaskExecutor() { + return builder(BnhzConstant.TASK.DEVICE_UP_MESSAGE_LOG_TASK); + } + + /*指令下发(服务下发)*/ + @Bean(BnhzConstant.TASK.FUNCTION_INVOKE_TASK) + public Executor functionInvokeTaskExecutor() { + return builder(BnhzConstant.TASK.FUNCTION_INVOKE_TASK); + } + + /*内部消费线程*/ + @Bean(BnhzConstant.TASK.MESSAGE_CONSUME_TASK) + public Executor messageConsumeTaskExecutor() { + return builder(BnhzConstant.TASK.MESSAGE_CONSUME_TASK); + } + + @Bean(BnhzConstant.TASK.MESSAGE_CONSUME_TASK_PUB) + public Executor messageConsumePubTaskExecutor(){ + return builder(BnhzConstant.TASK.MESSAGE_CONSUME_TASK_PUB); + } + + @Bean(BnhzConstant.TASK.MESSAGE_CONSUME_TASK_FETCH) + public Executor messageConsumeFetchTaskExecutor(){ + return builder(BnhzConstant.TASK.MESSAGE_CONSUME_TASK_FETCH); + } + + @Bean(BnhzConstant.TASK.DELAY_UPGRADE_TASK) + public Executor delayedTaskExecutor(){ + return builder(BnhzConstant.TASK.DELAY_UPGRADE_TASK); + } + + /*设备其他消息处理*/ + @Bean(BnhzConstant.TASK.DEVICE_OTHER_TASK) + public Executor deviceOtherTaskExecutor(){ + return builder(BnhzConstant.TASK.DEVICE_OTHER_TASK); + } + + /*组装线程池*/ + private ThreadPoolTaskExecutor builder(String threadNamePrefix){ + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(coreSize); + executor.setMaxPoolSize(maxSize); + executor.setKeepAliveSeconds(keepAlive); + executor.setQueueCapacity(queueCapacity); + // 线程池对拒绝任务的处理策略 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); + //线程池名的前缀 + executor.setThreadNamePrefix(threadNamePrefix); + executor.initialize(); + return executor; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/constant/BnhzConstant.java b/bnhz-common/src/main/java/com/bnhz/common/constant/BnhzConstant.java new file mode 100644 index 0000000..632f809 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/constant/BnhzConstant.java @@ -0,0 +1,623 @@ +package com.bnhz.common.constant; + +/** + * 常量 + * + * @author bill + */ +public interface BnhzConstant { + + interface SERVER { + String UFT8 = "UTF-8"; + String GB2312 = "GB2312"; + + + String MQTT = "mqtt"; + String PORT = "port"; + String ADAPTER = "adapter"; + String FRAMEDECODER = "frameDecoder"; + String DISPATCHER = "dispatcher"; + String DECODER = "decoder"; + String ENCODER = "encoder"; + String MAXFRAMELENGTH = "maxFrameLength"; + String SLICER = "slicer"; + String DELIMITERS = "delimiters"; + String IDLE = "idle"; + String WS_PREFIX = "web-"; + String WM_PREFIX = "server-"; + String FAST_PHONE = "phone-"; + + /*MQTT平台判定离线时间 keepAlive*1.5 */ + Long DEVICE_PING_EXPIRED = 90000L; + } + + interface CLIENT { + //加盐 + String TOKEN = "bnhz-smart!@#$123"; + } + + /*webSocket配置*/ + interface WS { + String HEART_BEAT = "heartbeat"; + String HTTP_SERVER_CODEC = "httpServerCodec"; + String AGGREGATOR = "aggregator"; + String COMPRESSOR = "compressor"; + String PROTOCOL = "protocol"; + String MQTT_WEBSOCKET = "mqttWebsocket"; + String DECODER = "decoder"; + String ENCODER = "encoder"; + String BROKER_HANDLER = "brokerHandler"; + + } + + interface TASK { + /** + * 设备上下线任务 + */ + String DEVICE_STATUS_TASK = "deviceStatusTask"; + /** + * 设备主动上报任务 + */ + String DEVICE_UP_MESSAGE_TASK = "deviceUpMessageTask"; + String DEVICE_UP_MESSAGE_LOG_TASK = "deviceUpMessageLogTask"; + /** + * 设备回调任务 + */ + String DEVICE_REPLY_MESSAGE_TASK = "deviceReplyMessageTask"; + /** + * 设备下行任务 + */ + String DEVICE_DOWN_MESSAGE_TASK = "deviceDownMessageTask"; + /** + * 服务调用(指令下发)任务 + */ + String FUNCTION_INVOKE_TASK = "functionInvokeTask"; + /** + * 属性读取任务,区分服务调用 + */ + String DEVICE_FETCH_PROP_TASK = "deviceFetchPropTask"; + /** + * 设备其他消息处理 + */ + String DEVICE_OTHER_TASK = "deviceOtherMsgTask"; + /** + * 消息消费线程 + */ + String MESSAGE_CONSUME_TASK = "messageConsumeTask"; + /*内部消费线程publish*/ + String MESSAGE_CONSUME_TASK_PUB = "messageConsumeTaskPub"; + /*内部消费线程Fetch*/ + String MESSAGE_CONSUME_TASK_FETCH = "messageConsumeTaskFetch"; + /*OTA升级延迟队列*/ + String DELAY_UPGRADE_TASK = "delayUpgradeTask"; + + } + + interface MQTT { + //*上报平台前缀*//* + String UP_TOPIC_SUFFIX = "post"; + //*下发设备前缀*//* + String DOWN_TOPIC_SUFFIX = "get"; + + /*模拟设备后缀*/ + String PROPERTY_GET_SIMULATE = "simulate"; + + String PREDIX = "/+/+"; + + String DUP = "dup"; + String QOS = "qos"; + String RETAIN = "retain"; + String CLEAN_SESSION = "cleanSession"; + + /*集群方式*/ + String REDIS_CHANNEL = "redis"; + String ROCKET_MQ = "rocketmq"; + } + + /*集群,全局发布的消息类型*/ + interface CHANNEL { + /*设备状态*/ + String DEVICE_STATUS = "device_status"; + /*平台读取属性*/ + String PROP_READ = "prop_read"; + /*推送消息*/ + String PUBLISH = "publish"; + /*服务下发*/ + String FUNCTION_INVOKE = "function_invoke"; + /*事件*/ + String EVENT = "event"; + /*other*/ + String OTHER = "other"; + /*Qos1 推送应答*/ + String PUBLISH_ACK = "publish_ack"; + /*Qos2 发布消息收到*/ + String PUB_REC = "pub_rec"; + /*Qos 发布消息释放*/ + String PUB_REL = "pub_rel"; + /*Qos2 发布消息完成*/ + String PUB_COMP = "pub_comp"; + + String UPGRADE = "upgrade"; + + /*-------------------------ROCKETMQ-------------------------*/ + String SUFFIX = "group"; + /*设备状态*/ + String DEVICE_STATUS_GROUP = DEVICE_STATUS + SUFFIX; + String PROP_READ_GROUP = PROP_READ + SUFFIX; + /*服务下发*/ + String FUNCTION_INVOKE_GROUP = FUNCTION_INVOKE + SUFFIX; + /*推送消息*/ + String PUBLISH_GROUP = PUBLISH + SUFFIX; + /*Qos1 推送应答*/ + String PUBLISH_ACK_GROUP = PUBLISH_ACK + SUFFIX; + /*Qos2 发布消息收到*/ + String PUB_REC_GROUP = PUB_REC + SUFFIX; + /*Qos 发布消息释放*/ + String PUB_REL_GROUP = PUB_REL + SUFFIX; + /*Qos2 发布消息完成*/ + String PUB_COMP_GROUP = PUB_COMP + SUFFIX; + /*OTA升级*/ + String UPGRADE_GROUP = UPGRADE + SUFFIX; + } + + + /** + * redisKey 定义 + */ + interface REDIS { + /*redis全局前缀*/ + String GLOBAL_PREFIX_KEY = "bnhz:"; + /*设备在线状态*/ + String DEVICE_STATUS_KEY = "device:status"; + /*在线设备列表*/ + String DEVICE_ONLINE_LIST = "device:online:list"; + /*设备实时状态key*/ + String DEVICE_RUNTIME_DATA = "device:runtime:"; + /*通讯协议参数*/ + String DEVICE_PROTOCOL_PARAM = "device:param:"; + /** + * 设备消息id缓存key + */ + String DEVICE_MESSAGE_ID = "device:messageid"; + /** + * 固件版本key + */ + String FIRMWARE_VERSION = "device:firmware:"; + /** + * 设备信息 + */ + String DEVICE_MSG = "device:msg:"; + + /** + * 采集点变更记录缓存key + */ + String COLLECT_POINT_CHANGE = "collect:point:change:"; + /** + * 属性下发回调 + */ + String PROP_READ_STORE = "prop:read:store:"; + /** + * sip + */ + String RECORDINFO_KEY = "sip:recordinfo:"; + String DEVICEID_KEY = "sip:deviceid:"; + String STREAM_KEY = "sip:stream:"; + String SIP_CSEQ_PREFIX = "sip:CSEQ:"; + String DEFAULT_SIP_CONFIG = "sip:config"; + String DEFAULT_MEDIA_CONFIG = "sip:mediaconfig"; + + /** + * rule + */ + String RULE_SILENT_TIME = "rule:SilentTime"; + + + /** + * 当前连接数 + */ + String MESSAGE_CONNECT_COUNT = "messages:connect:count"; + /** + * 总保留消息 + */ + String MESSAGE_RETAIN_TOTAL = "message:retain:total"; + + /** + * 主题数 + */ + String MESSAGE_TOPIC_TOTAL = "message:topic:total"; + /*发送消息数*/ + String MESSAGE_SEND_TOTAL = "message:send:total"; + /*接收消息数*/ + String MESSAGE_RECEIVE_TOTAL = "message:receive:total"; + /*连接次数*/ + String MESSAGE_CONNECT_TOTAL = "message:connect:total"; + /** + * 认证次数 + */ + String MESSAGE_AUTH_TOTAL = "message:auth:total"; + /** + * 订阅次数 + */ + String MESSAGE_SUBSCRIBE_TOTAL = "message:subscribe:total"; + + /** + * 今日接收消息 + */ + String MESSAGE_RECEIVE_TODAY = "message:receive:today"; + /** + * 今日发送消息 + */ + String MESSAGE_SEND_TODAY = "message:send:today"; + + + // 物模型值命名空间:Key:TSLV:{productId}_{deviceNumber} HKey:{identity#V/identity#S/identity#M/identity#N} + /** + * v-值 + * s-影子值 + * m-是否为检测值 + * n-名称 + */ + String DEVICE_PRE_KEY = "TSLV:"; + + // 物模型命名空间:Key:TSL:{productId} + String TSL_PRE_KEY = "TSL:"; + + /** + * modbus缓存指令 + */ + String POLL_MODBUS_KEY = "poll:modbus"; + + /** + * 通知企业微信应用消息accessToken缓存key + */ + String NOTIFY_WECOM_APPLY_ACCESSTOKEN = "notify:wecom:apply:"; + + + } + + interface TOPIC { + /*属性上报*/ + String PROP = "properties"; + //事件 + String EVENT = "event"; + //功能 + String FUNCTION = "functions"; + /*非OTA消息回复*/ + String MSG_REPLY = "message/reply"; + /*OTA升级回复*/ + String UPGRADE_REPLY = "upgrade/reply"; + /*网关子设备结尾*/ + String SUB = "/sub"; + + /** + * 设备状态 + */ + String DEVICE_STATUS = "EVENT_DEVICE_STATUS"; + + String DEVICE_REPORT_TOPIC = "DEVICE-REPORT-TOPIC"; + + + + } + + interface PROTOCOL { + String ModbusRtu = "MODBUS-RTU"; + String YinErDa = "YinErDa"; + String JsonObject = "JSONOBJECT"; + String JsonArray = "JSON"; + String ModbusRtuPak = "MODBUS-RTU-PAK"; + String FlowMeter = "FlowMeter"; + String RJ45 = "RJ45"; + String ModbusToJson = "MODBUS-JSON"; + String ModbusToJsonHP = "MODBUS-JSON-HP"; + String ModbusToJsonZQWL = "MODBUS-JSON-ZQWL"; + String JsonObject_ChenYi = "JSONOBJECT-CHENYI"; + String GEC6100D = "MODBUS-JSON-GEC6100D"; + String SGZ = "SGZ"; + String CH = "CH"; + + /** + * 212协议 + */ + String HJ212 = "HJ212"; + + + } + + interface URL { + /** + * 微信小程序订阅消息推送url前缀 + */ + String WX_MINI_PROGRAM_PUSH_URL_PREFIX = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send"; + /** + * 微信网站、移动应用登录获取用户access_token + */ + String WX_GET_ACCESS_TOKEN_URL_PREFIX = "https://api.weixin.qq.com/sns/oauth2/access_token"; + /** + * 微信小程序登录获取用户会话参数 + */ + String WX_MINI_PROGRAM_GET_USER_SESSION_URL_PREFIX = "https://api.weixin.qq.com/sns/jscode2session"; + /** + * 微信小程序、公众号获取access_token + */ + String WX_MINI_PROGRAM_GET_ACCESS_TOKEN_URL_PREFIX = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"; + /** + * 微信获取用户信息 + */ + String WX_GET_USER_INFO_URL_PREFIX = "https://api.weixin.qq.com/sns/userinfo"; + /** + * 获取用户手机号信息 + */ + String WX_GET_USER_PHONE_URL_PREFIX = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token="; + /** + * 企业微信获取accessToken + */ + String WECOM_GET_ACCESSTOKEN = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"; + /** + * 企业微信发送应用消息 + */ + String WECOM_APPLY_SEND = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="; + /** + * 微信公众号获取用户信息 + */ + String WX_PUBLIC_ACCOUNT_GET_USER_INFO_URL_PREFIX = "https://api.weixin.qq.com/cgi-bin/user/info"; + /** + * 微信公众号发送模版消息 + */ + String WX_PUBLIC_ACCOUNT_TEMPLATE_SEND_URL_PREFIX = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token="; + } + + interface TRANSPORT { + + String HTTP = "HTTP"; + + /** + * 华智视频传输协议 + */ + String HTTP_HZ = "HTTP_HZ"; + + /** + * 高空瞭望视频传输协议 + */ + String HTTP_HIGH_OBS = "HTTP_HIGH_OBS"; + + String TCP = "TCP"; + + } + + interface SUFFIX { + + /** + * 华智视频监控后缀 + */ + String HZ = "_HZ"; + + /** + * 高空瞭望后缀 + */ + String HIGH_OBS = "_HIGH_OBS"; + + } + + interface DataType { + String TYPE_INTEGER = "integer"; + String TYPE_DECIMAL = "decimal"; + String TYPE_STRING = "string"; + String TYPE_BOOL = "bool"; + String TYPE_ARRAY = "array"; + String TYPE_ENUM = "enum"; + String TYPE_OBJECT = "object"; + } + + interface ModelType { + /** + * 属性 + */ + int PROPERTY = 1; + + /** + * 功能 + */ + int FUNCTION = 2; + + /** + * 事件 + */ + int EVENT = 3; + } + + + interface BlackCarEvent { + + /** + * 交通流量 + */ + String JTLL = "BLACKCAR_JTLL"; + + /** + * 黑烟车 + */ + String HYC = "BLACKCAR_HYC"; + + /** + * 车流量 + */ + String CLL = "BLACKCAR_CLL"; + + /** + * 摄像头信息 + */ + String SXTXX = "BLACKCAR_SXTXX"; + } + + + interface KaCheckEvent { + + /** + * 交通流量 + */ + String JTLL = "KACHECK_JTLL"; + + /** + * 车流量 + */ + String CLL = "KACHECK_CLL"; + + /** + * 摄像头信息 + */ + String SXTXX = "KACHECK_SXTXX"; + } + + interface FumesEvent { + + /** + * 消息管理/报警信息 + */ + String ALARM_MSG = "FUMES_ALARMMSG"; + + /** + * 报警管理-监测 + */ + String DETECTOR = "FUMES_DETECTOR"; + + + /** + * 十分钟设备数据 + */ + String TEN_DATA = "FUMES_TENDATA"; + + /** + * 一分钟数据 + */ + String ONE_DATA = "FUMES_ONEDATA"; + + + /** + * 点位事件 + */ + String POINT_EVENT = "POINTEVENT"; + + /** + * 监测点位减排统计 + */ + String REDUCE_EVENT = "REDUCE_EVENT"; + + /** + * 实时数据 + */ + String REAL_TIME_EVENT = "REAL_TIME_EVENT"; + + } + interface FumesBis { + + String INTERFACE_PREFIX = "JK_"; + } + + interface VideoEvent { + String HIGH_OBS_FIRE_EVENT = "HIGH_OBS_FIRE_EVENT"; + String HIGH_OBS_PLAY_BACK_EVENT = "HIGH_OBS_PLAY_BACK_EVENT"; + String VIDEO_MONITOR_PREVIEW_EVENT = "VIDEO_MONITOR_PREVIEW_EVENT"; + String HIGH_OBS_PREVIEW_EVENT = "HIGH_OBS_PREVIEW_EVENT"; + String HIGH_OBS_JUDGE_EVENT = "HIGH_OBS_JUDGE_EVENT"; + } + + interface HJ212 { + + + /** + * 请求编号 QN + * QN=yyyyMMddHHmmssSSS 取当前系统时间,精确到毫秒值,用来唯一标识一次命令交互。 + * 例如:20240816150009167 + */ + String QN_KEY = "QN"; + + String CP_KEY = "CP"; + + /** + * 命令编码 + */ + String CN_KEY = "CN"; + /** + * 分钟数据 + */ + String CN_2051 = "2051"; + + /** + * 小时数据 + */ + String CN_2061 = "2061"; + + /** + * 日数据 + */ + String CN_2031 = "2031"; + } + + interface VideoProtocol { + String HLS = "hls"; + + String RTSP = "rtsp"; + } + + interface Heartbeat { + + String PREFIX = "7e81"; + + String SUFFIX = "7e"; + } + + interface DataBase { + + /** + * 数据时间 + */ + String DATA_TIME = "data_time"; + + /** + * 创建时间 + */ + String CREATE_TIME = "create_time"; + + /** + * 设备编号 + */ + String SERIAL_NUMBER = "serial_number"; + + + } + + interface TaskStatus { + /** + * 执行失败 + */ + int FAIL = -1; + + /** + * 执行中 + */ + int PROCESSING = 1; + + /** + * 执行完成 + */ + int COMPLETE = 2; + + } + + interface ProcessType { + + /** + * 属性 + */ + int property = 1; + + /** + * 事件 + */ + int event = 2; + + + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/constant/CacheConstants.java b/bnhz-common/src/main/java/com/bnhz/common/constant/CacheConstants.java new file mode 100644 index 0000000..9be2b78 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/constant/CacheConstants.java @@ -0,0 +1,90 @@ +package com.bnhz.common.constant; + +/** + * 缓存的key 常量 + * + * @author ruoyi + */ +public class CacheConstants +{ + /** + * 登录用户 redis key + */ + public static final String LOGIN_TOKEN_KEY = "login_tokens:"; + + /** + * 登录用户 redis key + */ + public static final String LOGIN_USERID_KEY = "login_userId:"; + + /** + * 验证码 redis key + */ + public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; + + /** + * 参数管理 cache key + */ + public static final String SYS_CONFIG_KEY = "sys_config:"; + + /** + * 字典管理 cache key + */ + public static final String SYS_DICT_KEY = "sys_dict:"; + + /** + * 防重提交 redis key + */ + public static final String REPEAT_SUBMIT_KEY = "repeat_submit:"; + + /** + * 限流 redis key + */ + public static final String RATE_LIMIT_KEY = "rate_limit:"; + + /** + * 登录账户密码错误次数 redis key + */ + public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:"; + + /** + * 短信登录验证码 redis key + */ + public static final String LOGIN_SMS_CAPTCHA_PHONE = "login_sms_captcha_phone:"; + + /** + * 微信获取accessToken redis key + */ + public static final String WECHAT_GET_ACCESS_TOKEN_APPID = "wechat_get_accessToken:"; + + public static final String HUAZHI_VIDEO_TOKEN = "huzhi_token:"; + + + public static final String FUMES_TOKEN = "fumes_token:"; + + + /** + * 上一次获取报警信息时间 + */ + public static final String FUMES_ALARM_LAST_TIME = "fumes:lastAlarmTime"; + + /** + * 上一次一分钟数据获取时间 + */ + public static final String FUMES_ONE_LAST_TIME = "fumes:lastOneTime"; + + /** + * 上一次十分钟数据 + */ + public static final String FUMES_TEN_LAST_TIME = "fumes:lastTenDataTime"; + + /** + * 最新点位事件时间戳 + */ + public static final String FUMES_POINT_EVENT_LAST_TIME = "fumes:lastPointEventTime"; + + /** + * 实时数据时间 + */ + public static final String FUMES_REAL_TIME = "fumes:lastRealTime"; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/constant/Constants.java b/bnhz-common/src/main/java/com/bnhz/common/constant/Constants.java new file mode 100644 index 0000000..e3e59ec --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/constant/Constants.java @@ -0,0 +1,142 @@ +package com.bnhz.common.constant; + +import io.jsonwebtoken.Claims; + +/** + * 通用常量信息 + * + * @author ruoyi + */ +public class Constants +{ + /** + * UTF-8 字符集 + */ + public static final String UTF8 = "UTF-8"; + + /** + * GBK 字符集 + */ + public static final String GBK = "GBK"; + + /** + * www主域 + */ + public static final String WWW = "www."; + + /** + * http请求 + */ + public static final String HTTP = "http://"; + + /** + * https请求 + */ + public static final String HTTPS = "https://"; + + /** + * 通用成功标识 + */ + public static final String SUCCESS = "0"; + + /** + * 通用失败标识 + */ + public static final String FAIL = "1"; + + /** + * 登录成功 + */ + public static final String LOGIN_SUCCESS = "Success"; + + /** + * 注销 + */ + public static final String LOGOUT = "Logout"; + + /** + * 注册 + */ + public static final String REGISTER = "Register"; + + /** + * 登录失败 + */ + public static final String LOGIN_FAIL = "Error"; + + /** + * 验证码有效期(分钟) + */ + public static final Integer CAPTCHA_EXPIRATION = 2; + + /** + * 令牌 + */ + public static final String TOKEN = "token"; + + /** + * 令牌前缀 + */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** + * 令牌前缀 + */ + public static final String LOGIN_USER_KEY = "login_user_key"; + + /** + * 用户ID + */ + public static final String JWT_USERID = "userid"; + + /** + * 用户名称 + */ + public static final String JWT_USERNAME = Claims.SUBJECT; + + /** + * 用户头像 + */ + public static final String JWT_AVATAR = "avatar"; + + /** + * 创建时间 + */ + public static final String JWT_CREATED = "created"; + + /** + * 用户权限 + */ + public static final String JWT_AUTHORITIES = "authorities"; + + /** + * 资源映射路径 前缀 + */ + public static final String RESOURCE_PREFIX = "/profile"; + + /** + * RMI 远程方法调用 + */ + public static final String LOOKUP_RMI = "rmi:"; + + /** + * LDAP 远程方法调用 + */ + public static final String LOOKUP_LDAP = "ldap:"; + + /** + * LDAPS 远程方法调用 + */ + public static final String LOOKUP_LDAPS = "ldaps:"; + + /** + * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) + */ + public static final String[] JOB_WHITELIST_STR = { "com.bnhz" }; + + /** + * 定时任务违规的字符 + */ + public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml", + "org.springframework", "org.apache", "com.bnhz.common.utils.file", "com.bnhz.common.config" }; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/constant/GenConstants.java b/bnhz-common/src/main/java/com/bnhz/common/constant/GenConstants.java new file mode 100644 index 0000000..a11b295 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/constant/GenConstants.java @@ -0,0 +1,117 @@ +package com.bnhz.common.constant; + +/** + * 代码生成通用常量 + * + * @author ruoyi + */ +public class GenConstants +{ + /** 单表(增删改查) */ + public static final String TPL_CRUD = "crud"; + + /** 树表(增删改查) */ + public static final String TPL_TREE = "tree"; + + /** 主子表(增删改查) */ + public static final String TPL_SUB = "sub"; + + /** 树编码字段 */ + public static final String TREE_CODE = "treeCode"; + + /** 树父编码字段 */ + public static final String TREE_PARENT_CODE = "treeParentCode"; + + /** 树名称字段 */ + public static final String TREE_NAME = "treeName"; + + /** 上级菜单ID字段 */ + public static final String PARENT_MENU_ID = "parentMenuId"; + + /** 上级菜单名称字段 */ + public static final String PARENT_MENU_NAME = "parentMenuName"; + + /** 数据库字符串类型 */ + public static final String[] COLUMNTYPE_STR = { "char", "varchar", "nvarchar", "varchar2" }; + + /** 数据库文本类型 */ + public static final String[] COLUMNTYPE_TEXT = { "tinytext", "text", "mediumtext", "longtext" }; + + /** 数据库时间类型 */ + public static final String[] COLUMNTYPE_TIME = { "datetime", "time", "date", "timestamp" }; + + /** 数据库数字类型 */ + public static final String[] COLUMNTYPE_NUMBER = { "tinyint", "smallint", "mediumint", "int", "number", "integer", + "bit", "bigint", "float", "double", "decimal" }; + + /** 页面不需要编辑字段 */ + public static final String[] COLUMNNAME_NOT_EDIT = { "id", "create_by", "create_time", "del_flag" }; + + /** 页面不需要显示的列表字段 */ + public static final String[] COLUMNNAME_NOT_LIST = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time" }; + + /** 页面不需要查询字段 */ + public static final String[] COLUMNNAME_NOT_QUERY = { "id", "create_by", "create_time", "del_flag", "update_by", + "update_time", "remark" }; + + /** Entity基类字段 */ + public static final String[] BASE_ENTITY = { "createBy", "createTime", "updateBy", "updateTime", "remark" }; + + /** Tree基类字段 */ + public static final String[] TREE_ENTITY = { "parentName", "parentId", "orderNum", "ancestors", "children" }; + + /** 文本框 */ + public static final String HTML_INPUT = "input"; + + /** 文本域 */ + public static final String HTML_TEXTAREA = "textarea"; + + /** 下拉框 */ + public static final String HTML_SELECT = "select"; + + /** 单选框 */ + public static final String HTML_RADIO = "radio"; + + /** 复选框 */ + public static final String HTML_CHECKBOX = "checkbox"; + + /** 日期控件 */ + public static final String HTML_DATETIME = "datetime"; + + /** 图片上传控件 */ + public static final String HTML_IMAGE_UPLOAD = "imageUpload"; + + /** 文件上传控件 */ + public static final String HTML_FILE_UPLOAD = "fileUpload"; + + /** 富文本控件 */ + public static final String HTML_EDITOR = "editor"; + + /** 字符串类型 */ + public static final String TYPE_STRING = "String"; + + /** 整型 */ + public static final String TYPE_INTEGER = "Integer"; + + /** 长整型 */ + public static final String TYPE_LONG = "Long"; + + /** 浮点型 */ + public static final String TYPE_DOUBLE = "Double"; + + /** 高精度计算类型 */ + public static final String TYPE_BIGDECIMAL = "BigDecimal"; + + /** 时间类型 */ + public static final String TYPE_DATE = "Date"; + + /** 模糊查询 */ + public static final String QUERY_LIKE = "LIKE"; + + /** 相等查询 */ + public static final String QUERY_EQ = "EQ"; + + /** 需要 */ + public static final String REQUIRE = "1"; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/constant/HttpStatus.java b/bnhz-common/src/main/java/com/bnhz/common/constant/HttpStatus.java new file mode 100644 index 0000000..28adae6 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/constant/HttpStatus.java @@ -0,0 +1,105 @@ +package com.bnhz.common.constant; + +/** + * 返回状态码 + * + * @author ruoyi + */ +public class HttpStatus +{ + /** + * 操作成功 + */ + public static final int SUCCESS = 200; + + /** + * 对象创建成功 + */ + public static final int CREATED = 201; + + /** + * 请求已经被接受 + */ + public static final int ACCEPTED = 202; + + /** + * 操作已经执行成功,但是没有返回数据 + */ + public static final int NO_CONTENT = 204; + + /** + * 资源已被移除 + */ + public static final int MOVED_PERM = 301; + + /** + * 重定向 + */ + public static final int SEE_OTHER = 303; + + /** + * 资源没有被修改 + */ + public static final int NOT_MODIFIED = 304; + + /** + * 参数列表错误(缺少,格式不匹配) + */ + public static final int BAD_REQUEST = 400; + + /** + * 未授权 + */ + public static final int UNAUTHORIZED = 401; + + /** + * 访问受限,授权过期 + */ + public static final int FORBIDDEN = 403; + + /** + * 资源,服务未找到 + */ + public static final int NOT_FOUND = 404; + + /** + * 不允许的http方法 + */ + public static final int BAD_METHOD = 405; + + /** + * 资源冲突,或者资源被锁 + */ + public static final int CONFLICT = 409; + + /** + * 不支持的数据,媒体类型 + */ + public static final int UNSUPPORTED_TYPE = 415; + + /** + * 用户不存在 + */ + public static final int USER_NO_EXIST = 450; + + /** + * 系统内部错误 + */ + public static final int ERROR = 500; + + /** + * 接口未实现 + */ + public static final int NOT_IMPLEMENTED = 501; + + /** + * 不弹窗显示 + */ + public static final int NO_MESSAGE_ALERT = 502; + + + /** + * 系统警告消息 + */ + public static final int WARN = 601; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/constant/ProductAuthConstant.java b/bnhz-common/src/main/java/com/bnhz/common/constant/ProductAuthConstant.java new file mode 100644 index 0000000..3ead077 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/constant/ProductAuthConstant.java @@ -0,0 +1,41 @@ +package com.bnhz.common.constant; + +/** + * + * @author fastb + * @date 2023-08-03 10:20 + */ +public class ProductAuthConstant { + + /** + * 产品设备认证方式-简单认证 + */ + public static final Integer AUTH_WAY_SIMPLE = 1; + /** + * 产品设备认证方式-简单认证 + */ + public static final Integer AUTH_WAY_ENCRYPT = 2; + /** + * 产品设备认证方式-简单认证 + */ + public static final Integer AUTH_WAY_SIMPLE_AND_ENCRYPT = 3; + + /** + * 产品设备客户端ID认证类型-简单认证 + */ + public static final String CLIENT_ID_AUTH_TYPE_SIMPLE = "S"; + + /** + * 产品设备客户端ID认证类型-简单认证 + */ + public static final String CLIENT_ID_AUTH_TYPE_ENCRYPT = "E"; + /** + * 设备授权 + */ + public static final Integer AUTHORIZE = 1; + /** + * 设备没有授权 + */ + public static final Integer NO_AUTHORIZE = 1; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/constant/ScadaConstant.java b/bnhz-common/src/main/java/com/bnhz/common/constant/ScadaConstant.java new file mode 100644 index 0000000..cc2f1ed --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/constant/ScadaConstant.java @@ -0,0 +1,14 @@ +package com.bnhz.common.constant; + +/** + * @author fastb + * @version 1.0 + * @description: 组态常量类 + * @date 2024-01-02 14:40 + */ +public class ScadaConstant { + + public static final String COMPONENT_TEMPLATE_DEFAULT = "
\n

自定义组件案例

\n

支持element ui、样式自定义、vue的语法等

\n 点击按钮\n
"; + public static final String COMPONENT_SCRIPT_DEFAULT = "export default {\n data() {\n return {}\n },\n created() {\n\n },\n mounted(){\n\n },\n methods:{\n handleClick(){\n this.$message('这是一条消息提示');\n }\n }\n}"; + public static final String COMPONENT_STYLE_DEFAULT = "h2 {\n color:#409EFF\n}\n\nh4 {\n color:#F56C6C\n}"; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/constant/ScheduleConstants.java b/bnhz-common/src/main/java/com/bnhz/common/constant/ScheduleConstants.java new file mode 100644 index 0000000..012ee15 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/constant/ScheduleConstants.java @@ -0,0 +1,50 @@ +package com.bnhz.common.constant; + +/** + * 任务调度通用常量 + * + * @author ruoyi + */ +public class ScheduleConstants +{ + public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME"; + + /** 执行目标key */ + public static final String TASK_PROPERTIES = "TASK_PROPERTIES"; + + /** 默认 */ + public static final String MISFIRE_DEFAULT = "0"; + + /** 立即触发执行 */ + public static final String MISFIRE_IGNORE_MISFIRES = "1"; + + /** 触发一次执行 */ + public static final String MISFIRE_FIRE_AND_PROCEED = "2"; + + /** 不触发立即执行 */ + public static final String MISFIRE_DO_NOTHING = "3"; + + public enum Status + { + /** + * 正常 + */ + NORMAL("0"), + /** + * 暂停 + */ + PAUSE("1"); + + private String value; + + private Status(String value) + { + this.value = value; + } + + public String getValue() + { + return value; + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/constant/SipConstants.java b/bnhz-common/src/main/java/com/bnhz/common/constant/SipConstants.java new file mode 100644 index 0000000..9249866 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/constant/SipConstants.java @@ -0,0 +1,9 @@ +package com.bnhz.common.constant; + +public class SipConstants { + public static final String MESSAGE_CATALOG = "Catalog"; + public static final String MESSAGE_KEEP_ALIVE = "Keepalive"; + public static final String MESSAGE_DEVICE_INFO = "DeviceInfo"; + public static final String MESSAGE_RECORD_INFO = "RecordInfo"; + public static final String MESSAGE_MEDIA_STATUS = "MediaStatus"; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/constant/TDengineReservedWord.java b/bnhz-common/src/main/java/com/bnhz/common/constant/TDengineReservedWord.java new file mode 100644 index 0000000..a1d012e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/constant/TDengineReservedWord.java @@ -0,0 +1,289 @@ +package com.bnhz.common.constant; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author Leo + * @date 2024/7/9 16:22 + */ +public class TDengineReservedWord { + + public static Set RESERVED_WORD = new HashSet<>(); + static { + + RESERVED_WORD.add("ABORT"); + RESERVED_WORD.add("ACCOUNT"); + RESERVED_WORD.add("ACCOUNTS"); + RESERVED_WORD.add("ADD"); + RESERVED_WORD.add("AFTER"); + RESERVED_WORD.add("AGGREGATE"); + RESERVED_WORD.add("ALIVE"); + RESERVED_WORD.add("ALL"); + RESERVED_WORD.add("ALTER"); + RESERVED_WORD.add("ANALYZE"); + RESERVED_WORD.add("AND"); + RESERVED_WORD.add("APPS"); + RESERVED_WORD.add("AS"); + RESERVED_WORD.add("ASC"); + RESERVED_WORD.add("AT_ONCE"); + RESERVED_WORD.add("ATTACH"); + RESERVED_WORD.add("BALANCE"); + RESERVED_WORD.add("BEFORE"); + RESERVED_WORD.add("BEGIN"); + RESERVED_WORD.add("BETWEEN"); + RESERVED_WORD.add("BIGINT"); + RESERVED_WORD.add("BINARY"); + RESERVED_WORD.add("BITAND"); + RESERVED_WORD.add("BITNOT"); + RESERVED_WORD.add("BITOR"); + RESERVED_WORD.add("BLOCKS"); + RESERVED_WORD.add("BNODE"); + RESERVED_WORD.add("BNODES"); + RESERVED_WORD.add("BOOL"); + RESERVED_WORD.add("BUFFER"); + RESERVED_WORD.add("BUFSIZE"); + RESERVED_WORD.add("BY"); + RESERVED_WORD.add("CACHE"); + RESERVED_WORD.add("CACHEMODEL"); + RESERVED_WORD.add("CACHESIZE"); + RESERVED_WORD.add("CASCADE"); + RESERVED_WORD.add("CAST"); + RESERVED_WORD.add("CHANGE"); + RESERVED_WORD.add("CLIENT_VERSION"); + RESERVED_WORD.add("CLUSTER"); + RESERVED_WORD.add("COLON"); + RESERVED_WORD.add("COLUMN"); + RESERVED_WORD.add("COMMA"); + RESERVED_WORD.add("COMMENT"); + RESERVED_WORD.add("COMP"); + RESERVED_WORD.add("COMPACT"); + RESERVED_WORD.add("CONCAT"); + RESERVED_WORD.add("CONFLICT"); + RESERVED_WORD.add("CONNECTION"); + RESERVED_WORD.add("CONNECTIONS"); + RESERVED_WORD.add("CONNS"); + RESERVED_WORD.add("CONSUMER"); + RESERVED_WORD.add("CONSUMERS"); + RESERVED_WORD.add("CONTAINS"); + RESERVED_WORD.add("COPY"); + RESERVED_WORD.add("COUNT"); + RESERVED_WORD.add("CREATE"); + RESERVED_WORD.add("CURRENT_USER"); + RESERVED_WORD.add("DATABASE"); + RESERVED_WORD.add("DATABASES"); + RESERVED_WORD.add("DBS"); + RESERVED_WORD.add("DEFERRED"); + RESERVED_WORD.add("DELETE"); + RESERVED_WORD.add("DELIMITERS"); + RESERVED_WORD.add("DESC"); + RESERVED_WORD.add("DESCRIBE"); + RESERVED_WORD.add("DETACH"); + RESERVED_WORD.add("DISTINCT"); + RESERVED_WORD.add("DISTRIBUTED"); + RESERVED_WORD.add("DIVIDE"); + RESERVED_WORD.add("DNODE"); + RESERVED_WORD.add("DNODES"); + RESERVED_WORD.add("DOT"); + RESERVED_WORD.add("DOUBLE"); + RESERVED_WORD.add("DROP"); + RESERVED_WORD.add("DURATION"); + RESERVED_WORD.add("EACH"); + RESERVED_WORD.add("ENABLE"); + RESERVED_WORD.add("END"); + RESERVED_WORD.add("EVERY"); + RESERVED_WORD.add("EXISTS"); + RESERVED_WORD.add("EXPIRED"); + RESERVED_WORD.add("EXPLAIN"); + RESERVED_WORD.add("FAIL"); + RESERVED_WORD.add("FILE"); + RESERVED_WORD.add("FILL"); + RESERVED_WORD.add("FIRST"); + RESERVED_WORD.add("FLOAT"); + RESERVED_WORD.add("FLUSH"); + RESERVED_WORD.add("FOR"); + RESERVED_WORD.add("FROM"); + RESERVED_WORD.add("FUNCTION"); + RESERVED_WORD.add("FUNCTIONS"); + RESERVED_WORD.add("GLOB"); + RESERVED_WORD.add("GRANT"); + RESERVED_WORD.add("GRANTS"); + RESERVED_WORD.add("GROUP"); + RESERVED_WORD.add("HAVING"); + RESERVED_WORD.add("ID"); + RESERVED_WORD.add("IF"); + RESERVED_WORD.add("IGNORE"); + RESERVED_WORD.add("IMMEDIATE"); + RESERVED_WORD.add("IMPORT"); + RESERVED_WORD.add("IN"); + RESERVED_WORD.add("INDEX"); + RESERVED_WORD.add("INDEXES"); + RESERVED_WORD.add("INITIALLY"); + RESERVED_WORD.add("INNER"); + RESERVED_WORD.add("INSERT"); + RESERVED_WORD.add("INSTEAD"); + RESERVED_WORD.add("INT"); + RESERVED_WORD.add("INTEGER"); + RESERVED_WORD.add("INTERVAL"); + RESERVED_WORD.add("INTO"); + RESERVED_WORD.add("IS"); + RESERVED_WORD.add("IS NULL"); + RESERVED_WORD.add("JOIN"); + RESERVED_WORD.add("JSON"); + RESERVED_WORD.add("KEEP"); + RESERVED_WORD.add("KEY"); + RESERVED_WORD.add("KILL"); + RESERVED_WORD.add("LAST"); + RESERVED_WORD.add("LAST_ROW"); + RESERVED_WORD.add("LICENCES"); + RESERVED_WORD.add("LIKE"); + RESERVED_WORD.add("LIMIT"); + RESERVED_WORD.add("LINEAR"); + RESERVED_WORD.add("LOCAL"); + RESERVED_WORD.add("MATCH"); + RESERVED_WORD.add("MAX_DELAY"); + RESERVED_WORD.add("BWLIMIT"); + RESERVED_WORD.add("MAXROWS"); + RESERVED_WORD.add("MAX_SPEED"); + RESERVED_WORD.add("MERGE"); + RESERVED_WORD.add("META"); + RESERVED_WORD.add("MINROWS"); + RESERVED_WORD.add("MINUS"); + RESERVED_WORD.add("MNODE"); + RESERVED_WORD.add("MNODES"); + RESERVED_WORD.add("MODIFY"); + RESERVED_WORD.add("MODULES"); + RESERVED_WORD.add("NCHAR"); + RESERVED_WORD.add("NEXT"); + RESERVED_WORD.add("NMATCH"); + RESERVED_WORD.add("NONE"); + RESERVED_WORD.add("NOT"); + RESERVED_WORD.add("NOT NULL"); + RESERVED_WORD.add("NOW"); + RESERVED_WORD.add("NULL"); + RESERVED_WORD.add("NULLS"); + RESERVED_WORD.add("OF"); + RESERVED_WORD.add("OFFSET"); + RESERVED_WORD.add("ON"); + RESERVED_WORD.add("OR"); + RESERVED_WORD.add("ORDER"); + RESERVED_WORD.add("OUTPUTTYPE"); + RESERVED_WORD.add("PAGES"); + RESERVED_WORD.add("PAGESIZE"); + RESERVED_WORD.add("PARTITIONS"); + RESERVED_WORD.add("PASS"); + RESERVED_WORD.add("PLUS"); + RESERVED_WORD.add("PORT"); + RESERVED_WORD.add("PPS"); + RESERVED_WORD.add("PRECISION"); + RESERVED_WORD.add("PREV"); + RESERVED_WORD.add("PRIVILEGE"); + RESERVED_WORD.add("QNODE"); + RESERVED_WORD.add("QNODES"); + RESERVED_WORD.add("QTIME"); + RESERVED_WORD.add("QUERIES"); + RESERVED_WORD.add("QUERY"); + RESERVED_WORD.add("RAISE"); + RESERVED_WORD.add("RANGE"); + RESERVED_WORD.add("RATIO"); + RESERVED_WORD.add("READ"); + RESERVED_WORD.add("REDISTRIBUTE"); + RESERVED_WORD.add("RENAME"); + RESERVED_WORD.add("REPLACE"); + RESERVED_WORD.add("REPLICA"); + RESERVED_WORD.add("RESET"); + RESERVED_WORD.add("RESTRICT"); + RESERVED_WORD.add("RETENTIONS"); + RESERVED_WORD.add("REVOKE"); + RESERVED_WORD.add("ROLLUP"); + RESERVED_WORD.add("ROW"); + RESERVED_WORD.add("SCHEMALESS"); + RESERVED_WORD.add("SCORES"); + RESERVED_WORD.add("SELECT"); + RESERVED_WORD.add("SEMI"); + RESERVED_WORD.add("SERVER_STATUS"); + RESERVED_WORD.add("SERVER_VERSION"); + RESERVED_WORD.add("SESSION"); + RESERVED_WORD.add("SET"); + RESERVED_WORD.add("SHOW"); + RESERVED_WORD.add("SINGLE_STABLE"); + RESERVED_WORD.add("SLIDING"); + RESERVED_WORD.add("SLIMIT"); + RESERVED_WORD.add("SMA"); + RESERVED_WORD.add("SMALLINT"); + RESERVED_WORD.add("SNODE"); + RESERVED_WORD.add("SNODES"); + RESERVED_WORD.add("SOFFSET"); + RESERVED_WORD.add("SPLIT"); + RESERVED_WORD.add("STABLE"); + RESERVED_WORD.add("STABLES"); + RESERVED_WORD.add("START"); + RESERVED_WORD.add("STATE"); + RESERVED_WORD.add("STATE_WINDOW"); + RESERVED_WORD.add("STATEMENT"); + RESERVED_WORD.add("STORAGE"); + RESERVED_WORD.add("STREAM"); + RESERVED_WORD.add("STREAMS"); + RESERVED_WORD.add("STRICT"); + RESERVED_WORD.add("STRING"); + RESERVED_WORD.add("SUBSCRIPTIONS"); + RESERVED_WORD.add("SYNCDB"); + RESERVED_WORD.add("SYSINFO"); + RESERVED_WORD.add("TABLE"); + RESERVED_WORD.add("TABLES"); + RESERVED_WORD.add("TAG"); + RESERVED_WORD.add("TAGS"); + RESERVED_WORD.add("TBNAME"); + RESERVED_WORD.add("TIMES"); + RESERVED_WORD.add("TIMESTAMP"); + RESERVED_WORD.add("TIMEZONE"); + RESERVED_WORD.add("TINYINT"); + RESERVED_WORD.add("TO"); + RESERVED_WORD.add("TODAY"); + RESERVED_WORD.add("TOPIC"); + RESERVED_WORD.add("TOPICS"); + RESERVED_WORD.add("TRANSACTION"); + RESERVED_WORD.add("TRANSACTIONS"); + RESERVED_WORD.add("TRIGGER"); + RESERVED_WORD.add("TRIM"); + RESERVED_WORD.add("TSERIES"); + RESERVED_WORD.add("TTL"); + RESERVED_WORD.add("UNION"); + RESERVED_WORD.add("UNSIGNED"); + RESERVED_WORD.add("UPDATE"); + RESERVED_WORD.add("USE"); + RESERVED_WORD.add("USER"); + RESERVED_WORD.add("USERS"); + RESERVED_WORD.add("USING"); + RESERVED_WORD.add("VALUE"); + RESERVED_WORD.add("VALUES"); + RESERVED_WORD.add("VARCHAR"); + RESERVED_WORD.add("VARIABLE"); + RESERVED_WORD.add("VARIABLES"); + RESERVED_WORD.add("VERBOSE"); + RESERVED_WORD.add("VGROUP"); + RESERVED_WORD.add("VGROUPS"); + RESERVED_WORD.add("VIEW"); + RESERVED_WORD.add("VNODES"); + RESERVED_WORD.add("WAL"); + RESERVED_WORD.add("WAL_FSYNC_PERIOD"); + RESERVED_WORD.add("WAL_LEVEL"); + RESERVED_WORD.add("WAL_RETENTION_PERIOD"); + RESERVED_WORD.add("WAL_RETENTION_SIZE"); + RESERVED_WORD.add("WATERMARK"); + RESERVED_WORD.add("WHERE"); + RESERVED_WORD.add("WINDOW_CLOSE"); + RESERVED_WORD.add("WITH"); + RESERVED_WORD.add("WRITE"); + RESERVED_WORD.add("_C0"); + RESERVED_WORD.add("_IROWTS"); + RESERVED_WORD.add("_QDURATION"); + RESERVED_WORD.add("_QEND"); + RESERVED_WORD.add("_QSTART"); + RESERVED_WORD.add("_ROWTS"); + RESERVED_WORD.add("_WDURATION"); + RESERVED_WORD.add("_WEND"); + RESERVED_WORD.add("_WSTART"); + + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/constant/UserConstants.java b/bnhz-common/src/main/java/com/bnhz/common/constant/UserConstants.java new file mode 100644 index 0000000..462c5de --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/constant/UserConstants.java @@ -0,0 +1,78 @@ +package com.bnhz.common.constant; + +/** + * 用户常量信息 + * + * @author ruoyi + */ +public class UserConstants +{ + /** + * 平台内系统用户的唯一标志 + */ + public static final String SYS_USER = "SYS_USER"; + + /** 正常状态 */ + public static final String NORMAL = "0"; + + /** 异常状态 */ + public static final String EXCEPTION = "1"; + + /** 用户封禁状态 */ + public static final String USER_DISABLE = "1"; + + /** 角色封禁状态 */ + public static final String ROLE_DISABLE = "1"; + + /** 部门正常状态 */ + public static final String DEPT_NORMAL = "0"; + + /** 部门停用状态 */ + public static final String DEPT_DISABLE = "1"; + + /** 字典正常状态 */ + public static final String DICT_NORMAL = "0"; + + /** 是否为系统默认(是) */ + public static final String YES = "Y"; + + /** 是否菜单外链(是) */ + public static final String YES_FRAME = "0"; + + /** 是否菜单外链(否) */ + public static final String NO_FRAME = "1"; + + /** 菜单类型(目录) */ + public static final String TYPE_DIR = "M"; + + /** 菜单类型(菜单) */ + public static final String TYPE_MENU = "C"; + + /** 菜单类型(按钮) */ + public static final String TYPE_BUTTON = "F"; + + /** Layout组件标识 */ + public final static String LAYOUT = "Layout"; + + /** ParentView组件标识 */ + public final static String PARENT_VIEW = "ParentView"; + + /** InnerLink组件标识 */ + public final static String INNER_LINK = "InnerLink"; + + /** 校验返回结果码 */ + public final static String UNIQUE = "0"; + public final static String NOT_UNIQUE = "1"; + + /** + * 用户名长度限制 + */ + public static final int USERNAME_MIN_LENGTH = 2; + public static final int USERNAME_MAX_LENGTH = 20; + + /** + * 密码长度限制 + */ + public static final int PASSWORD_MIN_LENGTH = 5; + public static final int PASSWORD_MAX_LENGTH = 20; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/controller/BaseController.java b/bnhz-common/src/main/java/com/bnhz/common/core/controller/BaseController.java new file mode 100644 index 0000000..b98830f --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/controller/BaseController.java @@ -0,0 +1,216 @@ +package com.bnhz.common.core.controller; + +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.constant.HttpStatus; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.core.page.PageDomain; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.core.page.TableSupport; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.PageUtils; +import com.bnhz.common.utils.SecurityUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.sql.SqlUtil; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; + +import javax.annotation.Resource; +import java.beans.PropertyEditorSupport; +import java.util.Date; +import java.util.List; + +/** + * web层通用数据处理 + * + * @author ruoyi + */ +public class BaseController +{ + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Resource + private RedisCache redisCache; + + /** + * 将前台传递过来的日期格式的字符串,自动转化为Date类型 + */ + @InitBinder + public void initBinder(WebDataBinder binder) + { + // Date 类型转换 + binder.registerCustomEditor(Date.class, new PropertyEditorSupport() + { + @Override + public void setAsText(String text) + { + setValue(DateUtils.parseDate(text)); + } + }); + } + + /** + * 设置请求分页数据 + */ + protected void startPage() + { + PageUtils.startPage(); + } + + /** + * 设置请求排序数据 + */ + protected void startOrderBy() + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + if (StringUtils.isNotEmpty(pageDomain.getOrderBy())) + { + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + PageHelper.orderBy(orderBy); + } + } + + /** + * 清理分页的线程变量 + */ + protected void clearPage() + { + PageUtils.clearPage(); + } + + /** + * 响应请求分页数据 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected TableDataInfo getDataTable(List list) + { + TableDataInfo rspData = new TableDataInfo(); + rspData.setCode(HttpStatus.SUCCESS); + rspData.setMsg("查询成功"); + rspData.setRows(list); + rspData.setTotal(new PageInfo(list).getTotal()); + return rspData; + } + + /** + * 返回成功 + */ + public AjaxResult success() + { + return AjaxResult.success(); + } + + /** + * 返回失败消息 + */ + public AjaxResult error() + { + return AjaxResult.error(); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(String message) + { + return AjaxResult.success(message); + } + + /** + * 返回成功消息 + */ + public AjaxResult success(Object data) + { + return AjaxResult.success(data); + } + + /** + * 返回失败消息 + */ + public AjaxResult error(String message) + { + return AjaxResult.error(message); + } + + /** + * 返回警告消息 + */ + public AjaxResult warn(String message) + { + return AjaxResult.warn(message); + } + + /** + * 响应返回结果 + * + * @param rows 影响行数 + * @return 操作结果 + */ + protected AjaxResult toAjax(int rows) + { + return rows > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 响应返回结果 + * + * @param result 结果 + * @return 操作结果 + */ + protected AjaxResult toAjax(boolean result) + { + return result ? success() : error(); + } + + /** + * 页面跳转 + */ + public String redirect(String url) + { + return StringUtils.format("redirect:{}", url); + } + + /** + * 获取用户缓存信息 + * 由于不同端不能获取最新用户信息,所以优先以用户id缓存key获取用户信息 + */ + public LoginUser getLoginUser() + { + LoginUser loginUser = SecurityUtils.getLoginUser(); + Long userId = loginUser.getUserId(); + if (userId != null) { + String userKey = CacheConstants.LOGIN_USERID_KEY + userId; + return redisCache.getCacheObject(userKey); + } + return loginUser; + } + + /** + * 获取登录用户id + */ + public Long getUserId() + { + return getLoginUser().getUserId(); + } + + /** + * 获取登录部门id + */ + public Long getDeptId() + { + return getLoginUser().getDeptId(); + } + + /** + * 获取登录用户名 + */ + public String getUsername() + { + return getLoginUser().getUsername(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/AjaxResult.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/AjaxResult.java new file mode 100644 index 0000000..b909184 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/AjaxResult.java @@ -0,0 +1,214 @@ +package com.bnhz.common.core.domain; + +import java.util.HashMap; +import com.bnhz.common.constant.HttpStatus; +import com.bnhz.common.utils.StringUtils; + +/** + * 操作消息提醒 + * + * @author ruoyi + */ +public class AjaxResult extends HashMap +{ + private static final long serialVersionUID = 1L; + + /** 状态码 */ + public static final String CODE_TAG = "code"; + + /** 返回内容 */ + public static final String MSG_TAG = "msg"; + + /** 数据对象 */ + public static final String DATA_TAG = "data"; + + /** + * 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。 + */ + public AjaxResult() + { + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + */ + public AjaxResult(int code, String msg) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + } + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + * @param data 数据对象 + */ + public AjaxResult(int code, String msg, Object data) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + if (StringUtils.isNotNull(data)) + { + super.put(DATA_TAG, data); + } + } + + + /** + * 初始化一个新创建的 AjaxResult 对象 + * + * @param code 状态码 + * @param msg 返回内容 + * @param data 数据对象 + */ + public AjaxResult(int code, String msg, Object data,int total) + { + super.put(CODE_TAG, code); + super.put(MSG_TAG, msg); + if (StringUtils.isNotNull(data)) + { + super.put(DATA_TAG, data); + } + super.put("total",total); + } + + /** + * 返回成功消息 + * + * @return 成功消息 + */ + public static AjaxResult success() + { + return AjaxResult.success("操作成功"); + } + + /** + * 返回成功数据 + * + * @return 成功消息 + */ + public static AjaxResult success(Object data) + { + return AjaxResult.success("操作成功", data); + } + + /** + * 返回成功数据 + * + * @return 成功消息 + */ + public static AjaxResult success(Object data,int total) + { + return new AjaxResult(HttpStatus.SUCCESS, "操作成功", data,total); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @return 成功消息 + */ + public static AjaxResult success(String msg) + { + return AjaxResult.success(msg, null); + } + + /** + * 返回成功消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 成功消息 + */ + public static AjaxResult success(String msg, Object data) + { + return new AjaxResult(HttpStatus.SUCCESS, msg, data); + } + + /** + * 返回警告消息 + * + * @param msg 返回内容 + * @return 警告消息 + */ + public static AjaxResult warn(String msg) + { + return AjaxResult.warn(msg, null); + } + + /** + * 返回警告消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 警告消息 + */ + public static AjaxResult warn(String msg, Object data) + { + return new AjaxResult(HttpStatus.WARN, msg, data); + } + + /** + * 返回错误消息 + * + * @return 错误消息 + */ + public static AjaxResult error() + { + return AjaxResult.error("操作失败"); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(String msg) + { + return AjaxResult.error(msg, null); + } + + /** + * 返回错误消息 + * + * @param msg 返回内容 + * @param data 数据对象 + * @return 错误消息 + */ + public static AjaxResult error(String msg, Object data) + { + return new AjaxResult(HttpStatus.ERROR, msg, data); + } + + /** + * 返回错误消息 + * + * @param code 状态码 + * @param msg 返回内容 + * @return 错误消息 + */ + public static AjaxResult error(int code, String msg) + { + return new AjaxResult(code, msg, null); + } + + /** + * 方便链式调用 + * + * @param key 键 + * @param value 值 + * @return 数据对象 + */ + @Override + public AjaxResult put(String key, Object value) + { + super.put(key, value); + return this; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/BaseDO.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/BaseDO.java new file mode 100644 index 0000000..aacdc50 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/BaseDO.java @@ -0,0 +1,43 @@ +package com.bnhz.common.core.domain; + +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 基类,时间类型改为LocalDateTime + * @author fastb + * @date 2023-08-22 9:11 + */ +@Data +public class BaseDO implements Serializable { + private static final long serialVersionUID = 1L; + + /** 创建者 */ + @ApiModelProperty("创建者") + private String createBy; + + /** 创建时间 */ + @ApiModelProperty("创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** 更新者 */ + @ApiModelProperty("更新者") + private String updateBy; + + /** 更新时间 */ + @ApiModelProperty("更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + + /** 逻辑删除 */ + @ApiModelProperty("逻辑删除") + @TableLogic + private Boolean delFlag; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/BaseEntity.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/BaseEntity.java new file mode 100644 index 0000000..b170e18 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/BaseEntity.java @@ -0,0 +1,126 @@ +package com.bnhz.common.core.domain; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.annotations.ApiModelProperty; + +/** + * Entity基类 + * + * @author ruoyi + */ +public class BaseEntity implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 搜索值 */ + @ApiModelProperty("搜索值") + @JsonIgnore + private String searchValue; + + /** 创建者 */ + @ApiModelProperty("创建者") + private String createBy; + + /** 创建时间 */ + @ApiModelProperty("创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** 更新者 */ + @ApiModelProperty("更新者") + private String updateBy; + + /** 更新时间 */ + @ApiModelProperty("更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + /** 备注 */ + @ApiModelProperty("备注") + private String remark; + + /** 请求参数 */ + @ApiModelProperty("请求参数") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map params; + + public String getSearchValue() + { + return searchValue; + } + + public void setSearchValue(String searchValue) + { + this.searchValue = searchValue; + } + + public String getCreateBy() + { + return createBy; + } + + public void setCreateBy(String createBy) + { + this.createBy = createBy; + } + + public Date getCreateTime() + { + return createTime; + } + + public void setCreateTime(Date createTime) + { + this.createTime = createTime; + } + + public String getUpdateBy() + { + return updateBy; + } + + public void setUpdateBy(String updateBy) + { + this.updateBy = updateBy; + } + + public Date getUpdateTime() + { + return updateTime; + } + + public void setUpdateTime(Date updateTime) + { + this.updateTime = updateTime; + } + + public String getRemark() + { + return remark; + } + + public void setRemark(String remark) + { + this.remark = remark; + } + + public Map getParams() + { + if (params == null) + { + params = new HashMap<>(); + } + return params; + } + + public void setParams(Map params) + { + this.params = params; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/BlackCarResult.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/BlackCarResult.java new file mode 100644 index 0000000..cb14fa3 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/BlackCarResult.java @@ -0,0 +1,38 @@ +package com.bnhz.common.core.domain; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Leo + * @date 2024/6/15 16:06 + */ + +@Data +@NoArgsConstructor +public class BlackCarResult { + + private static String SUCCESS = "1"; + private static String FAIL = "0"; + + + private String code; + + private String message; + + private T data; + + public BlackCarResult(String code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + public static BlackCarResult ok() { + return new BlackCarResult<>(SUCCESS, "", null); + } + + public static BlackCarResult ok(T t ) { + return new BlackCarResult<>(SUCCESS, "", t); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/CommonResult.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/CommonResult.java new file mode 100644 index 0000000..0ac82fe --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/CommonResult.java @@ -0,0 +1,112 @@ +package com.bnhz.common.core.domain; + +import com.bnhz.common.enums.GlobalErrorCodeConstants; +import com.bnhz.common.exception.ErrorCode; +import com.bnhz.common.exception.ServiceException; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + * + * @see ErrorCode#getCode() + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示,用户可阅读 + * + * @see ErrorCode#getMsg() () + */ + private String msg; + + /** + * 将传入的 result 对象,转换成另外一个泛型结果的对象 + * + * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 + * + * @param result 传入的 result 对象 + * @param 返回的泛型 + * @return 新的 CommonResult 对象 + */ + public static CommonResult error(CommonResult result) { + return error(result.getCode(), result.getMsg()); + } + + public static CommonResult error(Integer code, String message) { + Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = code; + result.msg = message; + return result; + } + + public static CommonResult error(ErrorCode errorCode) { + return error(errorCode.getCode(), errorCode.getMsg()); + } + + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); + result.data = data; + result.msg = ""; + return result; + } + + public static boolean isSuccess(Integer code) { + return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isSuccess() { + return isSuccess(code); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isError() { + return !isSuccess(); + } + + // ========= 和 Exception 异常体系集成 ========= + + /** + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 + */ + public void checkError() throws ServiceException { + if (isSuccess()) { + return; + } + // 业务异常 + throw new ServiceException(code, msg); + } + + /** + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 + * 如果没有,则返回 {@link #data} 数据 + */ + @JsonIgnore // 避免 jackson 序列化 + public T getCheckedData() { + checkError(); + return data; + } + + public static CommonResult error(ServiceException serviceException) { + return error(serviceException.getCode(), serviceException.getMessage()); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/PageParam.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/PageParam.java new file mode 100644 index 0000000..a1b43e6 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/PageParam.java @@ -0,0 +1,25 @@ +package com.bnhz.common.core.domain; + +import lombok.Data; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@Data +public class PageParam implements Serializable { + + private static final Integer PAGE_NO = 1; + private static final Integer PAGE_SIZE = 10; + + @NotNull(message = "页码不能为空") + @Min(value = 1, message = "页码最小值为 1") + private Integer pageNo = PAGE_NO; + + @NotNull(message = "每页条数不能为空") + @Min(value = 1, message = "每页条数最小值为 1") + @Max(value = 100, message = "每页条数最大值为 100") + private Integer pageSize = PAGE_SIZE; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/PageResult.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/PageResult.java new file mode 100644 index 0000000..f7fad63 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/PageResult.java @@ -0,0 +1,42 @@ +package com.bnhz.common.core.domain; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Api(tags = "分页结果") +@Data +public final class PageResult implements Serializable { + + @ApiModelProperty(value = "数据", required = true) + private List list; + + @ApiModelProperty(value = "总量", required = true) + private Long total; + + public PageResult() { + } + + public PageResult(List list, Long total) { + this.list = list; + this.total = total; + } + + public PageResult(Long total) { + this.list = new ArrayList<>(); + this.total = total; + } + + public static PageResult empty() { + return new PageResult<>(0L); + } + + public static PageResult empty(Long total) { + return new PageResult<>(total); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/R.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/R.java new file mode 100644 index 0000000..ef0b3c5 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/R.java @@ -0,0 +1,115 @@ +package com.bnhz.common.core.domain; + +import java.io.Serializable; +import com.bnhz.common.constant.HttpStatus; + +/** + * 响应信息主体 + * + * @author ruoyi + */ +public class R implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 成功 */ + public static final int SUCCESS = HttpStatus.SUCCESS; + + /** 失败 */ + public static final int FAIL = HttpStatus.ERROR; + + private int code; + + private String msg; + + private T data; + + public static R ok() + { + return restResult(null, SUCCESS, "操作成功"); + } + + public static R ok(T data) + { + return restResult(data, SUCCESS, "操作成功"); + } + + public static R ok(T data, String msg) + { + return restResult(data, SUCCESS, msg); + } + + public static R fail() + { + return restResult(null, FAIL, "操作失败"); + } + + public static R fail(String msg) + { + return restResult(null, FAIL, msg); + } + + public static R fail(T data) + { + return restResult(data, FAIL, "操作失败"); + } + + public static R fail(T data, String msg) + { + return restResult(data, FAIL, msg); + } + + public static R fail(int code, String msg) + { + return restResult(null, code, msg); + } + + private static R restResult(T data, int code, String msg) + { + R apiResult = new R<>(); + apiResult.setCode(code); + apiResult.setData(data); + apiResult.setMsg(msg); + return apiResult; + } + + public int getCode() + { + return code; + } + + public void setCode(int code) + { + this.code = code; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } + + public T getData() + { + return data; + } + + public void setData(T data) + { + this.data = data; + } + + public static Boolean isError(R ret) + { + return !isSuccess(ret); + } + + public static Boolean isSuccess(R ret) + { + return R.SUCCESS == ret.getCode(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/SortingField.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/SortingField.java new file mode 100644 index 0000000..8e886f1 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/SortingField.java @@ -0,0 +1,56 @@ +package com.bnhz.common.core.domain; + +import java.io.Serializable; + +/** + * 排序字段 DTO + * + * 类名加了 ing 的原因是,避免和 ES SortField 重名。 + */ +public class SortingField implements Serializable { + + /** + * 顺序 - 升序 + */ + public static final String ORDER_ASC = "asc"; + /** + * 顺序 - 降序 + */ + public static final String ORDER_DESC = "desc"; + + /** + * 字段 + */ + private String field; + /** + * 顺序 + */ + private String order; + + // 空构造方法,解决反序列化 + public SortingField() { + } + + public SortingField(String field, String order) { + this.field = field; + this.order = order; + } + + public String getField() { + return field; + } + + public SortingField setField(String field) { + this.field = field; + return this; + } + + public String getOrder() { + return order; + } + + public SortingField setOrder(String order) { + this.order = order; + return this; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/TenantBaseDO.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/TenantBaseDO.java new file mode 100644 index 0000000..d027e89 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/TenantBaseDO.java @@ -0,0 +1,20 @@ +package com.bnhz.common.core.domain; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 拓展多租户的 BaseDO 基类 + * + * @author bnhz + */ +@Data +@EqualsAndHashCode(callSuper = true) +public abstract class TenantBaseDO extends BaseDO { + + /** + * 多租户编号 + */ + private Long tenantId; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/TreeEntity.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/TreeEntity.java new file mode 100644 index 0000000..b3058b8 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/TreeEntity.java @@ -0,0 +1,79 @@ +package com.bnhz.common.core.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tree基类 + * + * @author ruoyi + */ +public class TreeEntity extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 父菜单名称 */ + private String parentName; + + /** 父菜单ID */ + private Long parentId; + + /** 显示顺序 */ + private Integer orderNum; + + /** 祖级列表 */ + private String ancestors; + + /** 子部门 */ + private List children = new ArrayList<>(); + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + public String getAncestors() + { + return ancestors; + } + + public void setAncestors(String ancestors) + { + this.ancestors = ancestors; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/TreeSelect.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/TreeSelect.java new file mode 100644 index 0000000..c03364b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/TreeSelect.java @@ -0,0 +1,77 @@ +package com.bnhz.common.core.domain; + +import java.io.Serializable; +import java.util.List; +import java.util.stream.Collectors; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.bnhz.common.core.domain.entity.SysDept; +import com.bnhz.common.core.domain.entity.SysMenu; + +/** + * Treeselect树结构实体类 + * + * @author ruoyi + */ +public class TreeSelect implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 节点ID */ + private Long id; + + /** 节点名称 */ + private String label; + + /** 子节点 */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List children; + + public TreeSelect() + { + + } + + public TreeSelect(SysDept dept) + { + this.id = dept.getDeptId(); + this.label = dept.getDeptName(); + this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public TreeSelect(SysMenu menu) + { + this.id = menu.getMenuId(); + this.label = menu.getMenuName(); + this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList()); + } + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysDept.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysDept.java new file mode 100644 index 0000000..8018400 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysDept.java @@ -0,0 +1,311 @@ +package com.bnhz.common.core.domain.entity; + +import java.util.ArrayList; +import java.util.List; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.bnhz.common.core.domain.BaseEntity; + +/** + * 部门表 sys_dept + * + * @author ruoyi + */ +@ApiModel(value = "SysDept", description = "部门表 sys_dept") +public class SysDept extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 部门ID */ + @ApiModelProperty("部门ID") + private Long deptId; + + /** + * 机构系统账号ID + */ + private Long deptUserId; + + /** 父部门ID */ + @ApiModelProperty("父部门ID") + private Long parentId; + + /** 祖级列表 */ + @ApiModelProperty("祖级列表") + private String ancestors; + + /** 部门名称 */ + @ApiModelProperty("部门名称") + private String deptName; + + /** 显示顺序 */ + @ApiModelProperty("显示顺序") + private Integer orderNum; + + /** 负责人 */ + @ApiModelProperty("负责人") + private String leader; + + /** 联系电话 */ + @ApiModelProperty("联系电话") + private String phone; + + /** 邮箱 */ + @ApiModelProperty("邮箱") + private String email; + + /** 部门状态:0正常,1停用 */ + @ApiModelProperty("部门状态:0正常,1停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + @ApiModelProperty("删除标志(0代表存在 2代表删除)") + private String delFlag; + + /** 父部门名称 */ + @ApiModelProperty("父部门名称") + private String parentName; + + /** 子部门 */ + @ApiModelProperty("子部门") + private List children = new ArrayList(); + + /** + * 系统账号名称 + */ + private String userName; + + /** + * 系统账号密码 + */ + private String password; + + /** + * 确认密码 + */ + private String confirmPassword; + + /** + * 机构类型 + */ + private Integer deptType; + + public Boolean getShowOwner() { + return showOwner; + } + + public void setShowOwner(Boolean showOwner) { + this.showOwner = showOwner; + } + + /** + * 是否显示自己 + */ + private Boolean showOwner; + + /** + * 管理员姓名 + */ + private String deptUserName; + + public String getDeptUserName() { + return deptUserName; + } + + public void setDeptUserName(String deptUserName) { + this.deptUserName = deptUserName; + } + + public Integer getDeptType() { + return deptType; + } + + public void setDeptType(Integer deptType) { + this.deptType = deptType; + } + + 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 String getConfirmPassword() { + return confirmPassword; + } + + public void setConfirmPassword(String confirmPassword) { + this.confirmPassword = confirmPassword; + } + + public Long getDeptUserId() { + return deptUserId; + } + + public void setDeptUserId(Long deptUserId) { + this.deptUserId = deptUserId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + public String getAncestors() + { + return ancestors; + } + + public void setAncestors(String ancestors) + { + this.ancestors = ancestors; + } + + @NotBlank(message = "部门名称不能为空") + @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符") + public String getDeptName() + { + return deptName; + } + + public void setDeptName(String deptName) + { + this.deptName = deptName; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + public String getLeader() + { + return leader; + } + + public void setLeader(String leader) + { + this.leader = leader; + } + + @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符") + public String getPhone() + { + return phone; + } + + public void setPhone(String phone) + { + this.phone = phone; + } + + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("deptId", getDeptId()) + .append("deptUserId", getDeptUserId()) + .append("parentId", getParentId()) + .append("ancestors", getAncestors()) + .append("deptName", getDeptName()) + .append("orderNum", getOrderNum()) + .append("leader", getLeader()) + .append("phone", getPhone()) + .append("email", getEmail()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .toString(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysDictData.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysDictData.java new file mode 100644 index 0000000..1b82266 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysDictData.java @@ -0,0 +1,189 @@ +package com.bnhz.common.core.domain.entity; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.bnhz.common.annotation.Excel; +import com.bnhz.common.annotation.Excel.ColumnType; +import com.bnhz.common.constant.UserConstants; +import com.bnhz.common.core.domain.BaseEntity; + +/** + * 字典数据表 sys_dict_data + * + * @author ruoyi + */ +@ApiModel(value = "SysDictData", description = "字典数据表 sys_dict_data") +public class SysDictData extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 字典编码 */ + @ApiModelProperty("字典编码") + @Excel(name = "字典编码", cellType = ColumnType.NUMERIC) + private Long dictCode; + + /** 字典排序 */ + @ApiModelProperty("字典排序") + @Excel(name = "字典排序", cellType = ColumnType.NUMERIC) + private Long dictSort; + + /** 字典标签 */ + @ApiModelProperty("字典标签") + @Excel(name = "字典标签") + private String dictLabel; + + /** 字典键值 */ + @ApiModelProperty("字典键值") + @Excel(name = "字典键值") + private String dictValue; + + /** 字典类型 */ + @ApiModelProperty("字典类型") + @Excel(name = "字典类型") + private String dictType; + + /** 样式属性(其他样式扩展) */ + @ApiModelProperty("样式属性(其他样式扩展)") + private String cssClass; + + /** 表格字典样式 */ + @ApiModelProperty("表格字典样式") + private String listClass; + + /** 是否默认(Y是 N否) */ + @ApiModelProperty("是否默认(Y是 N否)") + @Excel(name = "是否默认", readConverterExp = "Y=是,N=否") + private String isDefault; + + /** 状态(0正常 1停用) */ + @ApiModelProperty("状态(0正常 1停用)") + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictCode() + { + return dictCode; + } + + public void setDictCode(Long dictCode) + { + this.dictCode = dictCode; + } + + public Long getDictSort() + { + return dictSort; + } + + public void setDictSort(Long dictSort) + { + this.dictSort = dictSort; + } + + @NotBlank(message = "字典标签不能为空") + @Size(min = 0, max = 100, message = "字典标签长度不能超过100个字符") + public String getDictLabel() + { + return dictLabel; + } + + public void setDictLabel(String dictLabel) + { + this.dictLabel = dictLabel; + } + + @NotBlank(message = "字典键值不能为空") + @Size(min = 0, max = 100, message = "字典键值长度不能超过100个字符") + public String getDictValue() + { + return dictValue; + } + + public void setDictValue(String dictValue) + { + this.dictValue = dictValue; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型长度不能超过100个字符") + public String getDictType() + { + return dictType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + @Size(min = 0, max = 100, message = "样式属性长度不能超过100个字符") + public String getCssClass() + { + return cssClass; + } + + public void setCssClass(String cssClass) + { + this.cssClass = cssClass; + } + + public String getListClass() + { + return listClass; + } + + public void setListClass(String listClass) + { + this.listClass = listClass; + } + + public boolean getDefault() + { + return UserConstants.YES.equals(this.isDefault); + } + + public String getIsDefault() + { + return isDefault; + } + + public void setIsDefault(String isDefault) + { + this.isDefault = isDefault; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("dictCode", getDictCode()) + .append("dictSort", getDictSort()) + .append("dictLabel", getDictLabel()) + .append("dictValue", getDictValue()) + .append("dictType", getDictType()) + .append("cssClass", getCssClass()) + .append("listClass", getListClass()) + .append("isDefault", getIsDefault()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysDictType.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysDictType.java new file mode 100644 index 0000000..b88d401 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysDictType.java @@ -0,0 +1,104 @@ +package com.bnhz.common.core.domain.entity; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.bnhz.common.annotation.Excel; +import com.bnhz.common.annotation.Excel.ColumnType; +import com.bnhz.common.core.domain.BaseEntity; + +/** + * 字典类型表 sys_dict_type + * + * @author ruoyi + */ +@ApiModel(value = "SysDictType", description = "字典类型表 sys_dict_type") +public class SysDictType extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 字典主键 */ + @ApiModelProperty("字典主键") + @Excel(name = "字典主键", cellType = ColumnType.NUMERIC) + private Long dictId; + + /** 字典名称 */ + @ApiModelProperty("字典名称") + @Excel(name = "字典名称") + private String dictName; + + /** 字典类型 */ + @ApiModelProperty("字典类型") + @Excel(name = "字典类型") + private String dictType; + + /** 状态(0正常 1停用) */ + @ApiModelProperty("状态(0正常 1停用)") + @Excel(name = "状态", readConverterExp = "0=正常,1=停用") + private String status; + + public Long getDictId() + { + return dictId; + } + + public void setDictId(Long dictId) + { + this.dictId = dictId; + } + + @NotBlank(message = "字典名称不能为空") + @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符") + public String getDictName() + { + return dictName; + } + + public void setDictName(String dictName) + { + this.dictName = dictName; + } + + @NotBlank(message = "字典类型不能为空") + @Size(min = 0, max = 100, message = "字典类型类型长度不能超过100个字符") + @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)") + public String getDictType() + { + return dictType; + } + + public void setDictType(String dictType) + { + this.dictType = dictType; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("dictId", getDictId()) + .append("dictName", getDictName()) + .append("dictType", getDictType()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysMenu.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysMenu.java new file mode 100644 index 0000000..72a05a9 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysMenu.java @@ -0,0 +1,292 @@ +package com.bnhz.common.core.domain.entity; + +import java.util.ArrayList; +import java.util.List; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.bnhz.common.core.domain.BaseEntity; + +/** + * 菜单权限表 sys_menu + * + * @author ruoyi + */ +@ApiModel(value = "SysMenu", description = "菜单权限表 sys_menu") +public class SysMenu extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 菜单ID */ + @ApiModelProperty("菜单ID") + private Long menuId; + + /** 菜单名称 */ + @ApiModelProperty("菜单名称") + private String menuName; + + /** 父菜单名称 */ + @ApiModelProperty("父菜单名称") + private String parentName; + + /** 父菜单ID */ + @ApiModelProperty("父菜单ID") + private Long parentId; + + /** 显示顺序 */ + @ApiModelProperty("显示顺序") + private Integer orderNum; + + /** 路由地址 */ + @ApiModelProperty("路由地址") + private String path; + + /** 组件路径 */ + @ApiModelProperty("组件路径") + private String component; + + /** 路由参数 */ + @ApiModelProperty("路由参数") + private String query; + + /** 是否为外链(0是 1否) */ + @ApiModelProperty("是否为外链(0是 1否)") + private String isFrame; + + /** 是否缓存(0缓存 1不缓存) */ + @ApiModelProperty("是否缓存(0缓存 1不缓存)") + private String isCache; + + /** 类型(M目录 C菜单 F按钮) */ + @ApiModelProperty("类型(M目录 C菜单 F按钮)") + private String menuType; + + /** 显示状态(0显示 1隐藏) */ + @ApiModelProperty("显示状态(0显示 1隐藏)") + private String visible; + + /** 菜单状态(0正常 1停用) */ + @ApiModelProperty("菜单状态(0正常 1停用)") + private String status; + + /** 权限字符串 */ + @ApiModelProperty("权限字符串") + private String perms; + + /** 菜单图标 */ + @ApiModelProperty("菜单图标") + private String icon; + + /** 子菜单 */ + @ApiModelProperty("子菜单") + private List children = new ArrayList(); + + /** + * 部门id + */ + private Long deptId; + + public Long getDeptId() { + return deptId; + } + + public void setDeptId(Long deptId) { + this.deptId = deptId; + } + + public Long getMenuId() + { + return menuId; + } + + public void setMenuId(Long menuId) + { + this.menuId = menuId; + } + + @NotBlank(message = "菜单名称不能为空") + @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符") + public String getMenuName() + { + return menuName; + } + + public void setMenuName(String menuName) + { + this.menuName = menuName; + } + + public String getParentName() + { + return parentName; + } + + public void setParentName(String parentName) + { + this.parentName = parentName; + } + + public Long getParentId() + { + return parentId; + } + + public void setParentId(Long parentId) + { + this.parentId = parentId; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getOrderNum() + { + return orderNum; + } + + public void setOrderNum(Integer orderNum) + { + this.orderNum = orderNum; + } + + @Size(min = 0, max = 200, message = "路由地址不能超过200个字符") + public String getPath() + { + return path; + } + + public void setPath(String path) + { + this.path = path; + } + + @Size(min = 0, max = 200, message = "组件路径不能超过255个字符") + public String getComponent() + { + return component; + } + + public void setComponent(String component) + { + this.component = component; + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query = query; + } + + public String getIsFrame() + { + return isFrame; + } + + public void setIsFrame(String isFrame) + { + this.isFrame = isFrame; + } + + public String getIsCache() + { + return isCache; + } + + public void setIsCache(String isCache) + { + this.isCache = isCache; + } + + @NotBlank(message = "菜单类型不能为空") + public String getMenuType() + { + return menuType; + } + + public void setMenuType(String menuType) + { + this.menuType = menuType; + } + + public String getVisible() + { + return visible; + } + + public void setVisible(String visible) + { + this.visible = visible; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符") + public String getPerms() + { + return perms; + } + + public void setPerms(String perms) + { + this.perms = perms; + } + + public String getIcon() + { + return icon; + } + + public void setIcon(String icon) + { + this.icon = icon; + } + + public List getChildren() + { + return children; + } + + public void setChildren(List children) + { + this.children = children; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("menuId", getMenuId()) + .append("menuName", getMenuName()) + .append("parentId", getParentId()) + .append("orderNum", getOrderNum()) + .append("path", getPath()) + .append("component", getComponent()) + .append("isFrame", getIsFrame()) + .append("IsCache", getIsCache()) + .append("menuType", getMenuType()) + .append("visible", getVisible()) + .append("status ", getStatus()) + .append("perms", getPerms()) + .append("icon", getIcon()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysRole.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysRole.java new file mode 100644 index 0000000..2101799 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysRole.java @@ -0,0 +1,322 @@ +package com.bnhz.common.core.domain.entity; + +import java.util.Set; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.bnhz.common.annotation.Excel; +import com.bnhz.common.annotation.Excel.ColumnType; +import com.bnhz.common.core.domain.BaseEntity; + +/** + * 角色表 sys_role + * + * @author ruoyi + */ +@ApiModel(value = "SysRole", description = "角色表 sys_role") +public class SysRole extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 角色ID */ + @ApiModelProperty("角色ID") + @Excel(name = "角色序号", cellType = ColumnType.NUMERIC) + private Long roleId; + + /** 角色名称 */ + @ApiModelProperty("角色名称") + @Excel(name = "角色名称") + private String roleName; + + /** 角色权限 */ + @ApiModelProperty("角色权限") + @Excel(name = "角色权限") + private String roleKey; + + /** 角色排序 */ + @ApiModelProperty("角色排序") + @Excel(name = "角色排序") + private Integer roleSort; + + /** 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限) */ + @ApiModelProperty(value = "数据范围", notes = "(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限;5:仅本人数据权限)") + @Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限,5=仅本人数据权限") + private String dataScope; + + /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */ + @ApiModelProperty(value = "菜单树选择项是否关联显示", notes = "( 0:父子不互相关联显示 1:父子互相关联显示)") + private boolean menuCheckStrictly; + + /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */ + @ApiModelProperty(value = "部门树选择项是否关联显示", notes = "(0:父子不互相关联显示 1:父子互相关联显示 )") + private boolean deptCheckStrictly; + + /** 角色状态(0正常 1停用) */ + @ApiModelProperty("角色状态(0正常 1停用)") + @Excel(name = "角色状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + @ApiModelProperty("删除标志") + private String delFlag; + + /** 用户是否存在此角色标识 默认不存在 */ + private boolean flag = false; + + /** 菜单组 */ + @ApiModelProperty("菜单组") + private Long[] menuIds; + + /** 部门组(数据权限) */ + @ApiModelProperty("部门组") + private Long[] deptIds; + + /** 角色菜单权限 */ + @ApiModelProperty("角色菜单权限") + private Set permissions; + + /** + * 部门id + */ + private Long deptId; + + /** + * 部门名称 + */ + private String deptName; + + /** + * 是否显示下级机构数据 + */ + private Boolean showChild; + + /** + * 是否可以修改用户角色 + */ + private Boolean canEditRole; + + /** + * 是否是机构管理员角色 + */ + private Boolean manager; + + public Boolean getManager() { + return manager; + } + + public void setManager(Boolean manager) { + this.manager = manager; + } + + public Boolean getCanEditRole() { + return canEditRole; + } + + public void setCanEditRole(Boolean canEditRole) { + this.canEditRole = canEditRole; + } + + public Boolean getShowChild() { + return showChild; + } + + public void setShowChild(Boolean showChild) { + this.showChild = showChild; + } + + public Long getDeptId() { + return deptId; + } + + public void setDeptId(Long deptId) { + this.deptId = deptId; + } + + public String getDeptName() { + return deptName; + } + + public void setDeptName(String deptName) { + this.deptName = deptName; + } + + public SysRole() + { + + } + + public SysRole(Long roleId) + { + this.roleId = roleId; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + public boolean isAdmin() + { + return isAdmin(this.roleId); + } + + public static boolean isAdmin(Long roleId) + { + return roleId != null && 1L == roleId; + } + + @NotBlank(message = "角色名称不能为空") + @Size(min = 0, max = 30, message = "角色名称长度不能超过30个字符") + public String getRoleName() + { + return roleName; + } + + public void setRoleName(String roleName) + { + this.roleName = roleName; + } + + @NotBlank(message = "权限字符不能为空") + @Size(min = 0, max = 100, message = "权限字符长度不能超过100个字符") + public String getRoleKey() + { + return roleKey; + } + + public void setRoleKey(String roleKey) + { + this.roleKey = roleKey; + } + + @NotNull(message = "显示顺序不能为空") + public Integer getRoleSort() + { + return roleSort; + } + + public void setRoleSort(Integer roleSort) + { + this.roleSort = roleSort; + } + + public String getDataScope() + { + return dataScope; + } + + public void setDataScope(String dataScope) + { + this.dataScope = dataScope; + } + + public boolean isMenuCheckStrictly() + { + return menuCheckStrictly; + } + + public void setMenuCheckStrictly(boolean menuCheckStrictly) + { + this.menuCheckStrictly = menuCheckStrictly; + } + + public boolean isDeptCheckStrictly() + { + return deptCheckStrictly; + } + + public void setDeptCheckStrictly(boolean deptCheckStrictly) + { + this.deptCheckStrictly = deptCheckStrictly; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public boolean isFlag() + { + return flag; + } + + public void setFlag(boolean flag) + { + this.flag = flag; + } + + public Long[] getMenuIds() + { + return menuIds; + } + + public void setMenuIds(Long[] menuIds) + { + this.menuIds = menuIds; + } + + public Long[] getDeptIds() + { + return deptIds; + } + + public void setDeptIds(Long[] deptIds) + { + this.deptIds = deptIds; + } + + public Set getPermissions() + { + return permissions; + } + + public void setPermissions(Set permissions) + { + this.permissions = permissions; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("roleId", getRoleId()) + .append("roleName", getRoleName()) + .append("roleKey", getRoleKey()) + .append("roleSort", getRoleSort()) + .append("dataScope", getDataScope()) + .append("menuCheckStrictly", isMenuCheckStrictly()) + .append("deptCheckStrictly", isDeptCheckStrictly()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysUser.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysUser.java new file mode 100644 index 0000000..5247e5b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/entity/SysUser.java @@ -0,0 +1,372 @@ +package com.bnhz.common.core.domain.entity; + +import java.util.Date; +import java.util.List; +import javax.validation.constraints.*; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.bnhz.common.annotation.Excel; +import com.bnhz.common.annotation.Excel.ColumnType; +import com.bnhz.common.annotation.Excel.Type; +import com.bnhz.common.annotation.Excels; +import com.bnhz.common.core.domain.BaseEntity; +import com.bnhz.common.xss.Xss; + +/** + * 用户对象 sys_user + * + * @author ruoyi + */ +@ApiModel(value = "SysUser", description = "用户对象 sys_user") +public class SysUser extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 用户ID */ + @ApiModelProperty("用户ID") + @Excel(name = "用户序号", cellType = ColumnType.NUMERIC, prompt = "用户编号") + private Long userId; + + /** 部门ID */ + @ApiModelProperty("部门ID") + @Excel(name = "部门编号", type = Type.IMPORT) + private Long deptId; + + /** 用户账号 */ + @ApiModelProperty("用户账号") + @Excel(name = "登录名称") + private String userName; + + /** 用户昵称 */ + @ApiModelProperty("用户昵称") + @Excel(name = "用户名称") + private String nickName; + + /** 用户邮箱 */ + @ApiModelProperty("用户邮箱") + @Excel(name = "用户邮箱") + private String email; + + /** 手机号码 */ + @ApiModelProperty("手机号码") + @Excel(name = "手机号码") + private String phonenumber; + + /** 用户性别 */ + @ApiModelProperty("用户性别") + @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知") + private String sex; + + /** 用户头像 */ + @ApiModelProperty("用户头像") + private String avatar; + + /** 密码 */ + @ApiModelProperty("密码") + private String password; + + /** 帐号状态(0正常 1停用) */ + @ApiModelProperty("帐号状态(0正常 1停用)") + @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用") + private String status; + + /** 删除标志(0代表存在 2代表删除) */ + @ApiModelProperty("删除标志") + private String delFlag; + + /** 最后登录IP */ + @ApiModelProperty("最后登录IP") + @Excel(name = "最后登录IP", type = Type.EXPORT) + private String loginIp; + + /** 最后登录时间 */ + @ApiModelProperty("最后登录时间") + @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT) + private Date loginDate; + + /** 部门对象 */ + @ApiModelProperty("部门对象") + @Excels({ + @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT), + @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT) + }) + private SysDept dept; + + /** 角色对象 */ + @ApiModelProperty("角色对象") + private List roles; + + /** 角色组 */ + @ApiModelProperty("角色组") + private Long[] roleIds; + + /** 岗位组 */ + @ApiModelProperty("岗位组") + private Long[] postIds; + + /** 角色ID */ + @ApiModelProperty("角色ID") + private Long roleId; + + /** + * 是否显示下级机构数据 + */ + private Boolean showChild; + + /** + * 是否是机构管理员 + */ + private Boolean manager; + + public Boolean getManager() { + return manager; + } + + public void setManager(Boolean manager) { + this.manager = manager; + } + + public Boolean getShowChild() { + return showChild; + } + + public void setShowChild(Boolean showChild) { + this.showChild = showChild; + } + + public SysUser() + { + + } + + public SysUser(Long userId) + { + this.userId = userId; + } + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public boolean isAdmin() + { + return isAdmin(this.userId); + } + + public static boolean isAdmin(Long userId) + { + return userId != null && 1L == userId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + @Xss(message = "用户昵称不能包含脚本字符") + @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") + public String getNickName() + { + return nickName; + } + + public void setNickName(String nickName) + { + this.nickName = nickName; + } + + @Xss(message = "用户账号不能包含脚本字符") + @NotBlank(message = "用户账号不能为空") + @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") + public String getUserName() + { + return userName; + } + + public void setUserName(String userName) + { + this.userName = userName; + } + + @Email(message = "邮箱格式不正确") + @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") + public String getEmail() + { + return email; + } + + public void setEmail(String email) + { + this.email = email; + } + + @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符") + public String getPhonenumber() + { + return phonenumber; + } + + public void setPhonenumber(String phonenumber) + { + this.phonenumber = phonenumber; + } + + public String getSex() + { + return sex; + } + + public void setSex(String sex) + { + this.sex = sex; + } + + public String getAvatar() + { + return avatar; + } + + public void setAvatar(String avatar) + { + this.avatar = avatar; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public String getDelFlag() + { + return delFlag; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getLoginIp() + { + return loginIp; + } + + public void setLoginIp(String loginIp) + { + this.loginIp = loginIp; + } + + public Date getLoginDate() + { + return loginDate; + } + + public void setLoginDate(Date loginDate) + { + this.loginDate = loginDate; + } + + public SysDept getDept() + { + return dept; + } + + public void setDept(SysDept dept) + { + this.dept = dept; + } + + public List getRoles() + { + return roles; + } + + public void setRoles(List roles) + { + this.roles = roles; + } + + public Long[] getRoleIds() + { + return roleIds; + } + + public void setRoleIds(Long[] roleIds) + { + this.roleIds = roleIds; + } + + public Long[] getPostIds() + { + return postIds; + } + + public void setPostIds(Long[] postIds) + { + this.postIds = postIds; + } + + public Long getRoleId() + { + return roleId; + } + + public void setRoleId(Long roleId) + { + this.roleId = roleId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("userId", getUserId()) + .append("deptId", getDeptId()) + .append("userName", getUserName()) + .append("nickName", getNickName()) + .append("email", getEmail()) + .append("phonenumber", getPhonenumber()) + .append("sex", getSex()) + .append("avatar", getAvatar()) + .append("password", getPassword()) + .append("status", getStatus()) + .append("delFlag", getDelFlag()) + .append("loginIp", getLoginIp()) + .append("loginDate", getLoginDate()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .append("dept", getDept()) + .toString(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/BindLoginBody.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/BindLoginBody.java new file mode 100644 index 0000000..c1921aa --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/BindLoginBody.java @@ -0,0 +1,22 @@ +package com.bnhz.common.core.domain.model; + +/** + * 用户登录对象 + * + * @author ruoyi + */ +public class BindLoginBody extends LoginBody +{ + /** + * 绑定id + */ + private String bindId; + + public String getBindId() { + return bindId; + } + + public void setBindId(String bindId) { + this.bindId = bindId; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/BindRegisterBody.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/BindRegisterBody.java new file mode 100644 index 0000000..81f3591 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/BindRegisterBody.java @@ -0,0 +1,21 @@ +package com.bnhz.common.core.domain.model; + +/** + * 用户注册对象 + * + * @author ruoyi + */ +public class BindRegisterBody extends RegisterBody { + /** + * 绑定id + */ + private String bindId; + + public String getBindId() { + return bindId; + } + + public void setBindId(String bindId) { + this.bindId = bindId; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/LoginBody.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/LoginBody.java new file mode 100644 index 0000000..d1f2ad6 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/LoginBody.java @@ -0,0 +1,141 @@ +package com.bnhz.common.core.domain.model; + +import com.bnhz.common.utils.StringUtils; + +/** + * 用户登录对象 + * + * @author ruoyi + */ +public class LoginBody +{ + /** + * 用户名 + */ + private String username; + + /** + * 黑卡用户名 + */ + private String account; + + /** + * 用户密码 + */ + private String password; + + + private String pwdcode; + + /** + * 验证码 + */ + private String code; + + /** + * 唯一标识 + */ + private String uuid; + + /** + * 手机号 + */ + private String phonenumber; + + /** + * 登录平台 1-web端;2-移动端;3-小程序 + */ + private Integer sourceType; + + /** + * 短信验证码 + */ + private String smsCode; + + public String getSmsCode() { + return smsCode; + } + + public void setSmsCode(String smsCode) { + this.smsCode = smsCode; + } + + + public Integer getSourceType() { + return sourceType; + } + + public void setSourceType(Integer sourceType) { + this.sourceType = sourceType; + } + + public String getPhonenumber() { + return phonenumber; + } + + public void setPhonenumber(String phonenumber) { + this.phonenumber = phonenumber; + } + + public String getUsername() + { + if (StringUtils.isNotEmpty(this.account)) { + return account; + } + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + if (StringUtils.isNotEmpty(this.pwdcode)) { + return pwdcode; + } + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getCode() + { + return code; + } + + public void setCode(String code) + { + this.code = code; + } + + public String getUuid() + { + return uuid; + } + + public void setUuid(String uuid) + { + this.uuid = uuid; + } + + public String getAccount() { + return account; + } + + public void setAccount(String account) { + this.account = account; + } + + public String getPwdcode() { + return pwdcode; + } + + public void setPwdcode(String pwdcode) { + this.pwdcode = pwdcode; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/LoginUser.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/LoginUser.java new file mode 100644 index 0000000..571b5d4 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/LoginUser.java @@ -0,0 +1,266 @@ +package com.bnhz.common.core.domain.model; + +import java.util.Collection; +import java.util.Set; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import com.alibaba.fastjson2.annotation.JSONField; +import com.bnhz.common.core.domain.entity.SysUser; + +/** + * 登录用户身份权限 + * + * @author ruoyi + */ +public class LoginUser implements UserDetails +{ + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private Long userId; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 用户唯一标识 + */ + private String token; + + /** + * 登录时间 + */ + private Long loginTime; + + /** + * 过期时间 + */ + private Long expireTime; + + /** + * 登录IP地址 + */ + private String ipaddr; + + /** + * 登录地点 + */ + private String loginLocation; + + /** + * 浏览器类型 + */ + private String browser; + + /** + * 操作系统 + */ + private String os; + + /** + * 权限列表 + */ + private Set permissions; + + /** + * 用户信息 + */ + private SysUser user; + + public Long getUserId() + { + return userId; + } + + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getDeptId() + { + return deptId; + } + + public void setDeptId(Long deptId) + { + this.deptId = deptId; + } + + public String getToken() + { + return token; + } + + public void setToken(String token) + { + this.token = token; + } + + public LoginUser() + { + } + + public LoginUser(SysUser user, Set permissions) + { + this.user = user; + this.permissions = permissions; + } + + public LoginUser(Long userId, Long deptId, SysUser user, Set permissions) + { + this.userId = userId; + this.deptId = deptId; + this.user = user; + this.permissions = permissions; + } + + @JSONField(serialize = false) + @Override + public String getPassword() + { + return user.getPassword(); + } + + @Override + public String getUsername() + { + return user.getUserName(); + } + + /** + * 账户是否未过期,过期无法验证 + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonExpired() + { + return true; + } + + /** + * 指定用户是否解锁,锁定的用户无法进行身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isAccountNonLocked() + { + return true; + } + + /** + * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isCredentialsNonExpired() + { + return true; + } + + /** + * 是否可用 ,禁用的用户不能身份验证 + * + * @return + */ + @JSONField(serialize = false) + @Override + public boolean isEnabled() + { + return true; + } + + public Long getLoginTime() + { + return loginTime; + } + + public void setLoginTime(Long loginTime) + { + this.loginTime = loginTime; + } + + public String getIpaddr() + { + return ipaddr; + } + + public void setIpaddr(String ipaddr) + { + this.ipaddr = ipaddr; + } + + public String getLoginLocation() + { + return loginLocation; + } + + public void setLoginLocation(String loginLocation) + { + this.loginLocation = loginLocation; + } + + public String getBrowser() + { + return browser; + } + + public void setBrowser(String browser) + { + this.browser = browser; + } + + public String getOs() + { + return os; + } + + public void setOs(String os) + { + this.os = os; + } + + public Long getExpireTime() + { + return expireTime; + } + + public void setExpireTime(Long expireTime) + { + this.expireTime = expireTime; + } + + public Set getPermissions() + { + return permissions; + } + + public void setPermissions(Set permissions) + { + this.permissions = permissions; + } + + public SysUser getUser() + { + return user; + } + + public void setUser(SysUser user) + { + this.user = user; + } + + @Override + public Collection getAuthorities() + { + return null; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/RegisterBody.java b/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/RegisterBody.java new file mode 100644 index 0000000..df3c5eb --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/domain/model/RegisterBody.java @@ -0,0 +1,11 @@ +package com.bnhz.common.core.domain.model; + +/** + * 用户注册对象 + * + * @author ruoyi + */ +public class RegisterBody extends LoginBody +{ + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/iot/response/DashDeviceTotalDto.java b/bnhz-common/src/main/java/com/bnhz/common/core/iot/response/DashDeviceTotalDto.java new file mode 100644 index 0000000..8e34337 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/iot/response/DashDeviceTotalDto.java @@ -0,0 +1,22 @@ +package com.bnhz.common.core.iot.response; + +import lombok.Data; + +/** + * 大屏设备总览数据 + * @author bill + */ +@Data +public class DashDeviceTotalDto { + + /*设备总数*/ + private Integer total; + /*在线设备总数*/ + private Integer onlineCount; + /*离线设备总数*/ + private Integer OfflineCount; + /*未激活设备数*/ + private Integer unActiveCount; + + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/iot/response/DeCodeBo.java b/bnhz-common/src/main/java/com/bnhz/common/core/iot/response/DeCodeBo.java new file mode 100644 index 0000000..6850062 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/iot/response/DeCodeBo.java @@ -0,0 +1,26 @@ +package com.bnhz.common.core.iot.response; + +import lombok.Data; + +/** + * @author gsb + * @date 2023/4/8 15:43 + */ +@Data +public class DeCodeBo { + + /**原始报文*/ + private String payload; + /**从机编号*/ + private Integer slaveId; + /**寄存器地址*/ + private Integer address; + /**功能码*/ + private Integer code; + /**读取个数*/ + private Integer count; + /**写入值*/ + private Integer writeData; + /**读写类型 1-解析 2-读指令 3-写指令 */ + private Integer type; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/iot/response/IdentityAndName.java b/bnhz-common/src/main/java/com/bnhz/common/core/iot/response/IdentityAndName.java new file mode 100644 index 0000000..9f3356e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/iot/response/IdentityAndName.java @@ -0,0 +1,70 @@ +package com.bnhz.common.core.iot.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 物模型值的项 + * + * @author kerwincui + * @date 2021-12-16 + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +public class IdentityAndName +{ + + public IdentityAndName(String id,String value){ + this.id=id; + this.value=value; + } + + public IdentityAndName(String id,Integer isHistory){ + this.id=id; + this.isHistory=isHistory; + } + + public IdentityAndName(String id, Integer isHistory, String specs, String name, Integer type){ + this.id = id; + this.isHistory = isHistory; + this.dataType = specs; + this.name = name; + this.type = type; + } + + /** 物模型唯一标识符 */ + private String id; + /** 物模型值 */ + private Object value; + + private Integer isChart; + + /**是否监控*/ + private Integer isHistory; + /** + * 数据定义 + */ + private String dataType; + /**物模型名称*/ + private String name; + /** + * 物模型类型 + */ + private Integer type; + /** + * 是否是参数 + */ + private Integer isParams; + + private String formula; + + private Integer slaveId; + + private Integer tempSlaveId; + + private Integer quantity; + + private String code; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceReplyBo.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceReplyBo.java new file mode 100644 index 0000000..17e6183 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceReplyBo.java @@ -0,0 +1,17 @@ +package com.bnhz.common.core.mq; + +import lombok.Data; + +/** + * @author bill + */ +@Data +public class DeviceReplyBo { + + /*设备下发消息id*/ + private String messageId; + /*标识符*/ + private String id; + /**下发值*/ + private String value; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceReport.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceReport.java new file mode 100644 index 0000000..c6842ad --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceReport.java @@ -0,0 +1,95 @@ +package com.bnhz.common.core.mq; + +import com.bnhz.common.core.mq.message.SubDeviceMessage; +import com.bnhz.common.core.protocol.Message; +import com.bnhz.common.core.thingsModel.ThingsModelValuesInput; +import com.bnhz.common.enums.FunctionReplyStatus; +import com.bnhz.common.enums.ServerType; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; +import java.util.List; + + +/** + * 设备上行数据model + * + * @author bill + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class DeviceReport extends Message { + + /** + * 设备编号 + */ + private String serialNumber; + /** + * 产品ID + */ + private Long productId; + /** + * 平台时间 + */ + private Date platformDate; + /** + * 消息id + */ + private String messageId; + /** + * 设备主动上报的消息体 + * key 物模型Identifier + * value 物模型设备对应值 + */ + private ThingsModelValuesInput valuesInput; + + /** ================网关子设备====================*/ + /** + * 网关子设备编号 + */ + private List subDeviceCodes; + /** + * 网关子设备消息 + */ + private List subDeviceMessages; + /** ================回调数据====================*/ + + /** + * 是否设备回复数据 + */ + private Boolean isReply = false; + + /** + * 设备回复消息 + */ + private String replyMessage; + /** + * 设备回复状态 + */ + private FunctionReplyStatus status; + /** + * 从机编号 + */ + private Integer slaveId; + /** + * 服务器类型 + */ + private ServerType serverType; + /** + * 寄存器地址 + */ + private int address; + + private String protocolCode; + + private Long userId; + private String userName; + private String deviceName; + + /** + * 应答数据 + */ + private String answerMsg; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceReportBo.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceReportBo.java new file mode 100644 index 0000000..d878d72 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceReportBo.java @@ -0,0 +1,73 @@ +package com.bnhz.common.core.mq; + +import com.bnhz.common.core.mq.message.PropRead; +import com.bnhz.common.core.thingsModel.ThingsModelValuesInput; +import com.bnhz.common.enums.FunctionReplyStatus; +import com.bnhz.common.enums.ServerType; +import com.bnhz.common.enums.ThingsModelType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * 设备上报 + * @author bill + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DeviceReportBo { + + /*设备编号或IMEI号*/ + private String serialNumber; + /*产品ID*/ + private Long productId; + /*4G物联网卡CCID*/ + private String ccId; + /*topic*/ + private String topicName; + /*mqtt消息中的packetId*/ + private Long packetId; + /*上报时间*/ + private Date platformDate; + /*物模型类型 1=-属性,2-功能,3-事件 */ + private ThingsModelType type; + /*上报数据*/ + private byte[] data; + /*1-设备数据上报 2- 下发指令给设备,设备应答数据*/ + private Integer reportType; + /*消息id*/ + private String messageId; + /* modbus协议消息回调,记录数据*/ + private PropRead prop; + /*解析后组装好的数据*/ + private ThingsModelValuesInput valuesInput; + /*处理的消息服务类型*/ + private ServerType serverType; + private Integer slaveId; + + /** + * 是否设备回复数据 + */ + private Boolean isReply = false; + + /** + * 设备回复消息 + */ + private String replyMessage; + /** + * 设备回复状态 + */ + private FunctionReplyStatus status; + /** + * 寄存器地址 + */ + private int address; + + private String protocolCode; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceStatusBo.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceStatusBo.java new file mode 100644 index 0000000..18c018d --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/DeviceStatusBo.java @@ -0,0 +1,35 @@ +package com.bnhz.common.core.mq; + +import com.bnhz.common.enums.DeviceStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * 设备状态 + * @author bill + */ +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class DeviceStatusBo { + /** + * 设备客户端id + */ + private String serialNumber; + /**是否活跃*/ + private DeviceStatus status; + /**消息时间*/ + private Date timestamp; + /*host*/ + private String hostName; + /*port*/ + private Integer port; + + private String ip; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/InvokeReqDto.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/InvokeReqDto.java new file mode 100644 index 0000000..244775b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/InvokeReqDto.java @@ -0,0 +1,62 @@ +package com.bnhz.common.core.mq; + +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.utils.DateUtils; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Date; +import java.util.Map; + +/** + * @author gsb + * @date 2022/12/5 11:26 + */ +@Data +public class InvokeReqDto { + + @NotNull(message = "设备编号不能为空") + @ApiModelProperty(value = "设备编号") + private String serialNumber; + + @NotNull(message = "标识符不能为空") + @ApiModelProperty(value = "标识符") + private String identifier; + /**消息体*/ + @ApiModelProperty(value = "消息体") + private JSONObject value; + /**远程消息体*/ + @ApiModelProperty(value = "远程调用消息体") + private Map remoteCommand; + /**设备超时时间*/ + @ApiModelProperty(value = "设备超时响应时间,默认10s") + private Integer timeOut = 10; + + @ApiModelProperty(value = "下发物模型类型") + @NotNull + private Integer type; + + @ApiModelProperty(value = "是否是影子模式") + @NotNull + private Boolean isShadow; + + private String dataType; + + @NotNull(message = "产品id不能为空") + @ApiModelProperty(value = "产品id") + private Long productId; + /**从机编号*/ + private Integer slaveId; + /** + * 显示的值 + */ + private String showValue; + + /** + * 物模型名称 + */ + private String modelName; + + private Date timestamp = DateUtils.getNowDate(); +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/MQSendMessageBo.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/MQSendMessageBo.java new file mode 100644 index 0000000..c0c786c --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/MQSendMessageBo.java @@ -0,0 +1,51 @@ +package com.bnhz.common.core.mq; + +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.core.protocol.modbus.ModbusCode; +import com.bnhz.common.enums.ThingsModelType; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 服务(指令)下发对象 + * + * @author bill + */ +@Data +@NoArgsConstructor +public class MQSendMessageBo { + + /*设备编号*/ + private String serialNumber; + /*下发属性标识符*/ + private String identifier; + /*寄存器地址 10进制*/ + private String hexAddress; + /*topic*/ + private JSONObject command; + private String topicName; + /*产品ID*/ + private Long productId; + /*物模型类型 1=-属性,2-功能,3-事件,4-属性和功能*/ + private ThingsModelType type; + /*下发的数据*/ + private JSONObject value; + /*协议编号 例如:modbus-rtu*/ + private String protocolCode; + /*messageId生成放到调用接口的时候生成*/ + private String messageId; + /*流水号,针对某些协议没有消息流水号无法区分下发的消息和上报的消息是否对应*/ + private String serialNo; + /*从机id*/ + private Integer slaveId; + /**显示值*/ + private String showValue; + private String modelName; + private ModbusCode code; + /*是否是影子模式*/ + private Boolean isShadow; + /*传输协议*/ + private String transport; + + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/MessageReplyBo.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/MessageReplyBo.java new file mode 100644 index 0000000..e97c1db --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/MessageReplyBo.java @@ -0,0 +1,51 @@ +package com.bnhz.common.core.mq; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +/** + * 设备消息回调或者下发指令值 + * + * @author gsb + * @date 2022/5/11 9:27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MessageReplyBo { + + + private String id; + /** + * 消息回执的messageId,和下行消息呼应 + */ + private String messageId; + /** + * 设备处理消息的状态 + */ + private Integer status; + /** + * 抵达服务器时间 + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date timestamp; + /** + * 设备上报的时间 + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date deviceTimestamp; + /** + * 回执消息内容 + */ + private String body; + /*产品编号*/ + private Long productId; + /*设备编号*/ + private String serialNumber; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceData.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceData.java new file mode 100644 index 0000000..9244deb --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceData.java @@ -0,0 +1,51 @@ +package com.bnhz.common.core.mq.message; + +import com.bnhz.common.core.protocol.Message; +import com.bnhz.common.core.protocol.modbus.ModbusCode; +import io.netty.buffer.ByteBuf; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 消息解析model + * @author gsb + * @date 2022/10/10 15:53 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Builder +public class DeviceData extends Message { + + /*topic*/ + private String topicName; + + /*设备编号*/ + private String serialNumber; + + /*原数据*/ + private byte[] data; + + private ByteBuf buf; + + /*消息类型 1.设备上报数据 2.设备回调数据*/ + private int messageType; + + /*下发数据model*/ + private DeviceDownMessage downMessage; + + private Object body; + /*MQTT OR 其他*/ + private int type; + + /*Modbus*/ + private ModbusCode code; + + private PropRead prop; + /*是否使用modbus客户端模拟测试*/ + private boolean isEnabledTest; + /**产品id*/ + private Long productId; + + private boolean isControlled; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceDownMessage.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceDownMessage.java new file mode 100644 index 0000000..f7f9fd4 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceDownMessage.java @@ -0,0 +1,70 @@ +package com.bnhz.common.core.mq.message; + +import com.bnhz.common.core.protocol.modbus.ModbusCode; +import com.bnhz.common.enums.ServerType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 设备下发指令model + * + * @author gsb + * @date 2022/10/10 16:18 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DeviceDownMessage { + + private String messageId; + /** + * 时间戳,单位毫秒 + */ + private Long timestamp; + /** + * 消息体 + */ + private Object body; + /*下发的指令,服务调用的时候就是服务标识符*/ + private String identifier; + /*产品id*/ + private Long productId; + /** + * 设备编码 + */ + private String serialNumber; + /*网关设备编码*/ + String subSerialNumber; + /** + * true: 表示是一条发往网关子设备的指令 + * 默认是false + */ + Boolean subFlag = false; + //下发消息类型 + private int type; + /** + * 从机编号 + */ + private Integer slaveId; + private ModbusCode code; + private int count; + private int address; + private String protocolCode; + + private List values; + private String topic; + private String subCode; + private ServerType serverType; + + public DeviceDownMessage(List values, String topic, String subCode,String transport) { + this.values = values; + this.topic = topic; + this.subCode = subCode; + this.serverType = ServerType.explain(transport); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceFunctionMessage.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceFunctionMessage.java new file mode 100644 index 0000000..6d0cafc --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceFunctionMessage.java @@ -0,0 +1,33 @@ +package com.bnhz.common.core.mq.message; + +import lombok.Data; + +/** + * 平台下发指令数据model + * @author bill + */ +@Data +public class DeviceFunctionMessage { + + /*流水号,兼容modbus标准协议没有消息流水号*/ + private String seqNo; + /*平台时间*/ + private Long pfTimestamp; + /*下发的消息体*/ + private Object body; + /*下发的指令物模型标识符*/ + private String identifier; + /*下发的数据寄存器地址*/ + private String hexAddress; + /*产品ID*/ + private Long productId; + /*设备编号*/ + private String serialNumber; + /*网关设备编号*/ + private String protocolCode; + + /*是否有子设备 0-否,1-是*/ + private Integer hasSub; + /*子设备从机编号 例如 02 编号从机。通过主机集控下发的指定从机编号*/ + private String subDeviceCode; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceMessage.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceMessage.java new file mode 100644 index 0000000..f438d3e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/DeviceMessage.java @@ -0,0 +1,20 @@ +package com.bnhz.common.core.mq.message; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 集群消息 + * @author bill + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class DeviceMessage { + + /*数据*/ + private T data; + + private int nodeId; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/InstructionsMessage.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/InstructionsMessage.java new file mode 100644 index 0000000..90be848 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/InstructionsMessage.java @@ -0,0 +1,20 @@ +package com.bnhz.common.core.mq.message; + +import lombok.Data; + +/** + * 指令下发组将的model + * @author bill + */ +@Data +public class InstructionsMessage { + + /*下发的数据*/ + private byte[] message; + + /*MQTt-下发的topic*/ + private String topicName; + + /*设备编号*/ + private String serialNumber; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/MqttBo.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/MqttBo.java new file mode 100644 index 0000000..6e11a70 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/MqttBo.java @@ -0,0 +1,26 @@ +package com.bnhz.common.core.mq.message; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.util.Date; + +/** + * @author bill + */ +@Data +public class MqttBo { + + /*主题*/ + private String topic; + /*数据*/ + private String data; + /*消息质量*/ + private int qos = 1; + /*发送方向*/ + private String direction; + /*时间*/ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date ts; +} + diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/PropRead.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/PropRead.java new file mode 100644 index 0000000..3b92844 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/PropRead.java @@ -0,0 +1,39 @@ +package com.bnhz.common.core.mq.message; + +import com.bnhz.common.core.protocol.modbus.ModbusCode; +import lombok.Data; + +/** + * @author gsb + * @date 2022/12/9 10:15 + */ +@Data +public class PropRead { + + /**设备编号*/ + private String serialNumber; + /**寄存器起始地址*/ + private int address; + /** + * 读取寄存器个数 + */ + private int count; + /**数据结果长度计算值*/ + private int length; + /** + * 从机地址 + */ + private int slaveId; + /** + * 读取个数 + */ + private int quantity; + /** + * 数据 + */ + private String data; + /** + * 功能码 + */ + private ModbusCode code; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/ProtocolDto.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/ProtocolDto.java new file mode 100644 index 0000000..78f6ba3 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/ProtocolDto.java @@ -0,0 +1,21 @@ +package com.bnhz.common.core.mq.message; + +import lombok.Data; + +/** + * 协议bean + * @author gsb + * @date 2022/10/25 14:54 + */ +@Data +public class ProtocolDto { + + /**协议编号*/ + private String code; + private String name; + /*外部协议url*/ + private String protocolUrl; + private String description; + /**协议类型 协议类型 0:系统协议 1:jar,2.js,3.c*/ + private Integer protocolType; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/SubDeviceMessage.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/SubDeviceMessage.java new file mode 100644 index 0000000..bc3ec62 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/message/SubDeviceMessage.java @@ -0,0 +1,18 @@ +package com.bnhz.common.core.mq.message; + +import lombok.Data; + +/** + * 网关子设备model + * @author gsb + * @date 2022/10/10 10:18 + */ +@Data +public class SubDeviceMessage { + /*子设备编号或编码*/ + private String serialNumber; + /*数据*/ + private byte[] data; + /*消息id*/ + private String messageId; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/ota/OtaReplyMessage.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/ota/OtaReplyMessage.java new file mode 100644 index 0000000..dee33ef --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/ota/OtaReplyMessage.java @@ -0,0 +1,17 @@ +package com.bnhz.common.core.mq.ota; + +import lombok.Data; + +/** + * OTA升级回复model + * @author gsb + * @date 2022/10/24 17:20 + */ +@Data +public class OtaReplyMessage { + + private String messageId; + // 200成功 其他。。 + private int code; + private String msg; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/ota/OtaUpgradeBo.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/ota/OtaUpgradeBo.java new file mode 100644 index 0000000..04d1c90 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/ota/OtaUpgradeBo.java @@ -0,0 +1,46 @@ +package com.bnhz.common.core.mq.ota; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * OTA远程升级 + * @author gsb + * @date 2022/10/10 10:22 + */ +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class OtaUpgradeBo { + + /**OTAId*/ + private Long otaId; + @NotNull(message = "上传地址为空") + private String otaUrl; + @NotNull(message = "固件版本号不能为空") + private String firmwareVersion; + private String firmwareName; + @NotNull(message = "流水号不能为空") + private String seqNo; + @NotNull(message = "产品ID不能为空") + private Long productId; + private String signType = "16md5"; + @NotNull(message = "签名不能为空") + private String signCode; + /*产品名称*/ + private String productName; + private String fileBase64; + private Integer pushType; + /*设备编码,逐个升级*/ + private String serialNumber; + private String deviceName; + /*任务ID*/ + private Long taskId; + /*消息id*/ + private String messageId; + /*平台描述消息*/ + private String msg; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/mq/ota/OtaUpgradeDelayTask.java b/bnhz-common/src/main/java/com/bnhz/common/core/mq/ota/OtaUpgradeDelayTask.java new file mode 100644 index 0000000..8395087 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/mq/ota/OtaUpgradeDelayTask.java @@ -0,0 +1,52 @@ +package com.bnhz.common.core.mq.ota; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.bnhz.common.utils.DateUtils; +import lombok.Data; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +/** + * ota升级发送,实现Delayed延时接口 + * + * @author bill + */ +@Data +public class OtaUpgradeDelayTask implements Delayed { + + /*固件id*/ + private Long firmwareId; + private List devices; + /*任务id*/ + private Long taskId; + /*开始升级时间*/ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date startTime; + + + /** + * 设置延迟执行时间 开始升级时间 -当前时间 + * + * @param unit + * @return + */ + @Override + public long getDelay(TimeUnit unit) { + return startTime.getTime() - DateUtils.getTimestamp(); + } + + @Override + public int compareTo(Delayed o) { + OtaUpgradeDelayTask delayTask = (OtaUpgradeDelayTask) o; + //比较 + long diff = this.startTime.getTime() - delayTask.startTime.getTime(); + if (diff <= 0) { + return -1; + } else { + return 1; + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/AlertPushParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/AlertPushParams.java new file mode 100644 index 0000000..647fdfb --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/AlertPushParams.java @@ -0,0 +1,48 @@ +package com.bnhz.common.core.notify; + +import lombok.Data; + +import java.util.Set; + +/** + * @author fastb + * @version 1.0 + * @description: TODO + * @date 2023-12-26 11:03 + */ +@Data +public class AlertPushParams { + + /** + * 通知模版id + */ + private Long notifyTemplateId; + /** + * 告警时间 + */ + private String alertTime; + /** + * 设备名称 + */ + private String deviceName; + /** + * 设备编号 + */ + private String serialNumber; + /** + * 告警发生地点 + */ + private String address; + /** + * 告警名称 + */ + private String alertName; + /** + * 告警推送手机号 + */ + private Set userPhoneSet; + /** + * 告警推送用户id + */ + private Set userIdSet; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/AppGeTuiParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/AppGeTuiParams.java new file mode 100644 index 0000000..8f3c63e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/AppGeTuiParams.java @@ -0,0 +1,33 @@ +package com.bnhz.common.core.notify; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dromara.sms4j.provider.config.BaseConfig; + +/** + * 个推参数配置 + * @author gsb + * @date 2023/12/11 17:14 + */ +@Data +@Accessors(chain = true) +public class AppGeTuiParams extends BaseConfig { + + @ApiModelProperty("appId") + private String appId; + + @ApiModelProperty("appKey") + private String appKey; + + @ApiModelProperty("秘钥") + private String masterSecret; + + @ApiModelProperty("模板参数") + private String params; + + @Override + public String getSupplier() { + return null; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/EnterpriseWeChatAPPParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/EnterpriseWeChatAPPParams.java new file mode 100644 index 0000000..39a22ff --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/EnterpriseWeChatAPPParams.java @@ -0,0 +1,35 @@ +package com.bnhz.common.core.notify; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import org.dromara.sms4j.provider.config.BaseConfig; + +/** + * 企业微信(应用消息) + * @author gsb + * @date 2023/12/11 17:25 + */ +@Data +@Accessors(chain = true) +public class EnterpriseWeChatAPPParams extends BaseConfig { + + @ApiModelProperty("企业ID") + private String corpId; + @ApiModelProperty("企业应用私钥OA") + private String corpSecret; + @ApiModelProperty("企业应用的id") + private Integer agentId; + //@ApiModelProperty("token") + //private String token; + //@ApiModelProperty("aes秘钥") + //private String aesKey; + + @ApiModelProperty("模板参数") + private String params; + + @Override + public String getSupplier() { + return null; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/NotifyConfigVO.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/NotifyConfigVO.java new file mode 100644 index 0000000..3def0f4 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/NotifyConfigVO.java @@ -0,0 +1,37 @@ +package com.bnhz.common.core.notify; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author fastb + * @version 1.0 + * @description: 通知配置参数属性VO类 + * @date 2024-01-09 14:01 + */ +@AllArgsConstructor +@NoArgsConstructor +@Data +public class NotifyConfigVO { + + /** + * 配置属性 + */ + private String attribute; + + /** + * 配置属性名称 + */ + private String name; + + /** + * 配置属性样式 string-字符串;text-富文本;file-文件;boolean-启用;int-整数 + */ + private String type; + + /** + * 值 + */ + private String value; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/NotifySendResponse.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/NotifySendResponse.java new file mode 100644 index 0000000..42a77e0 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/NotifySendResponse.java @@ -0,0 +1,33 @@ +package com.bnhz.common.core.notify; + +import lombok.Data; + +/** + * @author fastb + * @version 1.0 + * @description: 通知发送响应类 + * @date 2024-01-11 16:06 + */ +@Data +public class NotifySendResponse { + + /** + * 发送结果 1-成功;0-失败 + */ + private Integer status = 0; + + /** + * 返回结果内容 + */ + private String resultContent = ""; + + /** + * 发送内容,变量替换后的 + */ + private String sendContent = ""; + + /** + * 不是使用sendAccount账号发送,而是像钉钉这种,发送所有人或部门,记录这个 + */ + private String otherSendAccount = ""; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/WeChatServerParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/WeChatServerParams.java new file mode 100644 index 0000000..bf9eb71 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/WeChatServerParams.java @@ -0,0 +1,31 @@ +package com.bnhz.common.core.notify; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 微信服务号推送参数 + * @author gsb + * @date 2023/12/11 17:11 + */ +@Data +@Accessors(chain = true) +public class WeChatServerParams { + + @ApiModelProperty("appId") + private String appId; + + @ApiModelProperty("app秘钥") + private String secret; + + @ApiModelProperty("模板ID") + private String templateId; + + @ApiModelProperty("跳转地址") + private String page; + + @ApiModelProperty("模板参数") + private String params; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/alertPush/AlertPushItem.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/alertPush/AlertPushItem.java new file mode 100644 index 0000000..4611fc6 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/alertPush/AlertPushItem.java @@ -0,0 +1,28 @@ +package com.bnhz.common.core.notify.alertPush; + +import lombok.Data; + +/** + * 推送项item + * @author bill + */ +@Data +public class AlertPushItem { + + /** + * 告警时间 + */ + private String alertTime; + /** + * 设备编号 + */ + private String serialNumber; + /** + * 告警发生地点 + */ + private String address; + /** + * 告警名称 + */ + private String alertName; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/alertPush/PushMsg.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/alertPush/PushMsg.java new file mode 100644 index 0000000..71be995 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/alertPush/PushMsg.java @@ -0,0 +1,32 @@ +package com.bnhz.common.core.notify.alertPush; + +import lombok.Data; + +import java.util.List; + +/** + * 推送配置信息 + * @author bill + */ +@Data +public class PushMsg { + + /** + * 用户Id + */ + private Long userId; + /** + * 设备id + */ + private Long deviceId; + /** + * 告警id + */ + private Long alertId; + /** + * 推送内容值 + */ + private AlertPushItem item; + + private List values; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/DingTalkConfigParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/DingTalkConfigParams.java new file mode 100644 index 0000000..5ab0568 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/DingTalkConfigParams.java @@ -0,0 +1,21 @@ +package com.bnhz.common.core.notify.config; + +import lombok.Data; + +/** + * @author fastb + * @version 1.0 + * @description: 钉钉渠道应用配置类 + * @date 2024-01-12 17:50 + */ +@Data +public class DingTalkConfigParams { + + private String appKey; + + private String appSecret; + + private String agentId; + + private String webHook; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/EmailConfigParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/EmailConfigParams.java new file mode 100644 index 0000000..a166781 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/EmailConfigParams.java @@ -0,0 +1,48 @@ +package com.bnhz.common.core.notify.config; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 邮箱配置参数 + * @author gsb + * @date 2023/12/11 17:17 + */ +@Data +public class EmailConfigParams { + + @ApiModelProperty("服务器地址") + private String smtpServer; + + @ApiModelProperty("端口号") + private String port; + + @ApiModelProperty("账号(发件人地址)") + private String username; + + @ApiModelProperty("密码") + private String password; + + /** + * 是否开启ssl 默认开启 QQ之类的邮箱默认都需要ssl + */ + @ApiModelProperty("是否启动ssl") + private Boolean sslEnable; + + /** + * 是否开启验证 默认开启 + */ + @ApiModelProperty("启动ttl") + private Boolean authEnable; + + /** + * 重试间隔(单位:秒),默认为5秒 + */ + private Integer retryInterval = 5; + + /** + * 重试次数,默认为1次 + */ + private Integer maxRetries = 1; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/VoiceConfigParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/VoiceConfigParams.java new file mode 100644 index 0000000..6c01ca0 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/VoiceConfigParams.java @@ -0,0 +1,23 @@ +package com.bnhz.common.core.notify.config; + +import lombok.Data; + +/** + * 语音配置 + * @author fastb + * @date 2023-12-09 17:32 + */ +@Data +public class VoiceConfigParams { + + /** + * 您的AccessKey ID + */ + private String accessKeyId; + + /** + * 您的AccessKey Secret + */ + private String accessKeySecret; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/WeChatConfigParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/WeChatConfigParams.java new file mode 100644 index 0000000..55f98da --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/config/WeChatConfigParams.java @@ -0,0 +1,25 @@ +package com.bnhz.common.core.notify.config; + +import lombok.Data; + +/** + * 微信推送配置参数 + * @author gsb + * @date 2023/12/11 17:11 + */ +@Data +public class WeChatConfigParams { + + private String appId; + + private String appSecret; + + private String corpId; + + private String corpSecret; + + private String agentId; + + private String webHook; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/DingTalkMsgParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/DingTalkMsgParams.java new file mode 100644 index 0000000..f4d3e06 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/DingTalkMsgParams.java @@ -0,0 +1,53 @@ +package com.bnhz.common.core.notify.msg; + +import lombok.Data; + +/** + * @author fastb + * @version 1.0 + * @description: 钉钉模版配置参数类 + * @date 2024-01-12 17:51 + */ +@Data +public class DingTalkMsgParams { + + /** + * 发送账号 + */ + private String sendAccount; + + /** + * 是否发送所有人 + */ + private String sendAllEnable; + + /** + * 发送什么类型的文本 + */ + private String msgType; + + /** + * 消息内容 + */ + private String content; + + /** + * 消息标题 + */ + private String title; + + /** + * 消息链接 + */ + private String messageUrl; + + /** + * 图片链接 + */ + private String picUrl; + + /** + * 所属部门id + */ + private String deptId; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/EmailMsgParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/EmailMsgParams.java new file mode 100644 index 0000000..bcf2106 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/EmailMsgParams.java @@ -0,0 +1,33 @@ +package com.bnhz.common.core.notify.msg; + +import lombok.Data; + +/** + * @author fastb + * @version 1.0 + * @description: 邮箱模板消息参数 + * @date 2023-12-22 10:47 + */ +@Data +public class EmailMsgParams { + + /** + * 发送账号 + */ + private String sendAccount; + + /** + * 标题 + */ + private String title; + + /** + * 内容 + */ + private String content; + + /** + * 附件 + */ + private String attachment; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/VoiceMsgParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/VoiceMsgParams.java new file mode 100644 index 0000000..71b3e58 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/VoiceMsgParams.java @@ -0,0 +1,49 @@ +package com.bnhz.common.core.notify.msg; + +import lombok.Data; + +/** + * @author fastb + * @version 1.0 + * @description: 语音消息模板参数 + * @date 2023-12-22 10:54 + */ +@Data +public class VoiceMsgParams { + + /** + * 发送账号 + */ + private String sendAccount; + + /** + * 模板ID + */ + private String templateId; + + /** + * 内容 + */ + private String content; + + /** + * 应用ID + */ + private String sdkAppId; + + /** + * 播放次数 1~3 + */ + private String playTimes = "1"; + + /** + * 播放音量 0-100 + */ + private String volume = "50"; + + /** + * 语速控制 -500-500 + */ + private String speed = "0"; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/WeComMsgParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/WeComMsgParams.java new file mode 100644 index 0000000..f80914e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/WeComMsgParams.java @@ -0,0 +1,43 @@ +package com.bnhz.common.core.notify.msg; + +import lombok.Data; + +/** + * @author fastb + * @version 1.0 + * @description: 企业微信消息模板参数 + * @date 2023-12-22 10:57 + */ +@Data +public class WeComMsgParams { + + /** + * 发送账号 + */ + private String sendAccount; + + /** + * 消息类型 + */ + private String msgType; + /** + * 消息内容 + */ + private String content; + /** + * 消息标题 + */ + private String title; + /** + * 消息描述 + */ + private String description; + /** + * 跳转链接 + */ + private String url; + /** + * 图片链接 + */ + private String picUrl; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/WechatMsgParams.java b/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/WechatMsgParams.java new file mode 100644 index 0000000..f5ffae9 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/notify/msg/WechatMsgParams.java @@ -0,0 +1,43 @@ +package com.bnhz.common.core.notify.msg; + +import lombok.Data; + +/** + * @author fastb + * @version 1.0 + * @description: 微信消息模板参数 + * @date 2023-12-22 10:57 + */ +@Data +public class WechatMsgParams { + + /** + * 发送账号 + */ + private String sendAccount; + + /** + * 模版id + */ + private String templateId; + + /** + * 内容 + */ + private String content; + + /** + * 跳转链接 + */ + private String redirectUrl; + + /** + * 跳转小程序appid + */ + private String appid; + + /** + * 小程序跳转路径 + */ + private String pagePath; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/page/PageDomain.java b/bnhz-common/src/main/java/com/bnhz/common/core/page/PageDomain.java new file mode 100644 index 0000000..83b2a83 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/page/PageDomain.java @@ -0,0 +1,101 @@ +package com.bnhz.common.core.page; + +import com.bnhz.common.utils.StringUtils; + +/** + * 分页数据 + * + * @author ruoyi + */ +public class PageDomain +{ + /** 当前记录起始索引 */ + private Integer pageNum; + + /** 每页显示记录数 */ + private Integer pageSize; + + /** 排序列 */ + private String orderByColumn; + + /** 排序的方向desc或者asc */ + private String isAsc = "asc"; + + /** 分页参数合理化 */ + private Boolean reasonable = true; + + public String getOrderBy() + { + if (StringUtils.isEmpty(orderByColumn)) + { + return ""; + } + return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc; + } + + public Integer getPageNum() + { + return pageNum; + } + + public void setPageNum(Integer pageNum) + { + this.pageNum = pageNum; + } + + public Integer getPageSize() + { + return pageSize; + } + + public void setPageSize(Integer pageSize) + { + this.pageSize = pageSize; + } + + public String getOrderByColumn() + { + return orderByColumn; + } + + public void setOrderByColumn(String orderByColumn) + { + this.orderByColumn = orderByColumn; + } + + public String getIsAsc() + { + return isAsc; + } + + public void setIsAsc(String isAsc) + { + if (StringUtils.isNotEmpty(isAsc)) + { + // 兼容前端排序类型 + if ("ascending".equals(isAsc)) + { + isAsc = "asc"; + } + else if ("descending".equals(isAsc)) + { + isAsc = "desc"; + } + this.isAsc = isAsc; + } + } + + public Boolean getReasonable() + { + if (StringUtils.isNull(reasonable)) + { + return Boolean.TRUE; + } + return reasonable; + } + + public void setReasonable(Boolean reasonable) + { + this.reasonable = reasonable; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/page/PageResult.java b/bnhz-common/src/main/java/com/bnhz/common/core/page/PageResult.java new file mode 100644 index 0000000..656b7bf --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/page/PageResult.java @@ -0,0 +1,33 @@ +package com.bnhz.common.core.page; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * @author Leo + * @date 2024/6/12 10:30 + */ +@Data +@NoArgsConstructor +public class PageResult implements Serializable { + + /** 总记录数 */ + private long total; + + /** 列表数据 */ + private List rows; + + /** 消息状态码 */ + private int code; + + /** 消息内容 */ + private String msg; + + public PageResult(List rows, long total) { + this.total = total; + this.rows = rows; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/page/TableDataInfo.java b/bnhz-common/src/main/java/com/bnhz/common/core/page/TableDataInfo.java new file mode 100644 index 0000000..f196afa --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/page/TableDataInfo.java @@ -0,0 +1,85 @@ +package com.bnhz.common.core.page; + +import java.io.Serializable; +import java.util.List; + +/** + * 表格分页数据对象 + * + * @author ruoyi + */ +public class TableDataInfo implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 总记录数 */ + private long total; + + /** 列表数据 */ + private List rows; + + /** 消息状态码 */ + private int code; + + /** 消息内容 */ + private String msg; + + /** + * 表格数据对象 + */ + public TableDataInfo() + { + } + + /** + * 分页 + * + * @param list 列表数据 + * @param total 总记录数 + */ + public TableDataInfo(List list, int total) + { + this.rows = list; + this.total = total; + } + + public long getTotal() + { + return total; + } + + public void setTotal(long total) + { + this.total = total; + } + + public List getRows() + { + return rows; + } + + public void setRows(List rows) + { + this.rows = rows; + } + + public int getCode() + { + return code; + } + + public void setCode(int code) + { + this.code = code; + } + + public String getMsg() + { + return msg; + } + + public void setMsg(String msg) + { + this.msg = msg; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/page/TableSupport.java b/bnhz-common/src/main/java/com/bnhz/common/core/page/TableSupport.java new file mode 100644 index 0000000..fb2207f --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/page/TableSupport.java @@ -0,0 +1,56 @@ +package com.bnhz.common.core.page; + +import com.bnhz.common.core.text.Convert; +import com.bnhz.common.utils.ServletUtils; + +/** + * 表格数据处理 + * + * @author ruoyi + */ +public class TableSupport +{ + /** + * 当前记录起始索引 + */ + public static final String PAGE_NUM = "pageNum"; + + /** + * 每页显示记录数 + */ + public static final String PAGE_SIZE = "pageSize"; + + /** + * 排序列 + */ + public static final String ORDER_BY_COLUMN = "orderByColumn"; + + /** + * 排序的方向 "desc" 或者 "asc". + */ + public static final String IS_ASC = "isAsc"; + + /** + * 分页参数合理化 + */ + public static final String REASONABLE = "reasonable"; + + /** + * 封装分页对象 + */ + public static PageDomain getPageDomain() + { + PageDomain pageDomain = new PageDomain(); + pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1)); + pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10)); + pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN)); + pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC)); + pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE)); + return pageDomain; + } + + public static PageDomain buildPageRequest() + { + return getPageDomain(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/protocol/Message.java b/bnhz-common/src/main/java/com/bnhz/common/core/protocol/Message.java new file mode 100644 index 0000000..50a996a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/protocol/Message.java @@ -0,0 +1,48 @@ +package com.bnhz.common.core.protocol; + +import io.netty.buffer.ByteBuf; +import lombok.Data; + +import java.io.Serializable; + +/** + * 基础消息 + * + * @author bill + */ +@Data +public class Message implements Serializable { + + /*获取客户端id*/ + public String clientId; + /*消息类型*/ + public String messageId; + /*消息流水号*/ + public String serNo; + /** + * 消息通道id + */ + public String channelId; + + public ByteBuf payload; + + /** + * 是否数据和注册包都封装到一起 + */ + private Boolean isPackage = false; + + private Object body; + + + /** + * 是否是心跳包 + */ + private boolean heartbeatPackage; + + + /** + * 消息id + */ + private String id; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/protocol/modbus/ModbusCode.java b/bnhz-common/src/main/java/com/bnhz/common/core/protocol/modbus/ModbusCode.java new file mode 100644 index 0000000..0589ae1 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/protocol/modbus/ModbusCode.java @@ -0,0 +1,87 @@ +package com.bnhz.common.core.protocol.modbus; + +import com.bnhz.common.exception.ServiceException; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Modbus功能码 + * @author bill + * + * {bit 位操作} + * 线圈寄存器: bit对应一个信号的开关状态。功能码里面又分为写单个线圈寄存器和写多个线圈寄存器。对应上面的功能码也就是:0x01 0x05 0x0f + * 离散输入寄存器:离散输入寄存器就 是 只读线圈寄存器,每个bit表示一个开关量,是不能够写的。 功能码: 0x02 + * + * {byte 字节操作} + * 保持寄存器: 两个byte,可读写的 写也分为单个写和多个写对应的三个:0x03 0x06 0x10 + * 输入寄存器: 和保持寄存器类似,只支持读而不能写,一般是读取各种实时数据。一个寄存器也是占据两个byte的空间。对应的功能码: 0x04 + * + */ +@Getter +@AllArgsConstructor +public enum ModbusCode { + + Read01("读线圈",(byte) 0x01), // 读线圈(读写位模式) + Read02("读离散量输入",(byte) 0x02), // 读离散量输入(位只读模式) + Read03("读保持寄存器",(byte) 0x03), // 读保持寄存器(字节读写模式) + Read04("读输入寄存器",(byte) 0x04), // 读输入寄存器(字节只读模式) + + Write05("写单个线圈(读写位模式)",(byte) 0x05), // 写单个线圈(读写位模式) + Write06("写多个线圈",(byte) 0x06), // 写单个保持寄存器 + Write0F("写多个线圈",(byte) 0x0F), // 写多个线圈 + Write10("写多个保持寄存器",(byte) 0x10) // 写多个保持寄存器 + ; + + private String desc; + private byte code; + + public static ModbusCode getInstance(int code) { + switch ((byte)code) { + case 0x01: + return Read01; + case 0x02: + return Read02; + case 0x03: + return Read03; + case 0x04: + return Read04; + + case 0x05: + return Write05; + case 0x06: + return Write06; + case 0x0F: + return Write0F; + case 0x10: + return Write10; + + default: + throw new ServiceException("功能码[" + code + "],未定义"); + } + } + + public static String getDes(int code){ + switch ((byte)code) { + case 0x01: + return Read01.desc; + case 0x02: + return Read02.desc; + case 0x03: + return Read03.desc; + case 0x04: + return Read04.desc; + case 0x05: + return Write05.desc; + case 0x06: + return Write06.desc; + case 0x0F: + return Write0F.desc; + case 0x10: + return Write10.desc; + + default: + return "UNKOWN"; + } + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisCache.java b/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisCache.java new file mode 100644 index 0000000..32c2329 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisCache.java @@ -0,0 +1,792 @@ +package com.bnhz.common.core.redis; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.utils.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.BooleanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.*; +import org.springframework.data.redis.support.atomic.RedisAtomicLong; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.regex.Pattern.compile; + +/** + * spring redis 工具类 + * + * @author ruoyi + **/ +@SuppressWarnings(value = {"unchecked", "rawtypes"}) +@Component +@Slf4j +public class RedisCache { + @Autowired + public RedisTemplate redisTemplate; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + */ + public void setCacheObject(final String key, final T value) { + redisTemplate.opsForValue().set(key, value); + } + + /** + * 缓存基本的对象,Integer、String、实体类等 + * + * @param key 缓存的键值 + * @param value 缓存的值 + * @param timeout 时间 + * @param timeUnit 时间颗粒度 + */ + public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout) { + return expire(key, timeout, TimeUnit.SECONDS); + } + + /** + * 设置有效时间 + * + * @param key Redis键 + * @param timeout 超时时间 + * @param unit 时间单位 + * @return true=设置成功;false=设置失败 + */ + public boolean expire(final String key, final long timeout, final TimeUnit unit) { + return redisTemplate.expire(key, timeout, unit); + } + + /** + * 获取有效时间 + * + * @param key Redis键 + * @return 有效时间 + */ + public long getExpire(final String key) { + return redisTemplate.getExpire(key); + } + + /** + * 判断 key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public Boolean hasKey(String key) { + return redisTemplate.hasKey(key); + } + + /** + * 获得缓存的基本对象。 + * + * @param key 缓存键值 + * @return 缓存键值对应的数据 + */ + public T getCacheObject(final String key) { + ValueOperations operation = redisTemplate.opsForValue(); + return operation.get(key); + } + + /** + * 删除单个对象 + * + * @param key + */ + public boolean deleteObject(final String key) { + return redisTemplate.delete(key); + } + + /** + * 删除集合对象 + * + * @param collection 多个对象 + * @return + */ + public boolean deleteObject(final Collection collection) { + return redisTemplate.delete(collection) > 0; + } + + /** + * 缓存List数据 + * + * @param key 缓存的键值 + * @param dataList 待缓存的List数据 + * @return 缓存的对象 + */ + public long setCacheList(final String key, final List dataList) { + Long count = redisTemplate.opsForList().rightPushAll(key, dataList); + return count == null ? 0 : count; + } + + /** + * 获得缓存的list对象 + * + * @param key 缓存的键值 + * @return 缓存键值对应的数据 + */ + public List getCacheList(final String key) { + return redisTemplate.opsForList().range(key, 0, -1); + } + + /** + * 缓存Set + * + * @param key 缓存键值 + * @param dataSet 缓存的数据 + * @return 缓存数据的对象 + */ + public BoundSetOperations setCacheSet(final String key, final Set dataSet) { + BoundSetOperations setOperation = redisTemplate.boundSetOps(key); + Iterator it = dataSet.iterator(); + while (it.hasNext()) { + setOperation.add(it.next()); + } + return setOperation; + } + + /** + * 获得缓存的set + * + * @param key + * @return + */ + public Set getCacheSet(final String key) { + return redisTemplate.opsForSet().members(key); + } + + /** + * 缓存Map + * + * @param key + * @param dataMap + */ + public void setCacheMap(final String key, final Map dataMap) { + if (dataMap != null) { + redisTemplate.opsForHash().putAll(key, dataMap); + } + } + + /** + * 获得缓存的Map + * + * @param key + * @return + */ + public Map getCacheMap(final String key) { + return redisTemplate.opsForHash().entries(key); + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setCacheMapValue(final String key, final String hKey, final T value) { + redisTemplate.opsForHash().put(key, hKey, value); + } + + /** + * 获取Hash中的数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return Hash中的对象 + */ + public T getCacheMapValue(final String key, final String hKey) { + HashOperations opsForHash = redisTemplate.opsForHash(); + return opsForHash.get(key, hKey); + } + + /** + * 获取多个Hash中的数据 + * + * @param key Redis键 + * @param hKeys Hash键集合 + * @return Hash对象集合 + */ + public List getMultiCacheMapValue(final String key, final Collection hKeys) { + return redisTemplate.opsForHash().multiGet(key, hKeys); + } + + /** + * 删除Hash中的某条数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @return 是否成功 + */ + public boolean deleteCacheMapValue(final String key, final String hKey) { + return redisTemplate.opsForHash().delete(key, hKey) > 0; + } + + /** + * 获得缓存的基本对象列表 + * + * @param pattern 字符串前缀 + * @return 对象列表 + */ + public Collection keys(final String pattern) { + return redisTemplate.keys(pattern); + } + + /** + * 是否存在key + * + * @param key 缓存key + * @return true:存在key ;false:key不存在或者已过期 + */ + public boolean containsKey(String key) { + return redisTemplate.hasKey(key); + } + + + /** + * 递增 + * + * @param key 键 + * @param delta 要增加几(大于0) + * @return + */ + public long incr(String key, long delta) { + if (delta < 0) { + throw new RuntimeException("递增因子必须大于0"); + } + return redisTemplate.opsForValue().increment(key, delta); + } + + /** + * redis 计数器自增 + * + * @param key key + * @param liveTime 过期时间,null不设置过期时间 + * @return 自增数 + */ + public Long incr2(String key, long liveTime) { + RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory()); + Long increment = entityIdCounter.getAndIncrement(); + + if (increment == 0 && liveTime > 0) {//初始设置过期时间 + entityIdCounter.expire(liveTime, TimeUnit.HOURS); + } + + return increment; + } + + /** + * 将数据放入set缓存 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sAdd(String key, Object... values) { + try { + return redisTemplate.opsForSet().add(key, values); + } catch (Exception e) { + e.printStackTrace(); + return 0; + } + } + + /** + * 将set数据放入缓存 + * + * @param key 键 + * @param time 时间(秒) + * @param values 值 可以是多个 + * @return 成功个数 + */ + public long sSetAndTime(String key, long time, Object... values) { + try { + Long count = redisTemplate.opsForSet().add(key, values); + if (time > 0) expire(key, time); + return count; + } catch (Exception e) { + e.printStackTrace(); + return 0; + } + } + + /** + * 移除set集合值为value的 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 移除的个数 + */ + public long setRemove(String key, Object... values) { + try { + Long count = redisTemplate.opsForSet().remove(key, values); + return count; + } catch (Exception e) { + e.printStackTrace(); + return 0; + } + } + + /** + * 添加一个元素, zset与set最大的区别就是每个元素都有一个score,因此有个排序的辅助功能; zadd + * + * @param key 键 + * @param value 值 + * @param score 分数 + */ + public boolean zSetAdd(String key, String value, double score) { + try { + Boolean aBoolean = stringRedisTemplate.opsForZSet().add(key, value, score); + return BooleanUtils.isTrue(aBoolean); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * 移除一个zset有序集合的key的一个或者多个值 + * zrem key member [member ...] :移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。当 key 存在但不是有序集类型时,返回一个错误。 + * + * @param key 集合的键key + * @param values 需要移除的value + * @return + */ + public boolean zRem(String key, Object... values) { + try { + Long aLong = stringRedisTemplate.opsForZSet().remove(key, values); + return aLong != null ? true : false; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + /** + * 移除有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。 + * + * @param key String + * @param start double 最小score + * @param end double 最大score + */ + public Long zRemBySocre(String key, double start, double end) { + try { + return stringRedisTemplate.opsForZSet().removeRangeByScore(key, start, end); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 判断value在zset中的排名 zrank命令 + * + * @param key 键 + * @param value 值 + * @return score 越小排名越高; + */ + public Long zRank(String key, String value) { + try { + return stringRedisTemplate.opsForZSet().rank(key, value); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 查询zSet集合中指定顺序的值, 0 -1 表示获取全部的集合内容 zrange + * + * @param key 键 + * @param start 开始 + * @param end 结束 + * @return 返回有序的集合,score小的在前面 + */ + public Set zRange(String key, int start, int end) { + try { + return stringRedisTemplate.opsForZSet().range(key, start, end); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。 + * 有序集成员按 score 值递增(从小到大)次序排列。 + * + * @param key String + * @param start double 最小score + * @param end double 最大score + */ + public Set zRangeByScore(String key, double start, double end) { + try { + return stringRedisTemplate.opsForZSet().rangeByScore(key, start, end); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 返回set集合的长度 + * + * @param key + * @return + */ + public Long zSize(String key) { + try { + return stringRedisTemplate.opsForZSet().zCard(key); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 根据前缀获取所有的key + * 例如:pro_* + */ + public Set getListKeyByPrefix(String prefix) { + Set keys = redisTemplate.keys(prefix.concat("*")); + return keys; + } + + /** + * 匹配获取键值对,ScanOptions.NONE为获取全部键对 + * + * @param key + * @param options + * @return + */ + public Cursor> hashScan(String key, ScanOptions options) { + return redisTemplate.opsForHash().scan(key, options); + } + + /** + * 获取所有键值对集合 + * + * @param key + */ + public Map hashEntity(String key) { + return redisTemplate.boundHashOps(key).entries(); + } + + /** + * 以map集合的形式添加键值对 + * + * @param key + * @param maps + */ + public void hashPutAll(String key, Map maps) { + redisTemplate.opsForHash().putAll(key, maps); + } + + /** + * 以map集合的形式添加键值对 + * + * @param key + * @param maps + */ + public void hashPutAllObj(String key, Map maps) { + redisTemplate.opsForHash().putAll(key, maps); + } + + /** + * 批量获取设备物模型值 + * + * @param keys 键的集合 + * @param hkeyCondition 筛选字段 + * @return + */ + public Map hashGetAllByKeys(Set keys, String hkeyCondition) { + return (Map) redisTemplate.execute((RedisCallback) con -> { + Iterator it = keys.iterator(); + Map mapList = new HashMap<>(); + while (it.hasNext()) { + String key = it.next(); + Map result = con.hGetAll(key.getBytes()); + Map ans; + if (CollectionUtils.isEmpty(result)) { + return new HashMap<>(0); + } + ans = new HashMap<>(result.size()); + for (Map.Entry entry : result.entrySet()) { + String field = new String((byte[]) entry.getKey()); + if (!"".equals(hkeyCondition)) { + if (field.endsWith(hkeyCondition)) { + ans.put(new String((byte[]) entry.getKey()), new String((byte[]) entry.getValue())); + } + } else { + ans.put(new String((byte[]) entry.getKey()), new String((byte[]) entry.getValue())); + } + } + mapList.put(key, ans); + } + return mapList; + }); + } + + /** + * 批量获取匹配触发器的物模型值(定时告警使用) + * + * @param keys 键的集合 + * @param operator 操作符 + * @param triggerValue 触发的值 + * @return + */ + public Map hashGetAllMatchByKeys(Set keys, String operator, String id, String triggerValue, String modelIndex) { + // 数组或数组对象拼接id和获取值索引 + String matchId; + int indexValue; + if (id.startsWith("array_")) { + int index = id.indexOf("_", id.indexOf("_") + 1); + matchId = id.substring(index + 1); + List list = StringUtils.str2List(id, "_", true, true); + indexValue = Integer.parseInt(list.get(1)); + } else { + indexValue = -1; + matchId = id; + } + return (Map) redisTemplate.execute((RedisCallback) con -> { + Iterator it = keys.iterator(); + Map mapList = new HashMap<>(); + while (it.hasNext()) { + String key = it.next(); + Map result = con.hGetAll(key.getBytes()); + if (CollectionUtils.isEmpty(result)) { + return new HashMap<>(0); + } + for (Map.Entry entry : result.entrySet()) { + String field = new String((byte[]) entry.getKey()); + // 获取物模型值并且匹配规则,获取值的类型和匹配规则后续还要仔细测了然后优化 + if (field.equals(matchId) || field.equals(matchId + "#V")) { + String valueStr = new String((byte[]) entry.getValue()); + JSONObject jsonObject = JSONObject.parseObject((String) JSON.parse(valueStr)); + String value = (String) jsonObject.get("value"); + // 数组或数组对象元素索引 + if (indexValue >= 0) { + List list = StringUtils.str2List(value, ",", true, true); + value = org.apache.commons.collections4.CollectionUtils.isEmpty(list) ? "" : list.get(indexValue); + } + if (ruleResult(operator, value, triggerValue)) { + mapList.put(key, value); + } + } + } + } + return mapList; + }); + } + + /** + * 批量获取匹配触发器的物模型值 + * + * @param productId productId + * @param operator 操作符 + * @param triggerValue 触发的值 + * @return + */ + public Map CheckMatchByProductId(Long productId, String operator, String id, String triggerValue) { + Set keys = getListKeyByPrefix("TSLV:" + productId); + return (Map) redisTemplate.execute((RedisCallback) con -> { + Iterator it = keys.iterator(); + Map mapList = new HashMap<>(); + while (it.hasNext()) { + String key = it.next(); + String value = CheckMatchByCacheKey(key,operator,id,triggerValue); + if(!Objects.equals(value, "")) { + mapList.put(key,value); + } + } + return mapList; + }); + } + + /** + * 获取匹配触发器的物模型值 + * + * @param cacheKey 设备key + * @param operator 操作符 + * @param triggerValue 触发的值 + * @return + */ + public String CheckMatchByCacheKey(String cacheKey, String operator, String id, String triggerValue) { + String cacheValue = getCacheMapValue(cacheKey, id); + Map result = JSON.parseObject(cacheValue, Map.class); + if (CollectionUtils.isEmpty(result)) { + return ""; + } + for (Map.Entry entry : result.entrySet()) { + String field = (String) entry.getKey(); + if (field.equals("value")) { + String value = (String) entry.getValue(); + value = value.replace("\"", ""); + if (ruleResult(operator, value, triggerValue)) { + return value; + } + } + } + return ""; + } + + /** + * 根据key集合获取字符串 + * + * @param keys 键的集合 + * @return + */ + public Map getStringAllByKeys(Set keys) { + return (Map) redisTemplate.execute((RedisCallback) con -> { + Iterator it = keys.iterator(); + Map mapList = new HashMap<>(); + while (it.hasNext()) { + String key = it.next(); + byte[] result = con.get(key.getBytes()); + if (result == null) { + return new HashMap<>(0); + } + String ans = new String((byte[]) result); + mapList.put(key, ans); + } + return mapList; + }); + } + + /** + * 根据条件返回所有键 + * + * @param query + * @return + */ + public List scan(String query) { + Set keys = (Set) redisTemplate.execute((RedisCallback>) connection -> { + Set keysTmp = new HashSet<>(); + Cursor cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match("*" + query + "*").count(1000).build()); + while (cursor.hasNext()) { + keysTmp.add(new String(cursor.next())); + } + return keysTmp; + }); + return new ArrayList<>(keys); + } + + /** + * 规则匹配结果 + * + * @param operator 操作符 + * @param value 上报的值 + * @param triggerValue 触发器的值 + * @return + */ + private boolean ruleResult(String operator, String value, String triggerValue) { + boolean result = false; + if ("".equals(value)) { + return result; + } + // 操作符比较 + switch (operator) { + case "=": + result = value.equals(triggerValue); + break; + case "!=": + result = !value.equals(triggerValue); + break; + case ">": + if (isNumeric(value) && isNumeric(triggerValue)) { + result = Double.parseDouble(value) > Double.parseDouble(triggerValue); + } + break; + case "<": + if (isNumeric(value) && isNumeric(triggerValue)) { + result = Double.parseDouble(value) < Double.parseDouble(triggerValue); + } + break; + case ">=": + if (isNumeric(value) && isNumeric(triggerValue)) { + result = Double.parseDouble(value) >= Double.parseDouble(triggerValue); + } + break; + case "<=": + if (isNumeric(value) && isNumeric(triggerValue)) { + result = Double.parseDouble(value) <= Double.parseDouble(triggerValue); + } + break; + case "contain": + result = value.contains(triggerValue); + break; + case "notcontain": + result = !value.contains(triggerValue); + break; + default: + break; + } + return result; + } + + /** + * 判断字符串是否为整数或小数 + */ + private boolean isNumeric(String str) { + Pattern pattern = compile("[0-9]*\\.?[0-9]+"); + Matcher isNum = pattern.matcher(str); + if (!isNum.matches()) { + return false; + } + return true; + } + + public void publish(Object message, String channel) { + try { + redisTemplate.convertAndSend(channel, message); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 往Hash中存入数据 + * + * @param key Redis键 + * @param hKey Hash键 + * @param value 值 + */ + public void setHashValue(final String key, final String hKey, final T value) { + redisTemplate.opsForHash().put(key, hKey, value); + } + + + /** + * 删除Hash中的数据 + * + * @param key + * @param hkey + */ + public void delHashValue(final String key, final String hkey) { + HashOperations hashOperations = redisTemplate.opsForHash(); + hashOperations.delete(key, hkey); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisKeyBuilder.java b/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisKeyBuilder.java new file mode 100644 index 0000000..560b5c3 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisKeyBuilder.java @@ -0,0 +1,100 @@ +package com.bnhz.common.core.redis; + +import com.bnhz.common.constant.BnhzConstant; + +/** + * 缓存key生成器 + * + * @author bill + */ +public class RedisKeyBuilder { + + /**设备在线列表缓存key*/ + public static String buildDeviceOnlineListKey(){ + return BnhzConstant.REDIS.DEVICE_ONLINE_LIST; + } + + /**设备实时数据key*/ + public static String buildDeviceRtCacheKey(String serialNumber){ + return BnhzConstant.REDIS.DEVICE_RUNTIME_DATA + serialNumber; + } + + /** + * 设备通讯协议参数 + */ + public static String buildDeviceRtParamsKey(String serialNumber){ + return BnhzConstant.REDIS.DEVICE_PROTOCOL_PARAM + serialNumber; + } + + /**固件版本缓存key*/ + public static String buildFirmwareCachedKey(Long firmwareId){ + return BnhzConstant.REDIS.FIRMWARE_VERSION + firmwareId; + } + + /**属性读取回调缓存key*/ + public static String buildPropReadCacheKey(String serialNumber){ + return BnhzConstant.REDIS.PROP_READ_STORE + serialNumber; + } + + /** + * 物模型值命名缓存key + * Key:TSLV:{productId}_{deviceNumber} HKey:{identity#V/identity#S/identity#M/identity#N} + */ + public static String buildTSLVCacheKey(Long productId,String serialNumber){ + return BnhzConstant.REDIS.DEVICE_PRE_KEY + productId + "_" + serialNumber.toUpperCase(); + } + + /** + * 物模型缓存key + * 物模型命名空间:Key:TSL:{productId} hkey: identity value: thingsModel + */ + public static String buildTSLCacheKey(Long productId){ + return BnhzConstant.REDIS.TSL_PRE_KEY + productId; + } + + /**录像缓存key*/ + public static String buildSipRecordinfoCacheKey(String recordKey){ + return BnhzConstant.REDIS.RECORDINFO_KEY + recordKey; + } + + /**设备id缓存key*/ + public static String buildSipDeviceidCacheKey(String id){ + return BnhzConstant.REDIS.DEVICEID_KEY + id; + } + /**ipCSEQ缓存key*/ + public static String buildStreamCacheKey(String steamId){ + return BnhzConstant.REDIS.STREAM_KEY + steamId; + } + + public static String buildStreamCacheKey(String deviceId, String channelId, String stream, String callId){ + return BnhzConstant.REDIS.STREAM_KEY + deviceId + ":" + channelId + ":" + stream + ":" + callId; + } + + /**ipCSEQ缓存key*/ + public static String buildSipCSEQCacheKey(String CSEQ){ + return BnhzConstant.REDIS.SIP_CSEQ_PREFIX + CSEQ; + } + + /**rule静默时间缓存key*/ + public static String buildSilentTimeacheKey(String key){ + return BnhzConstant.REDIS.RULE_SILENT_TIME + key; + } + + /**modbus指令缓存可以*/ + public static String buildModbusCacheKey(Long productId){ + return BnhzConstant.REDIS.POLL_MODBUS_KEY + productId; + } + + /*缓存设备下发指令消息ID*/ + public static String buildDownMessageIdCacheKey(String serialNumber){ + return BnhzConstant.REDIS.DEVICE_MESSAGE_ID + serialNumber; + } + + /** + * 缓存产品id,设备编号,协议编号 + */ + public static String buildDeviceMsgCacheKey(String serialNumber){ + return BnhzConstant.REDIS.DEVICE_MSG + serialNumber; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisKeyDefine.java b/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisKeyDefine.java new file mode 100644 index 0000000..cabb145 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisKeyDefine.java @@ -0,0 +1,113 @@ +package com.bnhz.common.core.redis; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; + +import java.time.Duration; + +/** + * Redis Key 定义类 + * + * @author bnhz + */ +@Data +public class RedisKeyDefine { + + @Getter + @AllArgsConstructor + public enum KeyTypeEnum { + + STRING("String"), + LIST("List"), + HASH("Hash"), + SET("Set"), + ZSET("Sorted Set"), + STREAM("Stream"), + PUBSUB("Pub/Sub"); + + /** + * 类型 + */ + @JsonValue + private final String type; + + } + + @Getter + @AllArgsConstructor + public enum TimeoutTypeEnum { + + FOREVER(1), // 永不超时 + DYNAMIC(2), // 动态超时 + FIXED(3); // 固定超时 + + /** + * 类型 + */ + @JsonValue + private final Integer type; + + } + + /** + * Key 模板 + */ + private final String keyTemplate; + /** + * Key 类型的枚举 + */ + private final KeyTypeEnum keyType; + /** + * Value 类型 + * + * 如果是使用分布式锁,设置为 {@link java.util.concurrent.locks.Lock} 类型 + */ + private final Class valueType; + /** + * 超时类型 + */ + private final TimeoutTypeEnum timeoutType; + /** + * 过期时间 + */ + private final Duration timeout; + /** + * 备注 + */ + private final String memo; + + private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, + TimeoutTypeEnum timeoutType, Duration timeout) { + this.memo = memo; + this.keyTemplate = keyTemplate; + this.keyType = keyType; + this.valueType = valueType; + this.timeout = timeout; + this.timeoutType = timeoutType; + // 添加注册表 + RedisKeyRegistry.add(this); + } + + public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, Duration timeout) { + this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout); + } + + public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, TimeoutTypeEnum timeoutType) { + this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO); + } + + /** + * 格式化 Key + * + * 注意,内部采用 {@link String#format(String, Object...)} 实现 + * + * @param args 格式化的参数 + * @return Key + */ + public String formatKey(Object... args) { + return String.format(keyTemplate, args); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisKeyRegistry.java b/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisKeyRegistry.java new file mode 100644 index 0000000..916392f --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/redis/RedisKeyRegistry.java @@ -0,0 +1,28 @@ +package com.bnhz.common.core.redis; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link RedisKeyDefine} 注册表 + */ +public class RedisKeyRegistry { + + /** + * Redis RedisKeyDefine 数组 + */ + private static final List DEFINES = new ArrayList<>(); + + public static void add(RedisKeyDefine define) { + DEFINES.add(define); + } + + public static List list() { + return DEFINES; + } + + public static int size() { + return DEFINES.size(); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/text/CharsetKit.java b/bnhz-common/src/main/java/com/bnhz/common/core/text/CharsetKit.java new file mode 100644 index 0000000..0c465f3 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/text/CharsetKit.java @@ -0,0 +1,86 @@ +package com.bnhz.common.core.text; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import com.bnhz.common.utils.StringUtils; + +/** + * 字符集工具类 + * + * @author ruoyi + */ +public class CharsetKit +{ + /** ISO-8859-1 */ + public static final String ISO_8859_1 = "ISO-8859-1"; + /** UTF-8 */ + public static final String UTF_8 = "UTF-8"; + /** GBK */ + public static final String GBK = "GBK"; + + /** ISO-8859-1 */ + public static final Charset CHARSET_ISO_8859_1 = Charset.forName(ISO_8859_1); + /** UTF-8 */ + public static final Charset CHARSET_UTF_8 = Charset.forName(UTF_8); + /** GBK */ + public static final Charset CHARSET_GBK = Charset.forName(GBK); + + /** + * 转换为Charset对象 + * + * @param charset 字符集,为空则返回默认字符集 + * @return Charset + */ + public static Charset charset(String charset) + { + return StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, String srcCharset, String destCharset) + { + return convert(source, Charset.forName(srcCharset), Charset.forName(destCharset)); + } + + /** + * 转换字符串的字符集编码 + * + * @param source 字符串 + * @param srcCharset 源字符集,默认ISO-8859-1 + * @param destCharset 目标字符集,默认UTF-8 + * @return 转换后的字符集 + */ + public static String convert(String source, Charset srcCharset, Charset destCharset) + { + if (null == srcCharset) + { + srcCharset = StandardCharsets.ISO_8859_1; + } + + if (null == destCharset) + { + destCharset = StandardCharsets.UTF_8; + } + + if (StringUtils.isEmpty(source) || srcCharset.equals(destCharset)) + { + return source; + } + return new String(source.getBytes(srcCharset), destCharset); + } + + /** + * @return 系统字符集编码 + */ + public static String systemCharset() + { + return Charset.defaultCharset().name(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/text/Convert.java b/bnhz-common/src/main/java/com/bnhz/common/core/text/Convert.java new file mode 100644 index 0000000..18b7a3f --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/text/Convert.java @@ -0,0 +1,1000 @@ +package com.bnhz.common.core.text; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.util.Set; +import com.bnhz.common.utils.StringUtils; +import org.apache.commons.lang3.ArrayUtils; + +/** + * 类型转换器 + * + * @author ruoyi + */ +public class Convert +{ + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static String toStr(Object value, String defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof String) + { + return (String) value; + } + return value.toString(); + } + + /** + * 转换为字符串
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static String toStr(Object value) + { + return toStr(value, null); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Character toChar(Object value, Character defaultValue) + { + if (null == value) + { + return defaultValue; + } + if (value instanceof Character) + { + return (Character) value; + } + + final String valueStr = toStr(value, null); + return StringUtils.isEmpty(valueStr) ? defaultValue : valueStr.charAt(0); + } + + /** + * 转换为字符
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Character toChar(Object value) + { + return toChar(value, null); + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Byte toByte(Object value, Byte defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Byte) + { + return (Byte) value; + } + if (value instanceof Number) + { + return ((Number) value).byteValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Byte.parseByte(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为byte
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Byte toByte(Object value) + { + return toByte(value, null); + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Short toShort(Object value, Short defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Short) + { + return (Short) value; + } + if (value instanceof Number) + { + return ((Number) value).shortValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Short.parseShort(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Short
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Short toShort(Object value) + { + return toShort(value, null); + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Number toNumber(Object value, Number defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Number) + { + return (Number) value; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return NumberFormat.getInstance().parse(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Number
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Number toNumber(Object value) + { + return toNumber(value, null); + } + + /** + * 转换为int
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Integer toInt(Object value, Integer defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Integer) + { + return (Integer) value; + } + if (value instanceof Number) + { + return ((Number) value).intValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Integer.parseInt(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为int
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Integer toInt(Object value) + { + return toInt(value, null); + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) + { + return toIntArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) + { + return toLongArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Integer[] {}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) + { + if (StringUtils.isEmpty(str)) + { + return new Long[] {}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) + { + final Long v = toLong(arr[i], null); + longs[i] = v; + } + return longs; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) + { + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) + { + return str.split(split); + } + + /** + * 转换为long
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Long toLong(Object value, Long defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Long) + { + return (Long) value; + } + if (value instanceof Number) + { + return ((Number) value).longValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).longValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为long
+ * 如果给定的值为null,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Long toLong(Object value) + { + return toLong(value, null); + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Double toDouble(Object value, Double defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Double) + { + return (Double) value; + } + if (value instanceof Number) + { + return ((Number) value).doubleValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + // 支持科学计数法 + return new BigDecimal(valueStr.trim()).doubleValue(); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为double
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Double toDouble(Object value) + { + return toDouble(value, null); + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Float toFloat(Object value, Float defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Float) + { + return (Float) value; + } + if (value instanceof Number) + { + return ((Number) value).floatValue(); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Float.parseFloat(valueStr.trim()); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Float
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Float toFloat(Object value) + { + return toFloat(value, null); + } + + /** + * 转换为boolean
+ * String支持的值为:true、false、yes、ok、no,1,0 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static Boolean toBool(Object value, Boolean defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof Boolean) + { + return (Boolean) value; + } + String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + valueStr = valueStr.trim().toLowerCase(); + switch (valueStr) + { + case "true": + case "yes": + case "ok": + case "1": + return true; + case "false": + case "no": + case "0": + return false; + default: + return defaultValue; + } + } + + /** + * 转换为boolean
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static Boolean toBool(Object value) + { + return toBool(value, null); + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * + * @param clazz Enum的Class + * @param value 值 + * @param defaultValue 默认值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value, E defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (clazz.isAssignableFrom(value.getClass())) + { + @SuppressWarnings("unchecked") + E myE = (E) value; + return myE; + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return Enum.valueOf(clazz, valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为Enum对象
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * + * @param clazz Enum的Class + * @param value 值 + * @return Enum + */ + public static > E toEnum(Class clazz, Object value) + { + return toEnum(clazz, value, null); + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value, BigInteger defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigInteger) + { + return (BigInteger) value; + } + if (value instanceof Long) + { + return BigInteger.valueOf((Long) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigInteger(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigInteger
+ * 如果给定的值为空,或者转换失败,返回默认值null
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigInteger toBigInteger(Object value) + { + return toBigInteger(value, null); + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @param defaultValue 转换错误时的默认值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value, BigDecimal defaultValue) + { + if (value == null) + { + return defaultValue; + } + if (value instanceof BigDecimal) + { + return (BigDecimal) value; + } + if (value instanceof Long) + { + return new BigDecimal((Long) value); + } + if (value instanceof Double) + { + return BigDecimal.valueOf((Double) value); + } + if (value instanceof Integer) + { + return new BigDecimal((Integer) value); + } + final String valueStr = toStr(value, null); + if (StringUtils.isEmpty(valueStr)) + { + return defaultValue; + } + try + { + return new BigDecimal(valueStr); + } + catch (Exception e) + { + return defaultValue; + } + } + + /** + * 转换为BigDecimal
+ * 如果给定的值为空,或者转换失败,返回默认值
+ * 转换失败不会报错 + * + * @param value 被转换的值 + * @return 结果 + */ + public static BigDecimal toBigDecimal(Object value) + { + return toBigDecimal(value, null); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @return 字符串 + */ + public static String utf8Str(Object obj) + { + return str(obj, CharsetKit.CHARSET_UTF_8); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charsetName 字符集 + * @return 字符串 + */ + public static String str(Object obj, String charsetName) + { + return str(obj, Charset.forName(charsetName)); + } + + /** + * 将对象转为字符串
+ * 1、Byte数组和ByteBuffer会被转换为对应字符串的数组 2、对象数组会调用Arrays.toString方法 + * + * @param obj 对象 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(Object obj, Charset charset) + { + if (null == obj) + { + return null; + } + + if (obj instanceof String) + { + return (String) obj; + } + else if (obj instanceof byte[]) + { + return str((byte[]) obj, charset); + } + else if (obj instanceof Byte[]) + { + byte[] bytes = ArrayUtils.toPrimitive((Byte[]) obj); + return str(bytes, charset); + } + else if (obj instanceof ByteBuffer) + { + return str((ByteBuffer) obj, charset); + } + return obj.toString(); + } + + /** + * 将byte数组转为字符串 + * + * @param bytes byte数组 + * @param charset 字符集 + * @return 字符串 + */ + public static String str(byte[] bytes, String charset) + { + return str(bytes, StringUtils.isEmpty(charset) ? Charset.defaultCharset() : Charset.forName(charset)); + } + + /** + * 解码字节码 + * + * @param data 字符串 + * @param charset 字符集,如果此字段为空,则解码的结果取决于平台 + * @return 解码后的字符串 + */ + public static String str(byte[] data, Charset charset) + { + if (data == null) + { + return null; + } + + if (null == charset) + { + return new String(data); + } + return new String(data, charset); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, String charset) + { + if (data == null) + { + return null; + } + + return str(data, Charset.forName(charset)); + } + + /** + * 将编码的byteBuffer数据转换为字符串 + * + * @param data 数据 + * @param charset 字符集,如果为空使用当前系统字符集 + * @return 字符串 + */ + public static String str(ByteBuffer data, Charset charset) + { + if (null == charset) + { + charset = Charset.defaultCharset(); + } + return charset.decode(data).toString(); + } + + // ----------------------------------------------------------------------- 全角半角转换 + /** + * 半角转全角 + * + * @param input String. + * @return 全角字符串. + */ + public static String toSBC(String input) + { + return toSBC(input, null); + } + + /** + * 半角转全角 + * + * @param input String + * @param notConvertSet 不替换的字符集合 + * @return 全角字符串. + */ + public static String toSBC(String input, Set notConvertSet) + { + char[] c = input.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == ' ') + { + c[i] = '\u3000'; + } + else if (c[i] < '\177') + { + c[i] = (char) (c[i] + 65248); + + } + } + return new String(c); + } + + /** + * 全角转半角 + * + * @param input String. + * @return 半角字符串 + */ + public static String toDBC(String input) + { + return toDBC(input, null); + } + + /** + * 替换全角为半角 + * + * @param text 文本 + * @param notConvertSet 不替换的字符集合 + * @return 替换后的字符 + */ + public static String toDBC(String text, Set notConvertSet) + { + char[] c = text.toCharArray(); + for (int i = 0; i < c.length; i++) + { + if (null != notConvertSet && notConvertSet.contains(c[i])) + { + // 跳过不替换的字符 + continue; + } + + if (c[i] == '\u3000') + { + c[i] = ' '; + } + else if (c[i] > '\uFF00' && c[i] < '\uFF5F') + { + c[i] = (char) (c[i] - 65248); + } + } + String returnString = new String(c); + + return returnString; + } + + /** + * 数字金额大写转换 先写个完整的然后将如零拾替换成零 + * + * @param n 数字 + * @return 中文大写数字 + */ + public static String digitUppercase(double n) + { + String[] fraction = { "角", "分" }; + String[] digit = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; + String[][] unit = { { "元", "万", "亿" }, { "", "拾", "佰", "仟" } }; + + String head = n < 0 ? "负" : ""; + n = Math.abs(n); + + String s = ""; + for (int i = 0; i < fraction.length; i++) + { + s += (digit[(int) (Math.floor(n * 10 * Math.pow(10, i)) % 10)] + fraction[i]).replaceAll("(零.)+", ""); + } + if (s.length() < 1) + { + s = "整"; + } + int integerPart = (int) Math.floor(n); + + for (int i = 0; i < unit[0].length && integerPart > 0; i++) + { + String p = ""; + for (int j = 0; j < unit[1].length && n > 0; j++) + { + p = digit[integerPart % 10] + unit[1][j] + p; + integerPart = integerPart / 10; + } + s = p.replaceAll("(零.)*零$", "").replaceAll("^$", "零") + unit[0][i] + s; + } + return head + s.replaceAll("(零.)*零元", "元").replaceFirst("(零.)+", "").replaceAll("(零.)+", "零").replaceAll("^整$", "零元整"); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/text/IntArrayValuable.java b/bnhz-common/src/main/java/com/bnhz/common/core/text/IntArrayValuable.java new file mode 100644 index 0000000..abbd8a2 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/text/IntArrayValuable.java @@ -0,0 +1,13 @@ +package com.bnhz.common.core.text; + +/** + * 可生成 Int 数组的接口 + */ +public interface IntArrayValuable { + + /** + * @return int 数组 + */ + int[] array(); + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/text/KeyValue.java b/bnhz-common/src/main/java/com/bnhz/common/core/text/KeyValue.java new file mode 100644 index 0000000..301015e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/text/KeyValue.java @@ -0,0 +1,20 @@ +package com.bnhz.common.core.text; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Key Value 的键值对 + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KeyValue { + + private K key; + private V value; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/text/StrFormatter.java b/bnhz-common/src/main/java/com/bnhz/common/core/text/StrFormatter.java new file mode 100644 index 0000000..b8e0d66 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/text/StrFormatter.java @@ -0,0 +1,92 @@ +package com.bnhz.common.core.text; + +import com.bnhz.common.utils.StringUtils; + +/** + * 字符串格式化 + * + * @author ruoyi + */ +public class StrFormatter +{ + public static final String EMPTY_JSON = "{}"; + public static final char C_BACKSLASH = '\\'; + public static final char C_DELIM_START = '{'; + public static final char C_DELIM_END = '}'; + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) + { + if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) + { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + // 初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0; + int delimIndex;// 占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) + { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) + { + if (handledPosition == 0) + { + return strPattern; + } + else + { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } + else + { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) + { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) + { + // 转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + else + { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } + else + { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(Convert.utf8Str(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // 加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/NeuronModel.java b/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/NeuronModel.java new file mode 100644 index 0000000..e6972ea --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/NeuronModel.java @@ -0,0 +1,44 @@ +package com.bnhz.common.core.thingsModel; + +import com.alibaba.fastjson2.JSONObject; +import lombok.Data; + +import java.util.Date; +import java.util.List; + +/** + * Neuron-JSON格式协议 + * @author gsb + * @date 2023/5/31 16:36 + */ +@Data +public class NeuronModel { + + /** + * 产品节点 + */ + private String node; + + /** + * 网关编号 + */ + private String group; + /** + * 上报时间 + */ + private Date timestamp; + + /** + * 上报JSON + */ + private JSONObject values; + /** + * 错误集合 + */ + private JSONObject errors; + /** + * 上报属性值集合 + */ + private List items; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/SceneThingsModelItem.java b/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/SceneThingsModelItem.java new file mode 100644 index 0000000..426638a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/SceneThingsModelItem.java @@ -0,0 +1,38 @@ +package com.bnhz.common.core.thingsModel; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +/** + * 物模型值的项 + * + * @author kerwincui + * @date 2021-12-16 + */ +@AllArgsConstructor +@Builder +@Data +public class SceneThingsModelItem +{ + /** 物模型唯一标识符 */ + private String id; + + /** 物模型值 */ + private String value; + + /** 类型:1=属性, 2=功能,3=事件, 4=设备升级,5=设备上线,6=设备下线 ,*/ + private int type; + + /** 脚本ID */ + private String stripId; + + /** 场景ID*/ + private Long sceneId; + + /** 产品ID */ + private Long productId; + + /** 设备编号 */ + private String DeviceNumber; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/ThingsModelRuleItem.java b/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/ThingsModelRuleItem.java new file mode 100644 index 0000000..302f00e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/ThingsModelRuleItem.java @@ -0,0 +1,29 @@ +package com.bnhz.common.core.thingsModel; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Builder; +import lombok.Data; + +import java.util.Date; +@Data +@Builder +public class ThingsModelRuleItem { + /** 物模型唯一标识符 */ + private String id; + + /** 物模型值 */ + private String value; + private String operator; + private String triggerValue; + + /** + * 更新时间 + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date ts; + + /** 备注 **/ + private String remark; + + private String timestamp; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/ThingsModelSimpleItem.java b/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/ThingsModelSimpleItem.java new file mode 100644 index 0000000..cab9892 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/ThingsModelSimpleItem.java @@ -0,0 +1,111 @@ +package com.bnhz.common.core.thingsModel; + +import com.bnhz.common.utils.DateUtils; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Builder; + +import java.util.Date; + +/** + * 物模型值的项 + * + * @author kerwincui + * @date 2021-12-16 + */ +@AllArgsConstructor +@Builder +public class ThingsModelSimpleItem +{ + /** 物模型唯一标识符 */ + private String id; + + /** 物模型值 */ + private String value; + + /** + * 更新时间 + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date ts; + + private Integer slaveId; + + /** 备注 **/ + private String remark; + + private String timestamp; + + private boolean isBit = false; + + public ThingsModelSimpleItem(String id, String value , String remark){ + this.id=id; + this.value=value; + this.remark=remark; + } + + public ThingsModelSimpleItem(String id, String value ,Integer slaveId, String remark){ + this.id=id; + this.value=value; + this.slaveId = slaveId; + this.remark=remark; + } + + public boolean isBit() { + return isBit; + } + + public void setBit(boolean bit) { + isBit = bit; + } + + public Integer getSlaveId() { + return slaveId; + } + + public void setSlaveId(Integer slaveId) { + this.slaveId = slaveId; + } + + public Date getTs() { + return ts; + } + + public void setTs(Date ts) { + this.ts = ts != null ? ts : DateUtils.getNowDate(); + } + + public ThingsModelSimpleItem(){} + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/ThingsModelValuesInput.java b/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/ThingsModelValuesInput.java new file mode 100644 index 0000000..7f9a3eb --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/core/thingsModel/ThingsModelValuesInput.java @@ -0,0 +1,159 @@ +package com.bnhz.common.core.thingsModel; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 设备输入物模型值参数 + * + * @author kerwincui + * @date 2021-12-16 + */ +public class ThingsModelValuesInput +{ + /** 产品ID **/ + private Long productId; + + private Long deviceId; + + /** 设备ID **/ + private String deviceNumber; + + /** 设备物模型值的字符串格式 **/ + private String stringValue; + + /** + * 1-属性,2-功能,3-事件 + */ + private int type; + + /** + * 模型id(如果是事件则有值) + */ + private Long modelId; + + /** + * 数据产生事件 + */ + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @JSONField(format = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime dataTime; + + @JSONField(format = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime = LocalDateTime.now(); + + /** 设备物模型值的集合 **/ + private List thingsModelSimpleItem; + + private Integer slaveId; + + /** + * 推送的topic + */ + private String topic; + + /** + * 推送的批次号 + */ + private Long batchNo; + + public LocalDateTime getCreateTime() { + return createTime; + } + + public void setCreateTime(LocalDateTime createTime) { + this.createTime = createTime; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Integer getSlaveId() { + return slaveId; + } + + public void setSlaveId(Integer slaveId) { + this.slaveId = slaveId; + } + + public Long getDeviceId() { + return deviceId; + } + + public void setDeviceId(Long deviceId) { + this.deviceId = deviceId; + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + public String getDeviceNumber() { + return deviceNumber; + } + + public void setDeviceNumber(String deviceNumber) { + this.deviceNumber = deviceNumber; + } + + public List getThingsModelValueRemarkItem() { + return thingsModelSimpleItem; + } + + public void setThingsModelValueRemarkItem(List thingsModelSimpleItem) { + this.thingsModelSimpleItem = thingsModelSimpleItem; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public Long getModelId() { + return modelId; + } + + public void setModelId(Long modelId) { + this.modelId = modelId; + } + + public LocalDateTime getDataTime() { + return dataTime; + } + + public void setDataTime(LocalDateTime dataTime) { + this.dataTime = dataTime; + } + + public Long getBatchNo() { + return batchNo; + } + + public void setBatchNo(Long batchNo) { + this.batchNo = batchNo; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/BusinessStatus.java b/bnhz-common/src/main/java/com/bnhz/common/enums/BusinessStatus.java new file mode 100644 index 0000000..07225ee --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/BusinessStatus.java @@ -0,0 +1,20 @@ +package com.bnhz.common.enums; + +/** + * 操作状态 + * + * @author ruoyi + * + */ +public enum BusinessStatus +{ + /** + * 成功 + */ + SUCCESS, + + /** + * 失败 + */ + FAIL, +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/BusinessType.java b/bnhz-common/src/main/java/com/bnhz/common/enums/BusinessType.java new file mode 100644 index 0000000..bcbfb0a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/BusinessType.java @@ -0,0 +1,59 @@ +package com.bnhz.common.enums; + +/** + * 业务操作类型 + * + * @author ruoyi + */ +public enum BusinessType +{ + /** + * 其它 + */ + OTHER, + + /** + * 新增 + */ + INSERT, + + /** + * 修改 + */ + UPDATE, + + /** + * 删除 + */ + DELETE, + + /** + * 授权 + */ + GRANT, + + /** + * 导出 + */ + EXPORT, + + /** + * 导入 + */ + IMPORT, + + /** + * 强退 + */ + FORCE, + + /** + * 生成代码 + */ + GENCODE, + + /** + * 清空数据 + */ + CLEAN, +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/CommonStatusEnum.java b/bnhz-common/src/main/java/com/bnhz/common/enums/CommonStatusEnum.java new file mode 100644 index 0000000..8a2ab63 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/CommonStatusEnum.java @@ -0,0 +1,36 @@ +package com.bnhz.common.enums; + +import com.bnhz.common.core.text.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 通用状态枚举 + + */ +@Getter +@AllArgsConstructor +public enum CommonStatusEnum implements IntArrayValuable { + + ENABLE(0, "开启"), + DISABLE(1, "关闭"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray(); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/DataEnum.java b/bnhz-common/src/main/java/com/bnhz/common/enums/DataEnum.java new file mode 100644 index 0000000..c88d7f9 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/DataEnum.java @@ -0,0 +1,37 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * @author gsb + * @date 2023/6/3 14:09 + */ +@Getter +@AllArgsConstructor +public enum DataEnum { + + DECIMAL("decimal", "十进制"), + DOUBLE("double", "双精度"), + ENUM("enum","枚举"), + BOOLEAN("bool","布尔类型"), + INTEGER("integer","整形"), + OBJECT("object", "对象"), + STRING("string","字符串"), + ARRAY("array","数组"); + + String type; + String msg; + + public static DataEnum convert(String type){ + for (DataEnum value : DataEnum.values()) { + if (Objects.equals(value.type, type)){ + return value; + } + } + return DataEnum.STRING; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/DataSourceType.java b/bnhz-common/src/main/java/com/bnhz/common/enums/DataSourceType.java new file mode 100644 index 0000000..6289bcb --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/DataSourceType.java @@ -0,0 +1,19 @@ +package com.bnhz.common.enums; + +/** + * 数据源 + * + * @author ruoyi + */ +public enum DataSourceType +{ + /** + * 主库 + */ + MASTER, + + /** + * 从库 + */ + SLAVE +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/DeviceStatus.java b/bnhz-common/src/main/java/com/bnhz/common/enums/DeviceStatus.java new file mode 100644 index 0000000..e90f9d9 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/DeviceStatus.java @@ -0,0 +1,36 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum DeviceStatus { + + UNACTIVATED(1,"NOTACTIVE","未激活"), + FORBIDDEN(2,"DISABLE","禁用"), + ONLINE(3,"ONLINE","在线"), + OFFLINE(4,"OFFLINE","离线"); + + private int type; + private String code; + private String description; + + public static DeviceStatus convert(int type){ + for (DeviceStatus value : DeviceStatus.values()) { + if (value.type == type){ + return value; + } + } + return null; + } + + public static DeviceStatus convert(String code){ + for (DeviceStatus value : DeviceStatus.values()) { + if (value.code.equals(code)){ + return value; + } + } + return null; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/ExceptionCode.java b/bnhz-common/src/main/java/com/bnhz/common/enums/ExceptionCode.java new file mode 100644 index 0000000..a812748 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/ExceptionCode.java @@ -0,0 +1,22 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author gsb + * @date 2022/11/3 11:05 + */ +@Getter +@AllArgsConstructor +public enum ExceptionCode { + + SUCCESS(200,"成功"), + TIMEOUT(400,"超时"), + OFFLINE(404,"设备断线"), + FAIL(500,"失败"); + ; + + public int code; + public String desc; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/FunctionReplyStatus.java b/bnhz-common/src/main/java/com/bnhz/common/enums/FunctionReplyStatus.java new file mode 100644 index 0000000..8529ecd --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/FunctionReplyStatus.java @@ -0,0 +1,21 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 设备回调状态 + * @author bill + */ +@Getter +@AllArgsConstructor +public enum FunctionReplyStatus { + SUCCESS(200,"设备执行成功"), + FAIl(201,"指令执行失败"), + UNKNOWN(204,"设备超时未回复"), + NORELY(203, "指令下发成功"); + + int code; + String message; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/GlobalErrorCodeConstants.java b/bnhz-common/src/main/java/com/bnhz/common/enums/GlobalErrorCodeConstants.java new file mode 100644 index 0000000..6ecfcff --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/GlobalErrorCodeConstants.java @@ -0,0 +1,55 @@ +package com.bnhz.common.enums; + + +import com.bnhz.common.exception.ErrorCode; + +/** + * 全局错误码枚举 + * 0-999 系统异常编码保留 + * + * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status + * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 + * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 + * + * @author bnhz + */ +public interface GlobalErrorCodeConstants { + + ErrorCode SUCCESS = new ErrorCode(0, "成功"); + + // ========== 客户端错误段 ========== + + ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确"); + ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录"); + ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限"); + ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到"); + ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); + ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许 + ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试"); + + // ========== 服务端错误段 ========== + + ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); + ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启"); + + // ========== 自定义错误段 ========== + ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求 + ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作"); + + ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); + ErrorCode ONLY_EVENT = new ErrorCode(902, "暂时只支持事件"); + ErrorCode TCP_ONLY_EVENT = new ErrorCode(903, "仅限TCP协议模型才能拷贝"); + + + /** + * 是否为服务端错误,参考 HTTP 5XX 错误码段 + * + * @param code 错误码 + * @return 是否 + */ + static boolean isServerErrorCode(Integer code) { + return code != null + && code >= INTERNAL_SERVER_ERROR.getCode() && code <= INTERNAL_SERVER_ERROR.getCode() + 99; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/HttpMethod.java b/bnhz-common/src/main/java/com/bnhz/common/enums/HttpMethod.java new file mode 100644 index 0000000..8862c8b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/HttpMethod.java @@ -0,0 +1,36 @@ +package com.bnhz.common.enums; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.lang.Nullable; + +/** + * 请求方式 + * + * @author ruoyi + */ +public enum HttpMethod +{ + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; + + private static final Map mappings = new HashMap<>(16); + + static + { + for (HttpMethod httpMethod : values()) + { + mappings.put(httpMethod.name(), httpMethod); + } + } + + @Nullable + public static HttpMethod resolve(@Nullable String method) + { + return (method != null ? mappings.get(method) : null); + } + + public boolean matches(String method) + { + return (this == resolve(method)); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/IErrorCode.java b/bnhz-common/src/main/java/com/bnhz/common/enums/IErrorCode.java new file mode 100644 index 0000000..3bac029 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/IErrorCode.java @@ -0,0 +1,14 @@ +package com.bnhz.common.enums; + +/** + * 常用API返回对象接口 + */ +public interface IErrorCode { + + /**返回码*/ + int getCode(); + + /**返回信息*/ + String getMessage(); + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/JobType.java b/bnhz-common/src/main/java/com/bnhz/common/enums/JobType.java new file mode 100644 index 0000000..7107227 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/JobType.java @@ -0,0 +1,35 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +@Getter +@AllArgsConstructor +public enum JobType { + //1==设备定时,2=设备告警,3=场景联动 4=规则引擎 + Device(1), + DeviceAlert(2), + Scene(3), + RuleEngine(4); + private final Integer value; + + public static JobType fromValue(Integer value) { + for (JobType type : JobType.values()) { + if (Objects.equals(type.getValue(), value)) { + return type; + } + } + return null; + } + + public static String getName(Integer value) { + for (JobType type : JobType.values()) { + if (Objects.equals(type.getValue(), value)) { + return type.name(); + } + } + return null; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/LimitType.java b/bnhz-common/src/main/java/com/bnhz/common/enums/LimitType.java new file mode 100644 index 0000000..7c18f4e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/LimitType.java @@ -0,0 +1,20 @@ +package com.bnhz.common.enums; + +/** + * 限流类型 + * + * @author ruoyi + */ + +public enum LimitType +{ + /** + * 默认策略全局限流 + */ + DEFAULT, + + /** + * 根据请求者IP进行限流 + */ + IP +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/ModbusDataType.java b/bnhz-common/src/main/java/com/bnhz/common/enums/ModbusDataType.java new file mode 100644 index 0000000..dd123c4 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/ModbusDataType.java @@ -0,0 +1,38 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * @author gsb + * @date 2023/9/4 14:46 + */ +@Getter +@AllArgsConstructor +public enum ModbusDataType { + + + U_SHORT("ushort","16位 无符号"), + SHORT("short","16位 有符号"), + LONG_ABCD("long-ABCD","32位 有符号(ABCD)"), + LONG_CDAB("long-CDAB","32位 有符号(CDAB)"), + U_LONG_ABCD("ulong-ABCD","32位 无符号(ABCD)"), + U_LONG_CDAB("ulong-CDAB","32位 无符号(CDAB)"), + FLOAT_ABCD("float-ABCD","32位 浮点数(ABCD)"), + FLOAT_CDAB("float-CDAB","32位 浮点数(CDAB)"), + BIT("bit","位"); + + String type; + String msg; + + public static ModbusDataType convert(String type){ + for (ModbusDataType value : ModbusDataType.values()) { + if (Objects.equals(value.type,type)){ + return value; + } + } + return ModbusDataType.U_SHORT; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/NotifyChannelEnum.java b/bnhz-common/src/main/java/com/bnhz/common/enums/NotifyChannelEnum.java new file mode 100644 index 0000000..093c577 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/NotifyChannelEnum.java @@ -0,0 +1,46 @@ +package com.bnhz.common.enums; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @description: 通知渠道枚举 + * @author fastb + * @date 2023-12-16 17:00 + * @version 1.0 + */ +@Getter +@AllArgsConstructor +public enum NotifyChannelEnum { + + /** + * 确保唯一,不能重复 + */ + SMS("sms", "短信"), + VOICE("voice","语音"), + WECHAT("wechat","微信"), + DING_TALK("dingtalk","钉钉"), + EMAIL("email", "邮箱"); + + + /** + * 渠道类型 + */ + private String type; + + /** + * 描述 + */ + private String desc; + + public static NotifyChannelEnum getNotifyChannelEnum(String type) { + for (NotifyChannelEnum notifyChannelEnum : NotifyChannelEnum.values()) { + if (type.equals(notifyChannelEnum.type)) { + return notifyChannelEnum; + } + } + return null; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/NotifyChannelProviderEnum.java b/bnhz-common/src/main/java/com/bnhz/common/enums/NotifyChannelProviderEnum.java new file mode 100644 index 0000000..05a6a4a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/NotifyChannelProviderEnum.java @@ -0,0 +1,315 @@ +package com.bnhz.common.enums; + +import com.bnhz.common.core.notify.NotifyConfigVO; +import com.bnhz.common.core.notify.config.DingTalkConfigParams; +import com.bnhz.common.core.notify.config.EmailConfigParams; +import com.bnhz.common.core.notify.config.VoiceConfigParams; +import com.bnhz.common.core.notify.config.WeChatConfigParams; +import com.bnhz.common.core.notify.msg.*; +import com.bnhz.common.utils.StringUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.dromara.sms4j.aliyun.config.AlibabaConfig; +import org.dromara.sms4j.cloopen.config.CloopenConfig; +import org.dromara.sms4j.ctyun.config.CtyunConfig; +import org.dromara.sms4j.emay.config.EmayConfig; +import org.dromara.sms4j.huawei.config.HuaweiConfig; +import org.dromara.sms4j.jdcloud.config.JdCloudConfig; +import org.dromara.sms4j.netease.config.NeteaseConfig; +import org.dromara.sms4j.tencent.config.TencentConfig; +import org.dromara.sms4j.yunpian.config.YunpianConfig; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author fastb + * @version 1.0 + * @description: 通知渠道枚举 + * @date 2023-12-18 11:52 + */ +@Getter +@AllArgsConstructor +@NoArgsConstructor +public enum NotifyChannelProviderEnum { + + /**** 短信 ******/ + SMS_ALIBABA("sms", "alibaba", "阿里云短信", AlibabaConfig.class, AlibabaConfig.class), + SMS_TENCENT("sms", "tencent", "腾讯云短信", TencentConfig.class, TencentConfig.class), + SMS_CTYUN("sms", "ctyun","天翼云短信", CtyunConfig.class, CtyunConfig.class), + SMS_HUAWEI("sms", "huawei", "华为云短信", HuaweiConfig.class, HuaweiConfig.class), + SMS_YUNPIAN("sms", "yunpian", "云片短信", YunpianConfig.class, YunpianConfig.class), + SMS_EMAY("sms", "emay","亿美软通短信", EmayConfig.class, EmayConfig.class), + SMS_CLOOPEN("sms", "cloopen","容连云短信", CloopenConfig.class, CloopenConfig.class), + SMS_JDCLOUD("sms", "jdcloud", "京东云短信", JdCloudConfig.class, JdCloudConfig.class), + SMS_NETEASE("sms", "netease", "网易云短信", NeteaseConfig.class, NeteaseConfig.class), + + /****** 语音 ******/ + VOICE_ALIBABA("voice", "alibaba", "阿里云语音", VoiceConfigParams.class, VoiceMsgParams.class), + VOICE_TENCENT("voice", "tencent", "腾讯云语音", VoiceMsgParams.class, VoiceMsgParams.class), + + /****** 邮箱 ******/ + EMAIL_QQ("email", "qq", "QQ邮箱", EmailConfigParams.class, EmailMsgParams.class), + EMAIL_163("email", "163", "163邮箱", EmailConfigParams.class, EmailMsgParams.class), + + /****** 微信 *****/ + WECHAT_MINI_PROGRAM("wechat", "mini_program", "微信小程序(订阅消息)", WeChatConfigParams.class, WechatMsgParams.class), + WECHAT_WECOM_APPLY("wechat", "wecom_apply", "企业微信应用消息", WeChatConfigParams.class, WechatMsgParams.class), + WECHAT_WECOM_ROBOT("wechat", "wecom_robot", "企业微信群机器人", WeChatConfigParams.class, WechatMsgParams.class), + WECHAT_PUBLIC_ACCOUNT("wechat", "public_account", "微信公众号", WeChatConfigParams.class, WechatMsgParams.class), + + /****** 钉钉 *****/ + DING_TALK_WORK("dingtalk", "work", "钉钉工作消息", DingTalkConfigParams.class, DingTalkMsgParams.class), + DING_TALK_GROUP_ROBOT("dingtalk", "group_robot", "钉钉群机器人", DingTalkConfigParams.class, DingTalkMsgParams.class); + + /** + * 渠道编码 + */ + private String channelType; + + /** + * 渠道编码 + */ + private String provider; + + /** + * 描述 + */ + private String desc; + + /** + * 渠道配置类 + */ + private Class configContentClass; + + /** + * 模板配置类 + */ + private Class msgParamsClass; + + + public static NotifyChannelProviderEnum getNotifyChannelCodeEnum(String channelCode) { + for (NotifyChannelProviderEnum channelCodeEnum : NotifyChannelProviderEnum.values()) { + if (channelCode.equals(channelCodeEnum.channelType)) { + return channelCodeEnum; + } + } + return null; + } + + public static NotifyChannelProviderEnum getByChannelTypeAndProvider(String channelType, String provider) { + for (NotifyChannelProviderEnum channelCodeEnum : NotifyChannelProviderEnum.values()) { + if (channelType.equals(channelCodeEnum.channelType) && provider.equals(channelCodeEnum.getProvider())) { + return channelCodeEnum; + } + } + return null; + } + + /** + * @description: 获取通知渠道配置信息 + * @param: type + * @return: java.lang.Object + */ + public static List getConfigContent(NotifyChannelProviderEnum type) { + List configVOList = new ArrayList<>(); + // 务必保证属性(attribute)参数名和各渠道对应配置类configContentClass里的属性名一致 + switch (type) { + case SMS_ALIBABA: + configVOList.add(new NotifyConfigVO("accessKeyId", "accessKeyId", "string", "")); + configVOList.add(new NotifyConfigVO("accessKeySecret", "accessKeySecret", "string", "")); + break; +// return new SmsAliConfigParams(); + case SMS_TENCENT: + configVOList.add(new NotifyConfigVO("accessKeyId", "accessKeyId", "string", "")); + configVOList.add(new NotifyConfigVO("accessKeySecret", "accessKeySecret", "string", "")); + break; +// return new SmsAliConfigParams(); +// case SMS_CTYUN: +// return new SmsAliConfigParams(); +// case SMS_HUAWEI: +// return new SmsAliConfigParams(); +// case SMS_YUNPIAN: +// return new SmsAliConfigParams(); +// case SMS_EMAY: +// return new SmsAliConfigParams(); +// case SMS_CLOOPEN: +// return new SmsAliConfigParams(); +// case SMS_JDCLOUD: +// return new SmsAliConfigParams(); +// case SMS_NETEASE: +// return new SmsAliConfigParams(); + case VOICE_ALIBABA: + case VOICE_TENCENT: + configVOList.add(new NotifyConfigVO("accessKeyId", "accessKeyId", "string", "")); + configVOList.add(new NotifyConfigVO("accessKeySecret", "accessKeySecret", "string", "")); + break; + case EMAIL_QQ: + case EMAIL_163: + if (EMAIL_QQ.equals(type)) { + configVOList.add(new NotifyConfigVO("smtpServer", "服务器地址", "string","smtp.qq.com")); + } + if (EMAIL_163.equals(type)) { + configVOList.add(new NotifyConfigVO("smtpServer", "服务器地址", "string","smtp.163.com")); + } + configVOList.add(new NotifyConfigVO("port", "端口号", "string", "465")); + configVOList.add(new NotifyConfigVO("username", "发件人账号", "string", "")); + configVOList.add(new NotifyConfigVO("password", "发件秘钥", "string", "")); + configVOList.add(new NotifyConfigVO("sslEnable", "是否启动ssl", "boolean", "true")); + configVOList.add(new NotifyConfigVO("authEnable", "开启验证", "boolean", "true")); + configVOList.add(new NotifyConfigVO("retryInterval", "重试间隔(秒)", "int", "5")); + configVOList.add(new NotifyConfigVO("maxRetries", "重试次数", "int","1")); + break; + case WECHAT_MINI_PROGRAM: + case WECHAT_PUBLIC_ACCOUNT: + configVOList.add(new NotifyConfigVO("appId", "appId", "string","")); + configVOList.add(new NotifyConfigVO("appSecret", "appSecret", "string","")); + break; + case WECHAT_WECOM_APPLY: + configVOList.add(new NotifyConfigVO("corpId", "企业ID", "string","")); + configVOList.add(new NotifyConfigVO("corpSecret", "应用Secret", "string","")); + configVOList.add(new NotifyConfigVO("agentId", "应用agentId", "string","")); + break; + case WECHAT_WECOM_ROBOT: + configVOList.add(new NotifyConfigVO("webHook", "webHook", "string","")); + break; + case DING_TALK_WORK: + configVOList.add(new NotifyConfigVO("appKey", "appKey", "string","")); + configVOList.add(new NotifyConfigVO("appSecret", "appSecret", "string","")); + configVOList.add(new NotifyConfigVO("agentId", "agentId", "string","")); + break; + case DING_TALK_GROUP_ROBOT: + configVOList.add(new NotifyConfigVO("webHook", "webHook", "string","")); + break; + default: + return configVOList; + } + return configVOList; + } + + /** + * @description: 获取通知模板配置信息 + * @param: type + * @return: java.lang.Object + */ + public static List getMsgParams(NotifyChannelProviderEnum type, String msgType) { + List configVOList = new ArrayList<>(); + switch (type) { + // 短信:配置参数来源于sms4j,务必属性字段名和sms4j配置参数字段名一致 + case SMS_ALIBABA: + configVOList.add(new NotifyConfigVO("sendAccount", "发送电话号", "string","")); + configVOList.add(new NotifyConfigVO("templateId", "模板CODE", "string","")); + configVOList.add(new NotifyConfigVO("signature", "签名", "string","")); + configVOList.add(new NotifyConfigVO("content", "模板内容", "string","")); + break; + case SMS_TENCENT: + configVOList.add(new NotifyConfigVO("sendAccount", "发送电话号", "string","")); + configVOList.add(new NotifyConfigVO("templateId", "模板ID", "string","")); + configVOList.add(new NotifyConfigVO("signature", "签名", "string","")); + configVOList.add(new NotifyConfigVO("sdkAppId", "应用SDKAppID", "string","")); + configVOList.add(new NotifyConfigVO("content", "模板内容", "string","")); + break; + // 邮箱 + case EMAIL_QQ: + case EMAIL_163: + configVOList.add(new NotifyConfigVO("sendAccount", "发送邮箱号", "string","")); + configVOList.add(new NotifyConfigVO("title", "标题", "string","")); + configVOList.add(new NotifyConfigVO("attachment", "附件", "file","")); + configVOList.add(new NotifyConfigVO("content", "邮箱正文", "text","")); + break; + case WECHAT_MINI_PROGRAM: + configVOList.add(new NotifyConfigVO("sendAccount", "发送用户ID", "string","")); + configVOList.add(new NotifyConfigVO("templateId", "模板ID", "string","")); + configVOList.add(new NotifyConfigVO("redirectUrl", "跳转链接", "string","")); + configVOList.add(new NotifyConfigVO("content", "模板内容", "string","")); + break; + case WECHAT_PUBLIC_ACCOUNT: + configVOList.add(new NotifyConfigVO("templateId", "模板ID", "string","")); + configVOList.add(new NotifyConfigVO("redirectUrl", "跳转链接", "string","")); + configVOList.add(new NotifyConfigVO("appid", "跳转小程序appid", "string","")); + configVOList.add(new NotifyConfigVO("pagePath", "跳转小程序路径", "string","")); + configVOList.add(new NotifyConfigVO("content", "模板内容", "string","")); + case WECHAT_WECOM_APPLY: + case WECHAT_WECOM_ROBOT: + if (StringUtils.isEmpty(msgType)) { + return configVOList; + } + if (type.equals(WECHAT_WECOM_APPLY)) { + configVOList.add(new NotifyConfigVO("sendAccount", "发送成员账号", "string","")); + } + switch (msgType) { + case "text": + case "markdown": + configVOList.add(new NotifyConfigVO("content", "消息内容", "string","")); + break; + case "news": + configVOList.add(new NotifyConfigVO("title", "消息标题", "string","")); + configVOList.add(new NotifyConfigVO("content", "消息内容", "string","")); + configVOList.add(new NotifyConfigVO("url", "跳转链接", "string","")); + configVOList.add(new NotifyConfigVO("picUrl", "图片链接", "file","")); + break; + default: + break; + } + break; + // 语音 + case VOICE_ALIBABA: + configVOList.add(new NotifyConfigVO("sendAccount", "发送电话号", "string","")); + configVOList.add(new NotifyConfigVO("templateId", "模板ID", "string","")); + configVOList.add(new NotifyConfigVO("content", "模板内容", "string","")); + configVOList.add(new NotifyConfigVO("playTimes", "播放次数 (1~3)", "int","2")); + configVOList.add(new NotifyConfigVO("volume", "播放音量 (0-100)", "string","50")); + configVOList.add(new NotifyConfigVO("speed", "语速控制 (-500-500)", "string","0")); + break; + case VOICE_TENCENT: + configVOList.add(new NotifyConfigVO("sendAccount", "发送电话号", "string","")); + configVOList.add(new NotifyConfigVO("sdkAppId", "应用SDKAppID", "string","")); + configVOList.add(new NotifyConfigVO("templateId", "模板ID", "string","")); + configVOList.add(new NotifyConfigVO("content", "模板内容", "string","")); + break; + // 钉钉 + case DING_TALK_WORK: + case DING_TALK_GROUP_ROBOT: + if (StringUtils.isEmpty(msgType)) { + return configVOList; + } + switch (msgType) { + case "text": + if (NotifyChannelProviderEnum.DING_TALK_WORK.equals(type)) { + configVOList.add(new NotifyConfigVO("deptId", "部门id", "string","")); + configVOList.add(new NotifyConfigVO("sendAllEnable", "发送所有人", "boolean","false")); + configVOList.add(new NotifyConfigVO("sendAccount", "员工UserID", "string","")); + } + configVOList.add(new NotifyConfigVO("content", "消息内容", "string","")); + break; + case "link": + if (NotifyChannelProviderEnum.DING_TALK_WORK.equals(type)) { + configVOList.add(new NotifyConfigVO("deptId", "部门id", "string","")); + configVOList.add(new NotifyConfigVO("sendAllEnable", "发送所有人", "boolean","false")); + configVOList.add(new NotifyConfigVO("sendAccount", "员工UserID", "string","")); + } + configVOList.add(new NotifyConfigVO("title", "消息标题", "string","")); + configVOList.add(new NotifyConfigVO("content", "消息内容", "string","")); + configVOList.add(new NotifyConfigVO("messageUrl", "消息链接", "string","")); + configVOList.add(new NotifyConfigVO("picUrl", "图片链接", "file","")); + break; + case "markdown": + if (NotifyChannelProviderEnum.DING_TALK_WORK.equals(type)) { + configVOList.add(new NotifyConfigVO("deptId", "部门id", "string","")); + configVOList.add(new NotifyConfigVO("sendAllEnable", "发送所有人", "boolean","false")); + configVOList.add(new NotifyConfigVO("sendAccount", "员工UserID", "string","")); + } + configVOList.add(new NotifyConfigVO("title", "消息标题", "string","")); + configVOList.add(new NotifyConfigVO("content", "消息内容", "string","")); + break; + default: + break; + } + break; + default: + return configVOList; + } + return configVOList; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/NotifyServiceCodeEnum.java b/bnhz-common/src/main/java/com/bnhz/common/enums/NotifyServiceCodeEnum.java new file mode 100644 index 0000000..35d8de0 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/NotifyServiceCodeEnum.java @@ -0,0 +1,44 @@ +package com.bnhz.common.enums; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @description: 通知业务编码枚举 + * @author fastb + * @date 2023-12-16 17:00 + * @version 1.0 + */ +@Getter +@AllArgsConstructor +public enum NotifyServiceCodeEnum { + + /** + * 确保唯一,不能重复 + */ + ALERT("alert", "设备告警"), + CAPTCHA("captcha","验证码"), + MARKETING("marketing", "营销通知"); + + + /** + * 业务编码 + */ + private String serviceCode; + + /** + * 描述 + */ + private String desc; + + public static NotifyServiceCodeEnum getNotifyServiceCodeEnum(String serviceCode) { + for (NotifyServiceCodeEnum notifyServiceCodeEnum : NotifyServiceCodeEnum.values()) { + if (serviceCode.equals(notifyServiceCodeEnum.serviceCode)) { + return notifyServiceCodeEnum; + } + } + return null; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/OTAUpgrade.java b/bnhz-common/src/main/java/com/bnhz/common/enums/OTAUpgrade.java new file mode 100644 index 0000000..23bf84d --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/OTAUpgrade.java @@ -0,0 +1,37 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * OTA升级状态 + * @author gsb + * @date 2022/10/24 17:29 + */ +@AllArgsConstructor +@Getter +public enum OTAUpgrade { + + + AWAIT(0, "等待升级","未推送固件到设备"), + SEND(1, "已发送","已发送设备"), + REPLY(2, "升级中","设备OTA升级中"), + SUCCESS(3, "成功","升级成功"), + FAILED(4, "失败","升级失败"), + STOP(5, "停止","设备离线停止推送"), + UNKNOWN(404, "未知","未知错误码"); + Integer status; + String subMsg; + String des; + + public static OTAUpgrade parse(Integer code){ + for (OTAUpgrade item: OTAUpgrade.values()){ + if(item.status.equals(code)){ + return item; + } + } + + return UNKNOWN; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/OperatorType.java b/bnhz-common/src/main/java/com/bnhz/common/enums/OperatorType.java new file mode 100644 index 0000000..d5b490c --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/OperatorType.java @@ -0,0 +1,24 @@ +package com.bnhz.common.enums; + +/** + * 操作人类别 + * + * @author ruoyi + */ +public enum OperatorType +{ + /** + * 其它 + */ + OTHER, + + /** + * 后台用户 + */ + MANAGE, + + /** + * 手机端用户 + */ + MOBILE +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/PushType.java b/bnhz-common/src/main/java/com/bnhz/common/enums/PushType.java new file mode 100644 index 0000000..c4b657b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/PushType.java @@ -0,0 +1,23 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 推送类型 + * @author bill + */ +@Getter +@AllArgsConstructor +public enum PushType { + + WECHAT_SERVER_PUSH("wechat_server_push","微信小程序服务号推送"); + + + /** + * 业务编号 + */ + private String serviceCode; + + private String desc; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/ResultCode.java b/bnhz-common/src/main/java/com/bnhz/common/enums/ResultCode.java new file mode 100644 index 0000000..6bec5a2 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/ResultCode.java @@ -0,0 +1,37 @@ +package com.bnhz.common.enums; + +import com.bnhz.common.constant.HttpStatus; +import lombok.AllArgsConstructor; + +/** + * API返回对象 + */ +@AllArgsConstructor +public enum ResultCode implements IErrorCode { + + SUCCESS(HttpStatus.SUCCESS,"请求成功"), + FAILED(HttpStatus.ERROR,"系统内部错误"), + ACCEPTED(HttpStatus.ACCEPTED,"请求已接收"), + REDIRECT(HttpStatus.SEE_OTHER,"重定向"), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"暂未登录或token过期"), + FORBIDDEN(HttpStatus.FORBIDDEN,"没有相关权限或授权过期"), + NOT_FOUND(HttpStatus.NOT_FOUND,"资源未找到"), + PARSE_MSG_EXCEPTION(4018, "解析协议异常"), + TIMEOUT(502, "响应超时!"), + FIRMWARE_VERSION_UNIQUE_ERROR(4022, "产品下已存在该版本固件"), + FIRMWARE_SEQ_UNIQUE_ERROR(4023, "产品下已存在该升级序列号"), + FIRMWARE_TASK_UNIQUE_ERROR(4024, "任务名已存在"), + REPLY_TIMEOUT(4001, "超时未回执"), + INVALID_USER_APP(4002, "用户信息不存在"), + INVALID_MQTT_USER(1003, "内部mqtt服务用户异常"), + DECODE_PROTOCOL_EXCEPTION(1000, "解析协议异常"), + MQTT_TOPIC_INVALID(1001, "MQTT订阅topic格式非法"); + + private int code; + private String message; + + public int getCode(){return code;} + + public String getMessage(){return message;} + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/ServerType.java b/bnhz-common/src/main/java/com/bnhz/common/enums/ServerType.java new file mode 100644 index 0000000..158be8c --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/ServerType.java @@ -0,0 +1,36 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author gsb + * @date 2022/9/15 9:10 + */ +@Getter +@AllArgsConstructor +public enum ServerType { + + MQTT(1, "MQTT","MQTT-BROKER"), + COAP(2, "COAP","COAP-SERVER"), + TCP(3, "TCP","TCP-SERVER"), + UDP(4, "UDP","UDP-SERVER"), + WEBSOCKET(5,"WEBSOCKET","WEBSOCKET-SERVER"), + GB28181(6,"GB28181","SIP-SERVER"), + OTHER(999,"WEBSOCKET","MQTT-BROKER"); + + private int type; + private String code; + private String des; + + + + public static ServerType explain(String code) { + for (ServerType value : ServerType.values()) { + if (value.code.equals(code)) { + return value; + } + } + return ServerType.MQTT; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/SocialPlatformType.java b/bnhz-common/src/main/java/com/bnhz/common/enums/SocialPlatformType.java new file mode 100644 index 0000000..7035704 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/SocialPlatformType.java @@ -0,0 +1,69 @@ +package com.bnhz.common.enums; + +import java.util.Arrays; +import java.util.List; + +/** + * 第三方登录平台枚举 + * + * @author json + */ +public enum SocialPlatformType { + WECHAT_OPEN_WEB("wechat_open_web", "微信开放平台网站应用"), + WECHAT_OPEN_WEB_BIND("wechat_open_web_bind", "微信开放平台网站应用个人中心绑定"), + WECHAT_OPEN_MOBILE("wechat_open_mobile", "微信开放平台移动应用"), + WECHAT_OPEN_MINI_PROGRAM("wechat_open_mini_program", "微信开放平台小程序"), + WECHAT_OPEN_PUBLIC_ACCOUNT("wechat_open_public_account", "微信开放平台公众号"), + QQ_OPEN_WEB("qq_open_web", "QQ互联网站应用"), + QQ_OPEN_APP("qq_open_app", "QQ互联移动应用"), + QQ_OPEN_MINI_PROGRAM("qq_open_mini_program", "QQ互联小程序"); +// ALIPAY_OPEN_WEB("alipay_open_web", ""), +// ALIPAY_OPEN_APP("alipay_open_app", ""), +// ALIPAY_OPEN_MINI_PROGRAM("alipay_open_mini_program", ""); + + public String sourceClient; + + public String desc; + + SocialPlatformType(String sourceClient, String desc) { + this.sourceClient = sourceClient; + this.desc = desc; + } + + // 查询微信绑定来源集合 + public static final List listWechatPlatform = Arrays.asList(WECHAT_OPEN_WEB.sourceClient, WECHAT_OPEN_MOBILE.sourceClient, WECHAT_OPEN_MINI_PROGRAM.sourceClient); + + public static String getDesc(String sourceClient) { + for (SocialPlatformType socialPlatformType : SocialPlatformType.values()) { + if (socialPlatformType.getSourceClient().equals(sourceClient)) { + return socialPlatformType.getDesc(); + } + } + return null; + } + + public static SocialPlatformType getSocialPlatformType(String sourceClient) { + for (SocialPlatformType socialPlatformType : SocialPlatformType.values()) { + if (socialPlatformType.getSourceClient().equals(sourceClient)) { + return socialPlatformType; + } + } + return null; + } + + public String getSourceClient() { + return sourceClient; + } + + public void setSourceClient(String sourceClient) { + this.sourceClient = sourceClient; + } + + public String getDesc() { + return desc; + } + + public void setDesc(String desc) { + this.desc = desc; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/ThingsModelType.java b/bnhz-common/src/main/java/com/bnhz/common/enums/ThingsModelType.java new file mode 100644 index 0000000..b25936e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/ThingsModelType.java @@ -0,0 +1,50 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 物模型类型 + * + * @author bill + */ +@Getter +@AllArgsConstructor +public enum ThingsModelType { + + PROP(1, "PROPERTY", "属性","properties"), + SERVICE(2, "FUNCTION", "服务","functions"), + EVENT(3, "EVENT", "事件","events"),; + + int code; + String type; + String name; + String list; + + public static ThingsModelType getType(int code) { + for (ThingsModelType value : ThingsModelType.values()) { + if (value.code == code) { + return value; + } + } + return ThingsModelType.PROP; + } + + public static ThingsModelType getType(String type) { + for (ThingsModelType value : ThingsModelType.values()) { + if (value.type.equals(type)) { + return value; + } + } + return ThingsModelType.PROP; + } + + public static String getName(int code) { + for (ThingsModelType value : ThingsModelType.values()) { + if (value.code == code) { + return value.list; + } + } + return ThingsModelType.PROP.list; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/TopicType.java b/bnhz-common/src/main/java/com/bnhz/common/enums/TopicType.java new file mode 100644 index 0000000..7ea4758 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/TopicType.java @@ -0,0 +1,73 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * topic类型 + * @author gsb + */ +@Getter +@AllArgsConstructor +public enum TopicType { + + /** + * @param type 0:标记是订阅主题 1:标记是发布属性 + * @param order 排序 + * @param topicSuffix topic后缀 + * @param msg 描述信息 + */ + + /*** 通用设备上报主题(平台订阅) ***/ + PROPERTY_POST(0,1,"/property/post", "订阅属性"), + EVENT_POST(0,2,"/event/post", "订阅事件"), + FUNCTION_POST(0,3,"/function/post", "订阅功能"), + INFO_POST(0,4,"/info/post","订阅设备信息"), + NTP_POST(0,5,"/ntp/post","订阅时钟同步"), + SERVICE_INVOKE_REPLY(0,8,"/service/reply", "订阅功能调用返回结果"), + FIRMWARE_UPGRADE_REPLY(0,9,"/upgrade/reply", "订阅设备OTA升级结果"), + + + /*** 通用设备订阅主题(平台下发)***/ + FUNCTION_GET(1,17,"/function/get", "发布功能"), + PROPERTY_GET(1,12,"/property/get" ,"发布设备属性读取"), + PROPERTY_SET(1,15,"/property/set" ,"设置设备属性读取"), + FIRMWARE_SET(1,14, "/upgrade/set","发布OTA升级"), + STATUS_POST(1,11,"/status/post","发布状态"), + NTP_GET(1,15,"/ntp/get","发布时钟同步"), + INFO_GET(1,18,"/info/get","发布设备信息"), + + + /*** 视频监控设备转协议发布 ***/ + DEV_INFO_POST(3,19,"/info/post","设备端发布设备信息"), + DEV_EVENT_POST(3,20,"/event/post","设备端发布事件"), + DEV_FUNCTION_POST(3,21,"/function/post", "设备端发布功能"), + DEV_PROPERTY_POST(3,22,"/property/post", "设备端发布属性"), + + + /*** webSocket转发前端使用 ***/ + WS_SERVICE_INVOKE(2,16,"/ws/service", "WS服务调用"), + WS_LOG_INVOKE(2,17,"/ws/log","ws下发指令日志"), + + + /*** 模拟设备使用 ***/ + PROPERTY_GET_SIMULATE(4,23,"/property/get/simulate" ,"发布属性读取"), + PROPERTY_SET_SIMULATE(4,13, "/property/set/simulate","发布属性写入"), + WS_SERVICE_INVOKE_SIMULATE(2,24,"/ws/post/simulate", "模拟设备WS推送"), + PROPERTY_POST_SIMULATE(2,25,"/property/simulate/post", "订阅属性"); + + Integer type; + Integer order; + String topicSuffix; + String msg; + + public static TopicType getType(String topicSuffix) { + for (TopicType value : TopicType.values()) { + if (value.topicSuffix.equals(topicSuffix)) { + return value; + } + } + return TopicType.PROPERTY_POST; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/UserStatus.java b/bnhz-common/src/main/java/com/bnhz/common/enums/UserStatus.java new file mode 100644 index 0000000..345561f --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/UserStatus.java @@ -0,0 +1,30 @@ +package com.bnhz.common.enums; + +/** + * 用户状态 + * + * @author ruoyi + */ +public enum UserStatus +{ + OK("0", "正常"), DISABLE("1", "停用"), DELETED("2", "删除"); + + private final String code; + private final String info; + + UserStatus(String code, String info) + { + this.code = code; + this.info = info; + } + + public String getCode() + { + return code; + } + + public String getInfo() + { + return info; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/enums/VerifyTypeEnum.java b/bnhz-common/src/main/java/com/bnhz/common/enums/VerifyTypeEnum.java new file mode 100644 index 0000000..24af94e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/enums/VerifyTypeEnum.java @@ -0,0 +1,23 @@ +package com.bnhz.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 验证类型枚举 + * @author fastb + * @date 2023-08-30 15:04 + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public enum VerifyTypeEnum { + + PASSWORD(1, "账号密码验证"), + SMS(2, "短信验证"); + + private Integer verifyType; + + private String desc; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/DemoModeException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/DemoModeException.java new file mode 100644 index 0000000..e5a662d --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/DemoModeException.java @@ -0,0 +1,15 @@ +package com.bnhz.common.exception; + +/** + * 演示模式异常 + * + * @author ruoyi + */ +public class DemoModeException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + public DemoModeException() + { + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/ErrorCode.java b/bnhz-common/src/main/java/com/bnhz/common/exception/ErrorCode.java new file mode 100644 index 0000000..894b127 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/ErrorCode.java @@ -0,0 +1,28 @@ +package com.bnhz.common.exception; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 错误码对象 + * + */ +@Data +@Accessors(chain = true) +public class ErrorCode { + + /** + * 错误码 + */ + private final Integer code; + /** + * 错误提示 + */ + private final String msg; + + public ErrorCode(Integer code, String message) { + this.code = code; + this.msg = message; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/GlobalException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/GlobalException.java new file mode 100644 index 0000000..ad0185b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/GlobalException.java @@ -0,0 +1,58 @@ +package com.bnhz.common.exception; + +/** + * 全局异常 + * + * @author ruoyi + */ +public class GlobalException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public GlobalException() + { + } + + public GlobalException(String message) + { + this.message = message; + } + + public String getDetailMessage() + { + return detailMessage; + } + + public GlobalException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } + + @Override + public String getMessage() + { + return message; + } + + public GlobalException setMessage(String message) + { + this.message = message; + return this; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/ServerException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/ServerException.java new file mode 100644 index 0000000..51a66fe --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/ServerException.java @@ -0,0 +1,60 @@ +package com.bnhz.common.exception; + +import com.bnhz.common.enums.GlobalErrorCodeConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 服务器异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class ServerException extends RuntimeException { + + /** + * 全局错误码 + * + * @see GlobalErrorCodeConstants + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServerException() { + } + + public ServerException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMsg(); + } + + public ServerException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public ServerException setCode(Integer code) { + this.code = code; + return this; + } + + @Override + public String getMessage() { + return message; + } + + public ServerException setMessage(String message) { + this.message = message; + return this; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/ServiceException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/ServiceException.java new file mode 100644 index 0000000..d1e1431 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/ServiceException.java @@ -0,0 +1,80 @@ +package com.bnhz.common.exception; + +/** + * 业务异常 + * + * @author ruoyi + */ +public final class ServiceException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private Integer code; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + * + * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() + { + } + + public ServiceException(String message) + { + this.message = message; + } + + public ServiceException(String message, Integer code) + { + this.message = message; + this.code = code; + } + + public ServiceException(Integer code, String message) + { + this.code = code; + this.message = message; + } + + public String getDetailMessage() + { + return detailMessage; + } + + @Override + public String getMessage() + { + return message; + } + + public Integer getCode() + { + return code; + } + + public ServiceException setMessage(String message) + { + this.message = message; + return this; + } + + public ServiceException setDetailMessage(String detailMessage) + { + this.detailMessage = detailMessage; + return this; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/ServiceExceptionUtil.java b/bnhz-common/src/main/java/com/bnhz/common/exception/ServiceExceptionUtil.java new file mode 100644 index 0000000..d76ec8a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/ServiceExceptionUtil.java @@ -0,0 +1,125 @@ +package com.bnhz.common.exception; + +import com.bnhz.common.enums.GlobalErrorCodeConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * {@link ServiceException} 工具类 + * + * 目的在于,格式化异常信息提示。 + * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 + * + * 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式: + * + * 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration + * 2. 异常提示信息,写在 .properties 等等配置文件 + * 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新 + * 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新 + */ +@Slf4j +public class ServiceExceptionUtil { + + /** + * 错误码提示模板 + */ + private static final ConcurrentMap MESSAGES = new ConcurrentHashMap<>(); + + public static void putAll(Map messages) { + ServiceExceptionUtil.MESSAGES.putAll(messages); + } + + public static void put(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.put(code, message); + } + + public static void delete(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.remove(code, message); + } + + // ========== 和 ServiceException 的集成 ========== + + public static ServiceException exception(ErrorCode errorCode) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); + return exception0(errorCode.getCode(), messagePattern); + } + + public static ServiceException exception(ErrorCode errorCode, Object... params) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); + return exception0(errorCode.getCode(), messagePattern, params); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @return 异常 + */ + public static ServiceException exception(Integer code) { + return exception0(code, MESSAGES.get(code)); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @param params 消息提示的占位符对应的参数 + * @return 异常 + */ + public static ServiceException exception(Integer code, Object... params) { + return exception0(code, MESSAGES.get(code), params); + } + + public static ServiceException exception0(Integer code, String messagePattern, Object... params) { + String message = doFormat(code, messagePattern, params); + return new ServiceException(code, message); + } + + public static ServiceException invalidParamException(String messagePattern, Object... params) { + return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params); + } + + // ========== 格式化方法 ========== + + /** + * 将错误编号对应的消息使用 params 进行格式化。 + * + * @param code 错误编号 + * @param messagePattern 消息模版 + * @param params 参数 + * @return 格式化后的提示 + */ + @VisibleForTesting + public static String doFormat(int code, String messagePattern, Object... params) { + StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); + int i = 0; + int j; + int l; + for (l = 0; l < params.length; l++) { + j = messagePattern.indexOf("{}", i); + if (j == -1) { + log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + if (i == 0) { + return messagePattern; + } else { + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + } else { + sbuf.append(messagePattern, i, j); + sbuf.append(params[l]); + i = j + 2; + } + } + if (messagePattern.indexOf("{}", i) != -1) { + log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + } + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/UtilException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/UtilException.java new file mode 100644 index 0000000..9d6c84e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/UtilException.java @@ -0,0 +1,26 @@ +package com.bnhz.common.exception; + +/** + * 工具类异常 + * + * @author ruoyi + */ +public class UtilException extends RuntimeException +{ + private static final long serialVersionUID = 8247610319171014183L; + + public UtilException(Throwable e) + { + super(e.getMessage(), e); + } + + public UtilException(String message) + { + super(message); + } + + public UtilException(String message, Throwable throwable) + { + super(message, throwable); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/base/BaseException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/base/BaseException.java new file mode 100644 index 0000000..d2a4ea3 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/base/BaseException.java @@ -0,0 +1,97 @@ +package com.bnhz.common.exception.base; + +import com.bnhz.common.utils.MessageUtils; +import com.bnhz.common.utils.StringUtils; + +/** + * 基础异常 + * + * @author ruoyi + */ +public class BaseException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + /** + * 所属模块 + */ + private String module; + + /** + * 错误码 + */ + private String code; + + /** + * 错误码对应的参数 + */ + private Object[] args; + + /** + * 错误消息 + */ + private String defaultMessage; + + public BaseException(String module, String code, Object[] args, String defaultMessage) + { + this.module = module; + this.code = code; + this.args = args; + this.defaultMessage = defaultMessage; + } + + public BaseException(String module, String code, Object[] args) + { + this(module, code, args, null); + } + + public BaseException(String module, String defaultMessage) + { + this(module, null, null, defaultMessage); + } + + public BaseException(String code, Object[] args) + { + this(null, code, args, null); + } + + public BaseException(String defaultMessage) + { + this(null, null, null, defaultMessage); + } + + @Override + public String getMessage() + { + String message = null; + if (!StringUtils.isEmpty(code)) + { + message = MessageUtils.message(code, args); + } + if (message == null) + { + message = defaultMessage; + } + return message; + } + + public String getModule() + { + return module; + } + + public String getCode() + { + return code; + } + + public Object[] getArgs() + { + return args; + } + + public String getDefaultMessage() + { + return defaultMessage; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/file/FileException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/file/FileException.java new file mode 100644 index 0000000..6246c39 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/file/FileException.java @@ -0,0 +1,19 @@ +package com.bnhz.common.exception.file; + +import com.bnhz.common.exception.base.BaseException; + +/** + * 文件信息异常类 + * + * @author ruoyi + */ +public class FileException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public FileException(String code, Object[] args) + { + super("file", code, args, null); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/file/FileNameLengthLimitExceededException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/file/FileNameLengthLimitExceededException.java new file mode 100644 index 0000000..fb4323b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/file/FileNameLengthLimitExceededException.java @@ -0,0 +1,16 @@ +package com.bnhz.common.exception.file; + +/** + * 文件名称超长限制异常类 + * + * @author ruoyi + */ +public class FileNameLengthLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileNameLengthLimitExceededException(int defaultFileNameLength) + { + super("upload.filename.exceed.length", new Object[] { defaultFileNameLength }); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/file/FileSizeLimitExceededException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/file/FileSizeLimitExceededException.java new file mode 100644 index 0000000..e243c47 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/file/FileSizeLimitExceededException.java @@ -0,0 +1,16 @@ +package com.bnhz.common.exception.file; + +/** + * 文件名大小限制异常类 + * + * @author ruoyi + */ +public class FileSizeLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileSizeLimitExceededException(long defaultMaxSize) + { + super("upload.exceed.maxSize", new Object[] { defaultMaxSize }); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/file/InvalidExtensionException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/file/InvalidExtensionException.java new file mode 100644 index 0000000..c10f989 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/file/InvalidExtensionException.java @@ -0,0 +1,81 @@ +package com.bnhz.common.exception.file; + +import java.util.Arrays; +import org.apache.commons.fileupload.FileUploadException; + +/** + * 文件上传 误异常类 + * + * @author ruoyi + */ +public class InvalidExtensionException extends FileUploadException +{ + private static final long serialVersionUID = 1L; + + private String[] allowedExtension; + private String extension; + private String filename; + + public InvalidExtensionException(String[] allowedExtension, String extension, String filename) + { + super("文件[" + filename + "]后缀[" + extension + "]不正确,请上传" + Arrays.toString(allowedExtension) + "格式"); + this.allowedExtension = allowedExtension; + this.extension = extension; + this.filename = filename; + } + + public String[] getAllowedExtension() + { + return allowedExtension; + } + + public String getExtension() + { + return extension; + } + + public String getFilename() + { + return filename; + } + + public static class InvalidImageExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidImageExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidFlashExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidFlashExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidMediaExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidMediaExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } + + public static class InvalidVideoExtensionException extends InvalidExtensionException + { + private static final long serialVersionUID = 1L; + + public InvalidVideoExtensionException(String[] allowedExtension, String extension, String filename) + { + super(allowedExtension, extension, filename); + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/iot/MqttAuthorizationException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/iot/MqttAuthorizationException.java new file mode 100644 index 0000000..be2eec7 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/iot/MqttAuthorizationException.java @@ -0,0 +1,17 @@ +package com.bnhz.common.exception.iot; + +import com.bnhz.common.exception.GlobalException; +import lombok.NoArgsConstructor; + +/** + * mqtt客户端权限校验异常 + * @author gsb + * @date 2022/10/8 14:11 + */ +@NoArgsConstructor +public class MqttAuthorizationException extends GlobalException { + + public MqttAuthorizationException(String messageId){ + super(messageId); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/iot/MqttClientUserNameOrPassException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/iot/MqttClientUserNameOrPassException.java new file mode 100644 index 0000000..e1bf757 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/iot/MqttClientUserNameOrPassException.java @@ -0,0 +1,17 @@ +package com.bnhz.common.exception.iot; + +import com.bnhz.common.exception.GlobalException; +import lombok.NoArgsConstructor; + +/** + * mqtt客户端校验 用户名或密码错误 + * @author gsb + * @date 2022/10/8 14:15 + */ +@NoArgsConstructor +public class MqttClientUserNameOrPassException extends GlobalException { + + public MqttClientUserNameOrPassException(String message){ + super(message); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/job/TaskException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/job/TaskException.java new file mode 100644 index 0000000..481a61a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/job/TaskException.java @@ -0,0 +1,34 @@ +package com.bnhz.common.exception.job; + +/** + * 计划策略异常 + * + * @author ruoyi + */ +public class TaskException extends Exception +{ + private static final long serialVersionUID = 1L; + + private Code code; + + public TaskException(String msg, Code code) + { + this(msg, code, null); + } + + public TaskException(String msg, Code code, Exception nestedEx) + { + super(msg, nestedEx); + this.code = code; + } + + public Code getCode() + { + return code; + } + + public enum Code + { + TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/user/CaptchaException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/user/CaptchaException.java new file mode 100644 index 0000000..814c1af --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/user/CaptchaException.java @@ -0,0 +1,16 @@ +package com.bnhz.common.exception.user; + +/** + * 验证码错误异常类 + * + * @author ruoyi + */ +public class CaptchaException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaException() + { + super("user.jcaptcha.error", null); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/user/CaptchaExpireException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/user/CaptchaExpireException.java new file mode 100644 index 0000000..a359d62 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/user/CaptchaExpireException.java @@ -0,0 +1,16 @@ +package com.bnhz.common.exception.user; + +/** + * 验证码失效异常类 + * + * @author ruoyi + */ +public class CaptchaExpireException extends UserException +{ + private static final long serialVersionUID = 1L; + + public CaptchaExpireException() + { + super("user.jcaptcha.expire", null); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/user/UserException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/user/UserException.java new file mode 100644 index 0000000..5fb8125 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/user/UserException.java @@ -0,0 +1,18 @@ +package com.bnhz.common.exception.user; + +import com.bnhz.common.exception.base.BaseException; + +/** + * 用户信息异常类 + * + * @author ruoyi + */ +public class UserException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public UserException(String code, Object[] args) + { + super("user", code, args, null); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/user/UserPasswordNotMatchException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/user/UserPasswordNotMatchException.java new file mode 100644 index 0000000..c76b653 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/user/UserPasswordNotMatchException.java @@ -0,0 +1,16 @@ +package com.bnhz.common.exception.user; + +/** + * 用户密码不正确或不符合规范异常类 + * + * @author ruoyi + */ +public class UserPasswordNotMatchException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordNotMatchException() + { + super("user.password.not.match", null); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/exception/user/UserPasswordRetryLimitExceedException.java b/bnhz-common/src/main/java/com/bnhz/common/exception/user/UserPasswordRetryLimitExceedException.java new file mode 100644 index 0000000..163e86b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/exception/user/UserPasswordRetryLimitExceedException.java @@ -0,0 +1,16 @@ +package com.bnhz.common.exception.user; + +/** + * 用户错误最大次数异常类 + * + * @author ruoyi + */ +public class UserPasswordRetryLimitExceedException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime) + { + super("user.password.retry.limit.exceed", new Object[] { retryLimitCount, lockTime }); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/filter/PropertyPreExcludeFilter.java b/bnhz-common/src/main/java/com/bnhz/common/filter/PropertyPreExcludeFilter.java new file mode 100644 index 0000000..b327f85 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/filter/PropertyPreExcludeFilter.java @@ -0,0 +1,24 @@ +package com.bnhz.common.filter; + +import com.alibaba.fastjson2.filter.SimplePropertyPreFilter; + +/** + * 排除JSON敏感属性 + * + * @author ruoyi + */ +public class PropertyPreExcludeFilter extends SimplePropertyPreFilter +{ + public PropertyPreExcludeFilter() + { + } + + public PropertyPreExcludeFilter addExcludes(String... filters) + { + for (int i = 0; i < filters.length; i++) + { + this.getExcludes().add(filters[i]); + } + return this; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/filter/RepeatableFilter.java b/bnhz-common/src/main/java/com/bnhz/common/filter/RepeatableFilter.java new file mode 100644 index 0000000..6cac37c --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/filter/RepeatableFilter.java @@ -0,0 +1,52 @@ +package com.bnhz.common.filter; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import org.springframework.http.MediaType; +import com.bnhz.common.utils.StringUtils; + +/** + * Repeatable 过滤器 + * + * @author ruoyi + */ +public class RepeatableFilter implements Filter +{ + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + ServletRequest requestWrapper = null; + if (request instanceof HttpServletRequest + && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) + { + requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response); + } + if (null == requestWrapper) + { + chain.doFilter(request, response); + } + else + { + chain.doFilter(requestWrapper, response); + } + } + + @Override + public void destroy() + { + + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/filter/RepeatedlyRequestWrapper.java b/bnhz-common/src/main/java/com/bnhz/common/filter/RepeatedlyRequestWrapper.java new file mode 100644 index 0000000..55a8046 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/filter/RepeatedlyRequestWrapper.java @@ -0,0 +1,76 @@ +package com.bnhz.common.filter; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import com.bnhz.common.utils.http.HttpHelper; +import com.bnhz.common.constant.Constants; + +/** + * 构建可重复读取inputStream的request + * + * @author ruoyi + */ +public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper +{ + private final byte[] body; + + public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException + { + super(request); + request.setCharacterEncoding(Constants.UTF8); + response.setCharacterEncoding(Constants.UTF8); + + body = HttpHelper.getBodyString(request).getBytes(Constants.UTF8); + } + + @Override + public BufferedReader getReader() throws IOException + { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + final ByteArrayInputStream bais = new ByteArrayInputStream(body); + return new ServletInputStream() + { + @Override + public int read() throws IOException + { + return bais.read(); + } + + @Override + public int available() throws IOException + { + return body.length; + } + + @Override + public boolean isFinished() + { + return false; + } + + @Override + public boolean isReady() + { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) + { + + } + }; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/filter/XssFilter.java b/bnhz-common/src/main/java/com/bnhz/common/filter/XssFilter.java new file mode 100644 index 0000000..619ad00 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/filter/XssFilter.java @@ -0,0 +1,75 @@ +package com.bnhz.common.filter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.enums.HttpMethod; + +/** + * 防止XSS攻击的过滤器 + * + * @author ruoyi + */ +public class XssFilter implements Filter +{ + /** + * 排除链接 + */ + public List excludes = new ArrayList<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + String tempExcludes = filterConfig.getInitParameter("excludes"); + if (StringUtils.isNotEmpty(tempExcludes)) + { + String[] url = tempExcludes.split(","); + for (int i = 0; url != null && i < url.length; i++) + { + excludes.add(url[i]); + } + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + if (handleExcludeURL(req, resp)) + { + chain.doFilter(request, response); + return; + } + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(xssRequest, response); + } + + private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) + { + String url = request.getServletPath(); + String method = request.getMethod(); + // GET DELETE 不过滤 + if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method)) + { + return true; + } + return StringUtils.matches(url, excludes); + } + + @Override + public void destroy() + { + + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/filter/XssHttpServletRequestWrapper.java b/bnhz-common/src/main/java/com/bnhz/common/filter/XssHttpServletRequestWrapper.java new file mode 100644 index 0000000..03c0e09 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/filter/XssHttpServletRequestWrapper.java @@ -0,0 +1,111 @@ +package com.bnhz.common.filter; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import org.apache.commons.io.IOUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.html.EscapeUtil; + +/** + * XSS过滤处理 + * + * @author ruoyi + */ +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper +{ + /** + * @param request + */ + public XssHttpServletRequestWrapper(HttpServletRequest request) + { + super(request); + } + + @Override + public String[] getParameterValues(String name) + { + String[] values = super.getParameterValues(name); + if (values != null) + { + int length = values.length; + String[] escapesValues = new String[length]; + for (int i = 0; i < length; i++) + { + // 防xss攻击和过滤前后空格 + escapesValues[i] = EscapeUtil.clean(values[i]).trim(); + } + return escapesValues; + } + return super.getParameterValues(name); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + // 非json类型,直接返回 + if (!isJsonRequest()) + { + return super.getInputStream(); + } + + // 为空,直接返回 + String json = IOUtils.toString(super.getInputStream(), "utf-8"); + if (StringUtils.isEmpty(json)) + { + return super.getInputStream(); + } + + // xss过滤 + json = EscapeUtil.clean(json).trim(); + byte[] jsonBytes = json.getBytes("utf-8"); + final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes); + return new ServletInputStream() + { + @Override + public boolean isFinished() + { + return true; + } + + @Override + public boolean isReady() + { + return true; + } + + @Override + public int available() throws IOException + { + return jsonBytes.length; + } + + @Override + public void setReadListener(ReadListener readListener) + { + } + + @Override + public int read() throws IOException + { + return bis.read(); + } + }; + } + + /** + * 是否是Json请求 + * + * @param request + */ + public boolean isJsonRequest() + { + String header = super.getHeader(HttpHeaders.CONTENT_TYPE); + return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/Arith.java b/bnhz-common/src/main/java/com/bnhz/common/utils/Arith.java new file mode 100644 index 0000000..37d3d46 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/Arith.java @@ -0,0 +1,114 @@ +package com.bnhz.common.utils; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 精确的浮点数运算 + * + * @author ruoyi + */ +public class Arith +{ + + /** 默认除法运算精度 */ + private static final int DEF_DIV_SCALE = 10; + + /** 这个类不能实例化 */ + private Arith() + { + } + + /** + * 提供精确的加法运算。 + * @param v1 被加数 + * @param v2 加数 + * @return 两个参数的和 + */ + public static double add(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.add(b2).doubleValue(); + } + + /** + * 提供精确的减法运算。 + * @param v1 被减数 + * @param v2 减数 + * @return 两个参数的差 + */ + public static double sub(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.subtract(b2).doubleValue(); + } + + /** + * 提供精确的乘法运算。 + * @param v1 被乘数 + * @param v2 乘数 + * @return 两个参数的积 + */ + public static double mul(double v1, double v2) + { + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + return b1.multiply(b2).doubleValue(); + } + + /** + * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 + * 小数点以后10位,以后的数字四舍五入。 + * @param v1 被除数 + * @param v2 除数 + * @return 两个参数的商 + */ + public static double div(double v1, double v2) + { + return div(v1, v2, DEF_DIV_SCALE); + } + + /** + * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 + * 定精度,以后的数字四舍五入。 + * @param v1 被除数 + * @param v2 除数 + * @param scale 表示表示需要精确到小数点以后几位。 + * @return 两个参数的商 + */ + public static double div(double v1, double v2, int scale) + { + if (scale < 0) + { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b1 = new BigDecimal(Double.toString(v1)); + BigDecimal b2 = new BigDecimal(Double.toString(v2)); + if (b1.compareTo(BigDecimal.ZERO) == 0) + { + return BigDecimal.ZERO.doubleValue(); + } + return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue(); + } + + /** + * 提供精确的小数位四舍五入处理。 + * @param v 需要四舍五入的数字 + * @param scale 小数点后保留几位 + * @return 四舍五入后的结果 + */ + public static double round(double v, int scale) + { + if (scale < 0) + { + throw new IllegalArgumentException( + "The scale must be a positive integer or zero"); + } + BigDecimal b = new BigDecimal(Double.toString(v)); + BigDecimal one = BigDecimal.ONE; + return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/Base64ToMultipartFile.java b/bnhz-common/src/main/java/com/bnhz/common/utils/Base64ToMultipartFile.java new file mode 100644 index 0000000..96b8686 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/Base64ToMultipartFile.java @@ -0,0 +1,80 @@ +package com.bnhz.common.utils; + +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * @author fastb + * @version 1.0 + * @description: TODO + * @date 2023-12-26 9:27 + */ +public class Base64ToMultipartFile implements MultipartFile { + private final byte[] fileContent; + + private final String extension; + private final String contentType; + + + /** + * @param base64 + * @param dataUri 格式类似于: data:image/png;base64 + */ + public Base64ToMultipartFile(String base64, String dataUri) { + this.fileContent = Base64.getDecoder().decode(base64.getBytes(StandardCharsets.UTF_8)); + this.extension = dataUri.split(";")[0].split("/")[1]; + this.contentType = dataUri.split(";")[0].split(":")[1]; + } + + public Base64ToMultipartFile(String base64, String extension, String contentType) { + this.fileContent = Base64.getDecoder().decode(base64.getBytes(StandardCharsets.UTF_8)); + this.extension = extension; + this.contentType = contentType; + } + + @Override + public String getName() { + return "param_" + System.currentTimeMillis(); + } + + @Override + public String getOriginalFilename() { + return "file_" + System.currentTimeMillis() + "." + extension; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public boolean isEmpty() { + return fileContent == null || fileContent.length == 0; + } + + @Override + public long getSize() { + return fileContent.length; + } + + @Override + public byte[] getBytes() throws IOException { + return fileContent; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(fileContent); + } + + @Override + public void transferTo(File file) throws IOException, IllegalStateException { + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(fileContent); + } + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/BeanMapUtilByReflect.java b/bnhz-common/src/main/java/com/bnhz/common/utils/BeanMapUtilByReflect.java new file mode 100644 index 0000000..b1f8b52 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/BeanMapUtilByReflect.java @@ -0,0 +1,73 @@ +package com.bnhz.common.utils; + +import com.bnhz.common.core.thingsModel.ThingsModelSimpleItem; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BeanMapUtilByReflect { + + /** + * 对象转Map + * @param object + * @return + * @throws IllegalAccessException + */ + public static Map beanToMap(Object object) throws IllegalAccessException { + Map map = new HashMap(); + Field[] fields = object.getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + map.put(field.getName(), field.get(object)); + } + return map; + } + + /** + * bean转item对象 + * @param object + * @return + * @throws IllegalAccessException + */ + public static List beanToItem(Object object) throws IllegalAccessException { + List result = new ArrayList<>(); + Field[] fields = object.getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + ThingsModelSimpleItem item = new ThingsModelSimpleItem(); + item.setId(field.getName()); + item.setValue(field.get(object)+""); + item.setTs(DateUtils.getNowDate()); + result.add(item); + } + return result; + } + + /** + * map转对象 + * @param map + * @param beanClass + * @param + * @return + * @throws Exception + */ + public static T mapToBean(Map map, Class beanClass) throws Exception { + T object = beanClass.newInstance(); + Field[] fields = object.getClass().getDeclaredFields(); + for (Field field : fields) { + int mod = field.getModifiers(); + if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) { + continue; + } + field.setAccessible(true); + if (map.containsKey(field.getName())) { + field.set(object, map.get(field.getName())); + } + } + return object; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/CaculateUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/CaculateUtils.java new file mode 100644 index 0000000..2f445a9 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/CaculateUtils.java @@ -0,0 +1,365 @@ +package com.bnhz.common.utils; + +import com.bnhz.common.exception.ServiceException; +import io.netty.buffer.ByteBufUtil; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 字符串公式计算工具 + */ +public class CaculateUtils { + + /** + * /* + * 暂时只支持加减乘除及括号的应用 + */ + private static final String symbol = "+-,*/,(),%"; + + + /** + * 公式计算 字符串 + * + * @param exeStr + */ + public static BigDecimal execute(String exeStr, Map replaceMap) { + //替换掉占位符 + exeStr = caculateReplace(exeStr, replaceMap); + exeStr = exeStr.replaceAll("\\s*", ""); + List suffixList = suffixHandle(exeStr); + return caculateAnalyse(suffixList); + + } + + /** + * 公式计算 后序list + * + * @param suffixList + * @return + */ + public static BigDecimal caculateAnalyse(List suffixList) { + + BigDecimal a = BigDecimal.ZERO; + BigDecimal b = BigDecimal.ZERO; + // 构建一个操作数栈 每当获得操作符号时取出最上面两个数进行计算。 + Stack caculateStack = new Stack(); + if (suffixList.size() > 1) { + + for (int i = 0; i < suffixList.size(); i++) { + String temp = suffixList.get(i); + if (symbol.contains(temp)) { + b = caculateStack.pop(); + a = caculateStack.pop(); + a = caculate(a, b, temp.toCharArray()[0]); + caculateStack.push(a); + } else { + if (isNumber(suffixList.get(i))) { + caculateStack.push(new BigDecimal(suffixList.get(i))); + } else { + throw new RuntimeException("公式异常!"); + } + } + } + } else if (suffixList.size() == 1) { + String temp = suffixList.get(0); + if (isNumber(temp)) { + a = BigDecimal.valueOf(Double.parseDouble(temp)); + } else { + throw new RuntimeException("公式异常!"); + } + } + return a; + } + + + /** + * 计算 使用double 进行计算 如果需要可以在这里使用bigdecimal 进行计算 + * + * @param a + * @param b + * @param symbol + * @return + */ + public static BigDecimal caculate(BigDecimal a, BigDecimal b, char symbol) { + switch (symbol) { + case '+': { + return a.add(b).stripTrailingZeros(); + } + case '-': + return a.subtract(b).stripTrailingZeros(); + case '*': + return a.multiply(b); + case '/': + return a.divide(b); + case '%': + int length = b.toString().split("\\.")[1].length(); + return a.divide(b, length, BigDecimal.ROUND_HALF_UP); + default: + throw new RuntimeException("操作符号异常!"); + } + + } + + /** + * 字符串直接 转 后序 + */ + public static List suffixHandle(String exeStr) { + StringBuilder buf = new StringBuilder(); + Stack stack = new Stack(); + char[] exeChars = exeStr.toCharArray(); + List res = new ArrayList(); + for (char x : exeChars) { + // 判断是不是操作符号 + if (symbol.indexOf(x) > -1) { + // 不管怎样先将数据添加进列表 + if (buf.length() > 0) { + // 添加数据到res + String temp = buf.toString(); + // 验证是否为数 + if (!isNumber(temp)) throw new RuntimeException(buf.append(" 格式不对").toString()); + + // 添加到结果列表中 + res.add(temp); + // 清空临时buf + buf.delete(0, buf.length()); + } + if (stack.size() > 0) { + + //2.判断是不是开是括号 + if (x == '(') { + stack.push(x); + continue; + } + //3.判断是不是闭合括号 + if (x == ')') { + while (stack.size() > 0) { + char con = (char) stack.peek(); + if (con == '(') { + stack.pop(); + continue; + } else { + res.add(String.valueOf(stack.pop())); + } + } + continue; + } + // 取出最后最近的一个操作符 + char last = (char) stack.peek(); + if (compare(x, last) > 0) { + stack.push(x); + } else if (compare(x, last) <= 0) { + if (last != '(') { + res.add(String.valueOf(stack.pop())); + } + stack.push(x); + } + } else { + stack.push(x); + } + } else { + buf.append(x); + } + } + if (buf.length() > 0) res.add(buf.toString()); + while (stack.size() > 0) { + res.add(String.valueOf(stack.pop())); + } + return res; + + } + + + /** + * 比较两个操作符号的优先级 + * + * @param a + * @param b + * @return + */ + public static int compare(char a, char b) { + if (symbol.indexOf(a) - symbol.indexOf(b) > 1) { + return 1; + } else if (symbol.indexOf(a) - symbol.indexOf(b) < -1) { + return -1; + } else { + return 0; + } + } + + + /** + * 判断是否为数 字符串 + * + * @param str + * @return + */ + public static boolean isNumber(String str) { + Pattern pattern = Pattern.compile("[0-9]+\\.{0,1}[0-9]*"); + Matcher isNum = pattern.matcher(str); + return isNum.matches(); + } + + public static String caculateReplace(String str, Map map) { + for (Map.Entry entry : map.entrySet()) { + str = str.replaceAll(entry.getKey(), entry.getValue()==null ? "1" : entry.getValue()); + } + return str; + } + + public static String toFloat(byte[] bytes) throws IOException { + ByteArrayInputStream mByteArrayInputStream = new ByteArrayInputStream(bytes); + DataInputStream mDataInputStream = new DataInputStream(mByteArrayInputStream); + try { + float v = mDataInputStream.readFloat(); + return String.format("%.6f",v); + }catch (Exception e){ + throw new ServiceException("modbus16转浮点数错误"); + } + finally { + mDataInputStream.close(); + mByteArrayInputStream.close(); + } + } + + /** + * 转16位无符号整形 + * @param value + * @return + */ + public static String toUnSign16(long value) { + long unSigned = value & 0xFFFF; + return unSigned +""; // 将字节数组转换为十六进制字符串 + } + + /** + * 32位有符号CDAB数据类型 + * @param value + * @return + */ + public static String toSign32_CDAB(long value) { + byte[] bytes = intToBytes2((int) value); + return bytesToInt2(bytes)+""; + } + + /** + * 32位无符号ABCD数据类型 + * @param value + * @return + */ + public static String toUnSign32_ABCD(long value) { + return Integer.toUnsignedString((int) value); + } + + /** + * 32位无符号CDAB数据类型 + * @param value + * @return + */ + public static String toUnSign32_CDAB(long value) { + byte[] bytes = intToBytes2((int) value); + int val = bytesToInt2(bytes); + return Integer.toUnsignedString(val); + } + + /** + * 转32位浮点数 ABCD + * @param bytes + * @return + */ + public static float toFloat32_ABCD(byte[] bytes) { + int intValue = (bytes[0] << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); + return Float.intBitsToFloat(intValue); + } + + /** + * 转32位浮点数 CDAB + * @param bytes + * @return + */ + public static Float toFloat32_CDAB(byte[] bytes) { + int intValue = ((bytes[2] & 0xFF) << 24) | ((bytes[3] & 0xFF) << 16) | ((bytes[0] & 0xFF) << 8) | ((bytes[1] & 0xFF)) ; + return Float.intBitsToFloat(intValue); + } + + /** + * byte数组中取int数值,本方法适用于(低位在后,高位在前)的顺序。和intToBytes2()配套使用 + */ + public static int bytesToInt2(byte[] src) { + return (((src[2] & 0xFF) << 24) | ((src[3] & 0xFF) << 16) | ((src[0] & 0xFF) << 8) | (src[1] & 0xFF)); + } + + /** + * 将int数值转换为占四个字节的byte数组,本方法适用于(高位在前,低位在后)的顺序。 和bytesToInt2()配套使用 + */ + public static byte[] intToBytes2(int value) { + byte[] src = new byte[4]; + src[0] = (byte) ((value >> 24) & 0xFF); + src[1] = (byte) ((value >> 16) & 0xFF); + src[2] = (byte) ((value >> 8) & 0xFF); + src[3] = (byte) (value & 0xFF); + return src; + } + + public static String subHexValue(String hexString){ + //截取报文中的值 + String substring = hexString.substring(4, 6); + int index = Integer.parseInt(substring); + return hexString.substring(6, 6 + index*2); + } + + + public static void main(String[] args) throws IOException { + Map map = new HashMap<>(); + map.put("%s", "10"); + String caculate = caculateReplace("%s*2", map); + System.out.println(caculate); + System.out.println(execute("%s%3.00",map)); + + String s4 = toUnSign16(-1); + System.out.println("转16位无符号:"+s4); + + String s1 = toSign32_CDAB(40100); + System.out.println("转32位有符号-CDAB序"+s1); + + String s2 = toUnSign32_ABCD(-10); + System.out.println("转32位无符号-ABCD序:"+s2); + + String s3 = toUnSign32_CDAB(123456789); + System.out.println("转32位无符号-CDAB序:"+s3); + + String hexToBytes = "3fea3d71"; + byte[] bytes = ByteBufUtil.decodeHexDump(hexToBytes); + + float v1 = toFloat32_ABCD(bytes); + System.out.println("转32位浮点型-ABCD序:"+v1); + + String hexToBytes1= "800041EE"; + long i = Long.parseLong(hexToBytes1, 16); + System.out.println(i); + byte[] bytes1 = ByteBufUtil.decodeHexDump(hexToBytes1); + + float v2 = toFloat32_CDAB(bytes1); + System.out.println("转32位浮点型-CDAB序:"+v2); + + int signedShort = -32627; // 16位有符号整形 + // 将有符号短整型转换为无符号短整型 + int unSignedInt = signedShort & 0xFFFF; + // 输出结果 + System.out.println(unSignedInt); // 输出: 0 + + long l = Long.parseLong("00501F40", 16); + System.out.println(l); + + int val1 = -6553510; + byte[] bytes2 = intToBytes2(val1); + int i1 = bytesToInt2(bytes2); + System.out.println(i1); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/DateUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/DateUtils.java new file mode 100644 index 0000000..66b0ef9 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/DateUtils.java @@ -0,0 +1,318 @@ +package com.bnhz.common.utils; + +import java.lang.management.ManagementFactory; +import java.sql.Timestamp; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.Random; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.time.DateFormatUtils; + +/** + * 时间工具类 + * + * @author ruoyi + */ +@Slf4j +public class DateUtils extends org.apache.commons.lang3.time.DateUtils { + public static String YYYY = "yyyy"; + + public static String YYYY_MM = "yyyy-MM"; + + public static final String YYYY_MM_DD = "yyyy-MM-dd"; + + public static final String YYYYMMDDHHMMSS = "yyyyMMddHHmmss"; + + public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; + + public static final String SS_MM_HH_DD_HH_YY = "ssmmHHddMMyy"; + + public static String YY_MM_DD_HH_MM_SS = "yy-MM-dd HH:mm:ss"; + + private static String[] parsePatterns = { + "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", + "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", + "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM"}; + + /** + * 获取当前Date型日期 + * + * @return Date() 当前日期 + */ + public static Date getNowDate() { + return new Date(); + } + + /** + * 获取当前日期, 默认格式为yyyy-MM-dd + * + * @return String + */ + public static String getDate() { + return dateTimeNow(YYYY_MM_DD); + } + + public static final String getTime() { + return dateTimeNow(YYYY_MM_DD_HH_MM_SS); + } + + public static final String dateTimeNow() { + return dateTimeNow(YYYYMMDDHHMMSS); + } + + public static final String dateTimeNow(final String format) { + return parseDateToStr(format, new Date()); + } + + public static final String dateTime(final Date date) { + return parseDateToStr(YYYY_MM_DD, date); + } + + public static final String parseDateToStr(final String format, final Date date) { + return new SimpleDateFormat(format).format(date); + } + + public static final Date dateTime(final String format, final String ts) { + try { + return new SimpleDateFormat(format).parse(ts); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + /** + * 日期路径 即年/月/日 如2018/08/08 + */ + public static final String datePath() { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyy/MM/dd"); + } + + /** + * 日期路径 即年/月/日 如20180808 + */ + public static final String dateTime() { + Date now = new Date(); + return DateFormatUtils.format(now, "yyyyMMdd"); + } + + + /** + * 日期路径 即年/月/日 如20180808 + */ + public static final String dateTimeYY(Date date) { + return DateFormatUtils.format(date, YY_MM_DD_HH_MM_SS); + } + + public static String dateTimeYMDHMS(Date date) { + return DateFormatUtils.format(date, YY_MM_DD_HH_MM_SS); + } + + public static String dateTimeYYYYMDHMS(Date date) { + return DateFormatUtils.format(date, YYYY_MM_DD_HH_MM_SS); + } + + /** + * 日期型字符串转化为日期 格式 + */ + public static Date parseDate(Object str) { + if (str == null) { + return null; + } + try { + return parseDate(str.toString(), parsePatterns); + } catch (ParseException e) { + return null; + } + } + + /** + * 获取服务器启动时间 + */ + public static Date getServerStartDate() { + long time = ManagementFactory.getRuntimeMXBean().getStartTime(); + return new Date(time); + } + + /** + * 计算相差天数 + */ + public static int differentDaysByMillisecond(Date date1, Date date2) { + return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24))); + } + + /** + * 计算相差秒数 + */ + public static int differentSeconds(Date date1, Date date2) { + return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000))); + } + + /** + * 计算两个时间差 + */ + public static String getDatePoor(Date endDate, Date nowDate) { + long nd = 1000 * 24 * 60 * 60; + long nh = 1000 * 60 * 60; + long nm = 1000 * 60; + // long ns = 1000; + // 获得两个时间的毫秒时间差异 + long diff = endDate.getTime() - nowDate.getTime(); + // 计算差多少天 + long day = diff / nd; + // 计算差多少小时 + long hour = diff % nd / nh; + // 计算差多少分钟 + long min = diff % nd % nh / nm; + // 计算差多少秒//输出结果 + // long sec = diff % nd % nh % nm / ns; + return day + "天" + hour + "小时" + min + "分钟"; + } + + /** + * 增加 LocalDateTime ==> Date + */ + public static Date toDate(LocalDateTime temporalAccessor) { + ZonedDateTime zdt = temporalAccessor.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } + + /** + * 增加 LocalDate ==> Date + */ + public static Date toDate(LocalDate temporalAccessor) { + LocalDateTime localDateTime = LocalDateTime.of(temporalAccessor, LocalTime.of(0, 0, 0)); + ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault()); + return Date.from(zdt.toInstant()); + } + + public static long getTimestamp() { + return System.currentTimeMillis(); + } + + public static long getTimestampSeconds() { + return System.currentTimeMillis() / 1000; + } + + public static String generateRandomHex(int length) { + Random random = new Random(); + StringBuilder sb = new StringBuilder(length); + // 添加"D"作为开头 + sb.append("D"); + for (int i = 1; i < length; i++) { + int randomInt = random.nextInt(16); // 生成0到15的随机整数 + char hexChar = Character.toUpperCase(Character.forDigit(randomInt, 16)); // 将整数转换为十六进制字符并转为大写 + sb.append(hexChar); + } + return sb.toString(); + } + + + public static LocalDateTime toLocalDateTime(Date date) { + + if (ObjectUtils.isEmpty(date)) { + return null; + } + // Step 1: Convert Date to Instant + Instant instant = date.toInstant(); + + // Step 2: Get the current system default time zone + ZoneId zoneId = ZoneId.systemDefault(); + + // Step 3: Convert Instant to LocalDateTime using the time zone + return instant.atZone(zoneId).toLocalDateTime(); + } + + public static LocalDateTime toLocalDateTime(Long timestamp) { + if (ObjectUtils.isEmpty(timestamp)) { + return null; + } + + // 获取系统默认时区 + ZoneId zoneId = ZoneId.systemDefault(); + + // 将long类型的时间戳转换为LocalDateTime + return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), zoneId); + } + + public static long toTimestamp(LocalDate date) { + + LocalDateTime localDateTime = date.atStartOfDay(); + + // 将 LocalDateTime 转换成 Instant,并获取纪元秒数或纪元毫秒数 + Instant instant = localDateTime.toInstant(ZoneOffset.UTC); + + // 获取时间戳(纪元秒数) + return instant.getEpochSecond(); + } + public static long toTimestamp(LocalDateTime dateTime) { + // 将 LocalDateTime 转换成 Instant,并获取纪元秒数或纪元毫秒数 + Instant instant = dateTime.toInstant(ZoneOffset.of("+8")); + + // 获取时间戳(纪元秒数) + return instant.getEpochSecond(); + } + + public static String toDateStr(LocalDate date) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(YYYY_MM_DD); + + // 将LocalDate格式化为字符串 + return date.format(formatter); + } + + public static String toDateTimeStr(LocalDateTime date) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS); + + // 将LocalDate格式化为字符串 + return date.format(formatter); + } + + public static String toISOUtc8Time(LocalDateTime localDateTime) { + + localDateTime = localDateTime.minusHours(8); + // 将LocalDateTime转为ZonedDateTime,设置时区为UTC + ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("UTC")); + + // 定义DateTimeFormatter,格式化为ISO 8601字符串 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"); + + // 格式化ZonedDateTime为字符串 + return zonedDateTime.format(formatter); + } + + @SneakyThrows + public static Date toDateSSS(String yyyyMMddHHmmssSSS) { + // 要转换的日期字符串 + SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmssSSS"); + + // 将字符串转换为日期对象 + return formatter.parse(yyyyMMddHHmmssSSS); + } + + /** + * 计算两个时间相差的分钟数 + * @param dateTime1 时间1 + * @param dateTime2 时间2 + * @return dateTime1 - dateTime2 + */ + public static long betweenMinute(LocalDateTime dateTime1, LocalDateTime dateTime2) { + // 计算两个 LocalDateTime 之间的 Duration + Duration duration = Duration.between(dateTime1, dateTime2); + + // 获取相差的分钟数 + return duration.toMinutes(); + } + + public static void main(String[] args) { + LocalDateTime now = LocalDateTime.now(); + long diff = betweenMinute(now, now.plusMinutes(5)); + System.out.println(diff); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/DictUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/DictUtils.java new file mode 100644 index 0000000..825d751 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/DictUtils.java @@ -0,0 +1,186 @@ +package com.bnhz.common.utils; + +import java.util.Collection; +import java.util.List; +import com.alibaba.fastjson2.JSONArray; +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.core.domain.entity.SysDictData; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.utils.spring.SpringUtils; + +/** + * 字典工具类 + * + * @author ruoyi + */ +public class DictUtils +{ + /** + * 分隔符 + */ + public static final String SEPARATOR = ","; + + /** + * 设置字典缓存 + * + * @param key 参数键 + * @param dictDatas 字典数据列表 + */ + public static void setDictCache(String key, List dictDatas) + { + SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas); + } + + /** + * 获取字典缓存 + * + * @param key 参数键 + * @return dictDatas 字典数据列表 + */ + public static List getDictCache(String key) + { + JSONArray arrayCache = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key)); + if (StringUtils.isNotNull(arrayCache)) + { + return arrayCache.toList(SysDictData.class); + } + return null; + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue) + { + return getDictLabel(dictType, dictValue, SEPARATOR); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel) + { + return getDictValue(dictType, dictLabel, SEPARATOR); + } + + /** + * 根据字典类型和字典值获取字典标签 + * + * @param dictType 字典类型 + * @param dictValue 字典值 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String getDictLabel(String dictType, String dictValue, String separator) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + + if (StringUtils.isNotNull(datas)) + { + if (StringUtils.containsAny(separator, dictValue)) + { + for (SysDictData dict : datas) + { + for (String value : dictValue.split(separator)) + { + if (value.equals(dict.getDictValue())) + { + propertyString.append(dict.getDictLabel()).append(separator); + break; + } + } + } + } + else + { + for (SysDictData dict : datas) + { + if (dictValue.equals(dict.getDictValue())) + { + return dict.getDictLabel(); + } + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 根据字典类型和字典标签获取字典值 + * + * @param dictType 字典类型 + * @param dictLabel 字典标签 + * @param separator 分隔符 + * @return 字典值 + */ + public static String getDictValue(String dictType, String dictLabel, String separator) + { + StringBuilder propertyString = new StringBuilder(); + List datas = getDictCache(dictType); + + if (StringUtils.containsAny(separator, dictLabel) && StringUtils.isNotEmpty(datas)) + { + for (SysDictData dict : datas) + { + for (String label : dictLabel.split(separator)) + { + if (label.equals(dict.getDictLabel())) + { + propertyString.append(dict.getDictValue()).append(separator); + break; + } + } + } + } + else + { + for (SysDictData dict : datas) + { + if (dictLabel.equals(dict.getDictLabel())) + { + return dict.getDictValue(); + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 删除指定字典缓存 + * + * @param key 字典键 + */ + public static void removeDictCache(String key) + { + SpringUtils.getBean(RedisCache.class).deleteObject(getCacheKey(key)); + } + + /** + * 清空字典缓存 + */ + public static void clearDictCache() + { + Collection keys = SpringUtils.getBean(RedisCache.class).keys(CacheConstants.SYS_DICT_KEY + "*"); + SpringUtils.getBean(RedisCache.class).deleteObject(keys); + } + + /** + * 设置cache key + * + * @param configKey 参数键 + * @return 缓存键key + */ + public static String getCacheKey(String configKey) + { + return CacheConstants.SYS_DICT_KEY + configKey; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/DigestUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/DigestUtils.java new file mode 100644 index 0000000..f1fa8fa --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/DigestUtils.java @@ -0,0 +1,75 @@ +package com.bnhz.common.utils; + +import com.bnhz.common.utils.uuid.IdUtils; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.Validate; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.SecureRandom; + +@NoArgsConstructor +public class DigestUtils { + private static SecureRandom random = new SecureRandom(); + private static IdUtils idUtils = new IdUtils(0,0); + + public static String getId(){ + return String.valueOf(Math.abs(random.nextLong())); + } + + public static String nextId(){ + return String.valueOf(idUtils.nextId()); + } + + + public static byte[] genSalt(int numBytes) { + Validate.isTrue(numBytes > 0, "numBytes argument must be a positive integer (1 or larger)", (long)numBytes); + byte[] bytes = new byte[numBytes]; + random.nextBytes(bytes); + return bytes; + } + + public static byte[] digest(byte[] input, String algorithm, byte[] salt, int iterations) { + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + if(salt != null) { + digest.update(salt); + } + + byte[] result = digest.digest(input); + + for(int i = 1; i < iterations; ++i) { + digest.reset(); + result = digest.digest(result); + } + + return result; + } catch (GeneralSecurityException var7) { + throw ExceptionUtils.unchecked(var7); + } + } + + public static byte[] digest(InputStream input, String algorithm) throws IOException { + try { + MessageDigest messageDigest = MessageDigest.getInstance(algorithm); + int bufferLength = 8192; + byte[] buffer = new byte[bufferLength]; + + for(int read = input.read(buffer, 0, bufferLength); read > -1; read = input.read(buffer, 0, bufferLength)) { + messageDigest.update(buffer, 0, read); + } + + return messageDigest.digest(); + } catch (GeneralSecurityException var6) { + throw ExceptionUtils.unchecked(var6); + } + } + + public static void main(String[] args) { + for (int i = 0; i < 10; i++) { + System.out.println(nextId()); + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/EmqxUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/EmqxUtils.java new file mode 100644 index 0000000..d955143 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/EmqxUtils.java @@ -0,0 +1,9 @@ +package com.bnhz.common.utils; + +/** + * @author bill + */ +public class EmqxUtils { + + //获取 +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/EncodeUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/EncodeUtils.java new file mode 100644 index 0000000..8000e4c --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/EncodeUtils.java @@ -0,0 +1,172 @@ +package com.bnhz.common.utils; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringEscapeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.crypto.codec.Hex; +import org.springframework.web.multipart.MultipartFile; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.regex.Pattern; + +public class EncodeUtils { + + private static final Logger logger = LoggerFactory.getLogger(EncodeUtils.class); + private static final String DEFAULT_URL_ENCODING = "UTF-8"; + private static final char[] BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray(); + private static Pattern p1 = Pattern.compile("<\\s*(script|link|style|iframe)([\\s\\S]+?)<\\/\\s*\\1\\s*>", 2); + private static Pattern p2 = Pattern.compile("\\s*on[a-z]+\\s*=\\s*(\"[^\"]+\"|'[^']+'|[^\\s]+)\\s*(?=>)", 2); + private static Pattern p3 = Pattern.compile("\\s*(href|src)\\s*=\\s*(\"\\s*(javascript|vbscript):[^\"]+\"|'\\s*(javascript|vbscript):[^']+'|(javascript|vbscript):[^\\s]+)\\s*(?=>)", 2); + private static Pattern p4 = Pattern.compile("epression\\((.|\\n)*\\);?", 2); + private static Pattern p5 = Pattern.compile("(?:')|(?:--)|(/\\*(?:.|[\\n\\r])*?\\*/)|(\\b(select|update|and|or|delete|insert|trancate|char|into|substr|ascii|declare|exec|count|master|into|drop|execute)\\b)", 2); + + public EncodeUtils() { + } + + public static String encodeHex(byte[] input) { + return new String(Hex.encode(input)); + } + + public static byte[] decodeHex(String input) { + try { + return Hex.decode(input); + } catch (Exception var2) { + throw ExceptionUtils.unchecked(var2); + } + } + + public static String encodeBase64(byte[] input) { + return new String(Base64.encodeBase64(input)); + } + + public static String encodeBase64(String input) { + try { + return new String(Base64.encodeBase64(input.getBytes("UTF-8"))); + } catch (UnsupportedEncodingException var2) { + return ""; + } + } + + public static byte[] decodeBase64(String input) { + return Base64.decodeBase64(input.getBytes()); + } + + public static String decodeBase64String(String input) { + try { + return new String(Base64.decodeBase64(input.getBytes()), "UTF-8"); + } catch (UnsupportedEncodingException var2) { + return ""; + } + } + + public static String encodeBase62(byte[] input) { + char[] chars = new char[input.length]; + + for(int i = 0; i < input.length; ++i) { + chars[i] = BASE62[(input[i] & 255) % BASE62.length]; + } + + return new String(chars); + } + + public static String encodeHtml(String html) { + return StringEscapeUtils.escapeHtml4(html); + } + + public static String decodeHtml(String htmlEscaped) { + return StringEscapeUtils.unescapeHtml4(htmlEscaped); + } + + public static String encodeXml(String xml) { + return StringEscapeUtils.escapeXml(xml); + } + + public static String decodeXml(String xmlEscaped) { + return StringEscapeUtils.unescapeXml(xmlEscaped); + } + + public static String encodeUrl(String part) { + return encodeUrl(part, "UTF-8"); + } + + public static String encodeUrl(String part, String encoding) { + if(part == null) { + return null; + } else { + try { + return URLEncoder.encode(part, encoding); + } catch (UnsupportedEncodingException var3) { + throw ExceptionUtils.unchecked(var3); + } + } + } + + public static String decodeUrl(String part) { + return decodeUrl(part, "UTF-8"); + } + + public static String decodeUrl(String part, String encoding) { + try { + return URLDecoder.decode(part, encoding); + } catch (UnsupportedEncodingException var3) { + throw ExceptionUtils.unchecked(var3); + } + } + + public static String decodeUrl2(String part) { + return decodeUrl(decodeUrl(part)); + } + + public static String xssFilter(String text) { + if(text == null) { + return null; + } else { + String oriValue = StringUtils.trim(text); + String value = p1.matcher(oriValue).replaceAll(""); + value = p2.matcher(value).replaceAll(""); + value = p3.matcher(value).replaceAll(""); + value = p4.matcher(value).replaceAll(""); + if(!StringUtils.startsWithIgnoreCase(value, "") && !StringUtils.startsWithIgnoreCase(value, "", ">"); + } + + if(logger.isInfoEnabled() && !value.equals(oriValue)) { + logger.info("xssFilter: {} to {}", text, value); + } + + return value; + } + } + + public static String sqlFilter(String text) { + if(text != null) { + String value = p5.matcher(text).replaceAll(""); + if(logger.isWarnEnabled() && !value.equals(text)) { + logger.warn("sqlFilter: {} to {}", text, value); + return ""; + } else { + return value; + } + } else { + return null; + } + } + + public static MultipartFile base64toMultipartFile(String base64) { + final String[] base64Array = base64.split(","); + String dataUir, data; + if (base64Array.length > 1) { + dataUir = base64Array[0]; + data = base64Array[1]; + } else { + //根据你base64代表的具体文件构建 + dataUir = "data:image/png;base64"; + data = base64Array[0]; + } + return new Base64ToMultipartFile(data, dataUir); + } +} + diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/ExceptionUtil.java b/bnhz-common/src/main/java/com/bnhz/common/utils/ExceptionUtil.java new file mode 100644 index 0000000..ed65062 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/ExceptionUtil.java @@ -0,0 +1,39 @@ +package com.bnhz.common.utils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import org.apache.commons.lang3.exception.ExceptionUtils; + +/** + * 错误信息处理类。 + * + * @author ruoyi + */ +public class ExceptionUtil +{ + /** + * 获取exception的详细错误信息。 + */ + public static String getExceptionMessage(Throwable e) + { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw, true)); + return sw.toString(); + } + + public static String getRootErrorMessage(Exception e) + { + Throwable root = ExceptionUtils.getRootCause(e); + root = (root == null ? e : root); + if (root == null) + { + return ""; + } + String msg = root.getMessage(); + if (msg == null) + { + return "null"; + } + return StringUtils.defaultString(msg); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/ExceptionUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/ExceptionUtils.java new file mode 100644 index 0000000..fd5529b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/ExceptionUtils.java @@ -0,0 +1,53 @@ +package com.bnhz.common.utils; + +import lombok.NoArgsConstructor; + +import javax.servlet.http.HttpServletRequest; +import java.io.PrintWriter; +import java.io.StringWriter; + +@NoArgsConstructor +public class ExceptionUtils { + + + public static Throwable getThrowable(HttpServletRequest request) { + Throwable ex = null; + if(request.getAttribute("exception") != null) { + ex = (Throwable)request.getAttribute("exception"); + } else if(request.getAttribute("javax.servlet.error.exception") != null) { + ex = (Throwable)request.getAttribute("javax.servlet.error.exception"); + } + + return ex; + } + + public static String getStackTraceAsString(Throwable e) { + if(e == null) { + return ""; + } else { + StringWriter stringWriter = new StringWriter(); + e.printStackTrace(new PrintWriter(stringWriter)); + return stringWriter.toString(); + } + } + + public static boolean isCausedBy(Exception ex, Class... causeExceptionClasses) { + for(Throwable cause = ex.getCause(); cause != null; cause = cause.getCause()) { + Class[] var3 = causeExceptionClasses; + int var4 = causeExceptionClasses.length; + + for(int var5 = 0; var5 < var4; ++var5) { + Class causeClass = var3[var5]; + if(causeClass.isInstance(cause)) { + return true; + } + } + } + + return false; + } + + public static RuntimeException unchecked(Exception e) { + return e instanceof RuntimeException?(RuntimeException)e:new RuntimeException(e); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/LogUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/LogUtils.java new file mode 100644 index 0000000..edaf9ea --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/LogUtils.java @@ -0,0 +1,18 @@ +package com.bnhz.common.utils; + +/** + * 处理并记录日志文件 + * + * @author ruoyi + */ +public class LogUtils +{ + public static String getBlock(Object msg) + { + if (msg == null) + { + msg = ""; + } + return "[" + msg.toString() + "]"; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/MapUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/MapUtils.java new file mode 100644 index 0000000..9112352 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/MapUtils.java @@ -0,0 +1,66 @@ +package com.bnhz.common.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.bnhz.common.core.text.KeyValue; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Map 工具类 + * + * @author 芋道源码 + */ +public class MapUtils { + + /** + * 从哈希表表中,获得 keys 对应的所有 value 数组 + * + * @param multimap 哈希表 + * @param keys keys + * @return value 数组 + */ + public static List getList(Multimap multimap, Collection keys) { + List result = new ArrayList<>(); + keys.forEach(k -> { + Collection values = multimap.get(k); + if (CollectionUtil.isEmpty(values)) { + return; + } + result.addAll(values); + }); + return result; + } + + /** + * 从哈希表查找到 key 对应的 value,然后进一步处理 + * 注意,如果查找到的 value 为 null 时,不进行处理 + * + * @param map 哈希表 + * @param key key + * @param consumer 进一步处理的逻辑 + */ + public static void findAndThen(Map map, K key, Consumer consumer) { + if (CollUtil.isEmpty(map)) { + return; + } + V value = map.get(key); + if (value == null) { + return; + } + consumer.accept(value); + } + + public static Map convertMap(List> keyValues) { + Map map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); + keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); + return map; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/Md5Utils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/Md5Utils.java new file mode 100644 index 0000000..cd59905 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/Md5Utils.java @@ -0,0 +1,82 @@ +package com.bnhz.common.utils; + +import lombok.NoArgsConstructor; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +@NoArgsConstructor +public class Md5Utils { + private static final String MD5 = "MD5"; + private static final String DEFAULT_ENCODING = "UTF-8"; + + + + public static String md5(String input) { + return md5((String) input, 1); + } + + public static String md5(String input, int iterations) { + try { + return EncodeUtils.encodeHex(DigestUtils.digest(input.getBytes("UTF-8"), "MD5", (byte[]) null, iterations)); + } catch (UnsupportedEncodingException var3) { + return ""; + } + } + + public static byte[] md5(byte[] input) { + return md5((byte[]) input, 1); + } + + public static byte[] md5(byte[] input, int iterations) { + return DigestUtils.digest(input, "MD5", (byte[]) null, iterations); + } + + public static byte[] md5(InputStream input) throws IOException { + return DigestUtils.digest(input, "MD5"); + } + + public static boolean isMd5(String str) { + int cnt = 0; + for (int i = 0; i < str.length(); ++i) { + switch (str.charAt(i)) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + ++cnt; + if (32 <= cnt) return true; + break; + case '/': + if ((i + 10) < str.length()) {// "/storage/" + char ch1 = str.charAt(i + 1); + char ch2 = str.charAt(i + 8); + if ('/' == ch2 && ('s' == ch1 || 'S' == ch1)) return true; + } + default: + cnt = 0; + break; + } + } + return false; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/MessageUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/MessageUtils.java new file mode 100644 index 0000000..0a2b41f --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/MessageUtils.java @@ -0,0 +1,26 @@ +package com.bnhz.common.utils; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import com.bnhz.common.utils.spring.SpringUtils; + +/** + * 获取i18n资源文件 + * + * @author ruoyi + */ +public class MessageUtils +{ + /** + * 根据消息键和参数 获取消息 委托给spring messageSource + * + * @param code 消息键 + * @param args 参数 + * @return 获取国际化翻译值 + */ + public static String message(String code, Object... args) + { + MessageSource messageSource = SpringUtils.getBean(MessageSource.class); + return messageSource.getMessage(code, args, LocaleContextHolder.getLocale()); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/PageUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/PageUtils.java new file mode 100644 index 0000000..f15854a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/PageUtils.java @@ -0,0 +1,46 @@ +package com.bnhz.common.utils; + +import com.github.pagehelper.PageHelper; +import com.bnhz.common.core.page.PageDomain; +import com.bnhz.common.core.page.TableSupport; +import com.bnhz.common.utils.sql.SqlUtil; + +/** + * 分页工具类 + * + * @author ruoyi + */ +public class PageUtils extends PageHelper +{ + /** + * 设置请求分页数据 + */ + public static void startPage() + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + Integer pageNum = pageDomain.getPageNum(); + Integer pageSize = pageDomain.getPageSize(); + String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); + Boolean reasonable = pageDomain.getReasonable(); + PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable); + } + + /** + * 清理分页的线程变量 + */ + public static void clearPage() + { + PageHelper.clearPage(); + } + public static boolean hasNext(long total, int pageNo, int pageSize) { + if (pageNo == 0) { + pageNo = 1; + } + return total > (long) pageNo * pageSize; + } + + public static boolean hasNextByStartAt(long total ,int startAt, int currentSize) { + return startAt + currentSize < total; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/SecurityUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/SecurityUtils.java new file mode 100644 index 0000000..a74e978 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/SecurityUtils.java @@ -0,0 +1,120 @@ +package com.bnhz.common.utils; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import com.bnhz.common.constant.HttpStatus; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.exception.ServiceException; + +/** + * 安全服务工具类 + * + * @author ruoyi + */ +public class SecurityUtils +{ + /** + * 用户ID + **/ + public static Long getUserId() + { + try + { + return getLoginUser().getUserId(); + } + catch (Exception e) + { + throw new ServiceException("获取用户ID异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取部门ID + **/ + public static Long getDeptId() + { + try + { + return getLoginUser().getDeptId(); + } + catch (Exception e) + { + throw new ServiceException("获取部门ID异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取用户账户 + **/ + public static String getUsername() + { + try + { + return getLoginUser().getUsername(); + } + catch (Exception e) + { + throw new ServiceException("获取用户账户异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取用户 + **/ + public static LoginUser getLoginUser() + { + try + { + return (LoginUser) getAuthentication().getPrincipal(); + } + catch (Exception e) + { + throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED); + } + } + + /** + * 获取Authentication + */ + public static Authentication getAuthentication() + { + return SecurityContextHolder.getContext().getAuthentication(); + } + + /** + * 生成BCryptPasswordEncoder密码 + * + * @param password 密码 + * @return 加密字符串 + */ + public static String encryptPassword(String password) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.encode(password); + } + + /** + * 判断密码是否相同 + * + * @param rawPassword 真实密码 + * @param encodedPassword 加密后字符 + * @return 结果 + */ + public static boolean matchesPassword(String rawPassword, String encodedPassword) + { + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 是否为管理员 + * + * @param userId 用户ID + * @return 结果 + */ + public static boolean isAdmin(Long userId) + { + return userId != null && 1L == userId; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/ServletUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/ServletUtils.java new file mode 100644 index 0000000..f60e4e7 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/ServletUtils.java @@ -0,0 +1,228 @@ +package com.bnhz.common.utils; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import cn.hutool.extra.servlet.ServletUtil; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.core.text.Convert; + +/** + * 客户端工具类 + * + * @author ruoyi + */ +public class ServletUtils +{ + /** + * 获取String参数 + */ + public static String getParameter(String name) + { + return getRequest().getParameter(name); + } + + /** + * 获取String参数 + */ + public static String getParameter(String name, String defaultValue) + { + return Convert.toStr(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name) + { + return Convert.toInt(getRequest().getParameter(name)); + } + + /** + * 获取Integer参数 + */ + public static Integer getParameterToInt(String name, Integer defaultValue) + { + return Convert.toInt(getRequest().getParameter(name), defaultValue); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name) + { + return Convert.toBool(getRequest().getParameter(name)); + } + + /** + * 获取Boolean参数 + */ + public static Boolean getParameterToBool(String name, Boolean defaultValue) + { + return Convert.toBool(getRequest().getParameter(name), defaultValue); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParams(ServletRequest request) + { + final Map map = request.getParameterMap(); + return Collections.unmodifiableMap(map); + } + + /** + * 获得所有请求参数 + * + * @param request 请求对象{@link ServletRequest} + * @return Map + */ + public static Map getParamMap(ServletRequest request) + { + Map params = new HashMap<>(); + for (Map.Entry entry : getParams(request).entrySet()) + { + params.put(entry.getKey(), StringUtils.join(entry.getValue(), ",")); + } + return params; + } + + /** + * 获取request + */ + public static HttpServletRequest getRequest() + { + return getRequestAttributes().getRequest(); + } + + /** + * 获取response + */ + public static HttpServletResponse getResponse() + { + return getRequestAttributes().getResponse(); + } + + /** + * 获取session + */ + public static HttpSession getSession() + { + return getRequest().getSession(); + } + + public static ServletRequestAttributes getRequestAttributes() + { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + return (ServletRequestAttributes) attributes; + } + + /** + * 将字符串渲染到客户端 + * + * @param response 渲染对象 + * @param string 待渲染的字符串 + */ + public static void renderString(HttpServletResponse response, String string) + { + try + { + response.setStatus(200); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().print(string); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + /** + * 是否是Ajax异步请求 + * + * @param request + */ + public static boolean isAjaxRequest(HttpServletRequest request) + { + String accept = request.getHeader("accept"); + if (accept != null && accept.contains("application/json")) + { + return true; + } + + String xRequestedWith = request.getHeader("X-Requested-With"); + if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) + { + return true; + } + + String uri = request.getRequestURI(); + if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) + { + return true; + } + + String ajax = request.getParameter("__ajax"); + return StringUtils.inStringIgnoreCase(ajax, "json", "xml"); + } + + /** + * 内容编码 + * + * @param str 内容 + * @return 编码后的内容 + */ + public static String urlEncode(String str) + { + try + { + return URLEncoder.encode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } + + /** + * 内容解码 + * + * @param str 内容 + * @return 解码后的内容 + */ + public static String urlDecode(String str) + { + try + { + return URLDecoder.decode(str, Constants.UTF8); + } + catch (UnsupportedEncodingException e) + { + return StringUtils.EMPTY; + } + } + + public static String getClientIP() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return ServletUtil.getClientIP(request); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/StringUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/StringUtils.java new file mode 100644 index 0000000..d55301d --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/StringUtils.java @@ -0,0 +1,871 @@ +package com.bnhz.common.utils; + +import com.bnhz.common.constant.Constants; +import com.bnhz.common.core.text.StrFormatter; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import org.apache.commons.collections4.MapUtils; +import org.springframework.util.AntPathMatcher; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 字符串工具类 + * + * @author ruoyi + */ +public class StringUtils extends org.apache.commons.lang3.StringUtils { + /** + * 空字符串 + */ + private static final String NULLSTR = ""; + + /** + * 下划线 + */ + private static final char SEPARATOR = '_'; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) { + return value != null ? value : defaultValue; + } + + /** + * * 判断一个Collection是否为空, 包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Collection coll) { + return isNull(coll) || coll.isEmpty(); + } + + /** + * * 判断一个Collection是否非空,包含List,Set,Queue + * + * @param coll 要判断的Collection + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Collection coll) { + return !isEmpty(coll); + } + + /** + * * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + * * @return true:为空 false:非空 + */ + public static boolean isEmpty(Object[] objects) { + return isNull(objects) || (objects.length == 0); + } + + /** + * * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Object[] objects) { + return !isEmpty(objects); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:为空 false:非空 + */ + public static boolean isEmpty(Map map) { + return isNull(map) || map.isEmpty(); + } + + /** + * * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true:非空 false:空 + */ + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + /** + * * 判断一个字符串是否为空串 + * + * @param str String + * @return true:为空 false:非空 + */ + public static boolean isEmpty(String str) { + return isNull(str) || NULLSTR.equals(str.trim()); + } + + /** + * * 判断一个字符串是否为非空串 + * + * @param str String + * @return true:非空串 false:空串 + */ + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * * 判断一个对象是否为空 + * + * @param object Object + * @return true:为空 false:非空 + */ + public static boolean isNull(Object object) { + return object == null; + } + + /** + * * 判断一个对象是否非空 + * + * @param object Object + * @return true:非空 false:空 + */ + public static boolean isNotNull(Object object) { + return !isNull(object); + } + + /** + * * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true:是数组 false:不是数组 + */ + public static boolean isArray(Object object) { + return isNotNull(object) && object.getClass().isArray(); + } + + /** + * 去空格 + */ + public static String trim(String str) { + return (str == null ? "" : str.trim()); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) { + if (str == null) { + return NULLSTR; + } + + if (start < 0) { + start = str.length() + start; + } + + if (start < 0) { + start = 0; + } + if (start > str.length()) { + return NULLSTR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) { + if (str == null) { + return NULLSTR; + } + + if (end < 0) { + end = str.length() + end; + } + if (start < 0) { + start = str.length() + start; + } + + if (end > str.length()) { + end = str.length(); + } + + if (start > end) { + return NULLSTR; + } + + if (start < 0) { + start = 0; + } + if (end < 0) { + end = 0; + } + + return str.substring(start, end); + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(String template, Object... params) { + if (isEmpty(params) || isEmpty(template)) { + return template; + } + return StrFormatter.format(template, params); + } + + /** + * 是否为http(s)://开头 + * + * @param link 链接 + * @return 结果 + */ + public static boolean ishttp(String link) { + return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS); + } + + /** + * 字符串转set + * + * @param str 字符串 + * @param sep 分隔符 + * @return set集合 + */ + public static final Set str2Set(String str, String sep) { + return new HashSet(str2List(str, sep, true, false)); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @param filterBlank 过滤纯空白 + * @param trim 去掉首尾空白 + * @return list集合 + */ + public static final List str2List(String str, String sep, boolean filterBlank, boolean trim) { + List list = new ArrayList(); + if (StringUtils.isEmpty(str)) { + return list; + } + + // 过滤空白字符串 + if (filterBlank && StringUtils.isBlank(str)) { + return list; + } + String[] split = str.split(sep); + for (String string : split) { + if (filterBlank && StringUtils.isBlank(string)) { + continue; + } + if (trim) { + string = string.trim(); + } + list.add(string); + } + + return list; + } + + /** + * 判断给定的set列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value + * + * @param set 给定的集合 + * @param array 给定的数组 + * @return boolean 结果 + */ + public static boolean containsAny(Collection collection, String... array) { + if (isEmpty(collection) || isEmpty(array)) { + return false; + } else { + for (String str : array) { + if (collection.contains(str)) { + return true; + } + } + return false; + } + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + * + * @param cs 指定字符串 + * @param searchCharSequences 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + */ + public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) { + if (isEmpty(cs) || isEmpty(searchCharSequences)) { + return false; + } + for (CharSequence testStr : searchCharSequences) { + if (containsIgnoreCase(cs, testStr)) { + return true; + } + } + return false; + } + + /** + * 驼峰转下划线命名 + */ + public static String toUnderScoreCase(String str) { + if (str == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean curreCharIsUpperCase = true; + // 下一字符是否大写 + boolean nexteCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (i > 0) { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } else { + preCharIsUpperCase = false; + } + + curreCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) { + nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase) { + sb.append(SEPARATOR); + } else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase) { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strs 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strs) { + if (str != null && strs != null) { + for (String s : strs) { + if (str.equalsIgnoreCase(trim(s))) { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) { + // 没必要转换 + return ""; + } else if (!name.contains("_")) { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 例如:user_name->userName + */ + public static String toCamelCase(String s) { + if (s == null) { + return null; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (c == SEPARATOR) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strs 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strs) { + if (isEmpty(str) || isEmpty(strs)) { + return false; + } + for (String pattern : strs) { + if (isMatch(pattern, str)) { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: + * ? 表示单个字符; + * * 表示一层路径内的任意字符串,不可跨层级; + * ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + * @return + */ + public static boolean isMatch(String pattern, String url) { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + @SuppressWarnings("unchecked") + public static T cast(Object obj) { + return (T) obj; + } + + /** + * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 + * + * @param num 数字对象 + * @param size 字符串指定长度 + * @return 返回数字的字符串格式,该字符串为指定长度。 + */ + public static final String padl(final Number num, final int size) { + return padl(num.toString(), size, '0'); + } + + /** + * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。 + * + * @param s 原始字符串 + * @param size 字符串指定长度 + * @param c 用于补齐的字符 + * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 + */ + public static final String padl(final String s, final int size, final char c) { + final StringBuilder sb = new StringBuilder(size); + if (s != null) { + final int len = s.length(); + if (s.length() <= size) { + for (int i = size - len; i > 0; i--) { + sb.append(c); + } + sb.append(s); + } else { + return s.substring(len - size, len); + } + } else { + for (int i = size; i > 0; i--) { + sb.append(c); + } + } + return sb.toString(); + } + + /*将字符串转小写,首字母大写,其他小写*/ + public static String upperCase(String str) { + char[] ch = str.toLowerCase().toCharArray(); + if (ch[0] >= 'a' && ch[0] <= 'z') { + ch[0] = (char) (ch[0] - 32); + } + return new String(ch); + } + + public static String toString(Object value) { + if (value == null) { + return "null"; + } + if (value instanceof ByteBuf) { + return ByteBufUtil.hexDump((ByteBuf) value); + } + if (!value.getClass().isArray()) { + return value.toString(); + } + + StringBuilder root = new StringBuilder(32); + toString(value, root); + return root.toString(); + } + + public static StringBuilder toString(Object value, StringBuilder builder) { + if (value == null) { + return builder; + } + + builder.append('['); + int start = builder.length(); + + if (value instanceof long[]) { + long[] array = (long[]) value; + for (long t : array) { + builder.append(t).append(','); + } + + } else if (value instanceof int[]) { + int[] array = (int[]) value; + for (int t : array) { + builder.append(t).append(','); + } + + } else if (value instanceof short[]) { + short[] array = (short[]) value; + for (short t : array) { + builder.append(t).append(','); + } + + } else if (value instanceof byte[]) { + byte[] array = (byte[]) value; + for (byte t : array) { + builder.append(t).append(','); + } + + } else if (value instanceof char[]) { + char[] array = (char[]) value; + for (char t : array) { + builder.append(t).append(','); + } + + } else if (value instanceof double[]) { + double[] array = (double[]) value; + for (double t : array) { + builder.append(t).append(','); + } + + } else if (value instanceof float[]) { + float[] array = (float[]) value; + for (float t : array) { + builder.append(t).append(','); + } + + } else if (value instanceof boolean[]) { + boolean[] array = (boolean[]) value; + for (boolean t : array) { + builder.append(t).append(','); + } + + } else if (value instanceof String[]) { + String[] array = (String[]) value; + for (String t : array) { + builder.append(t).append(','); + } + + } else if (isArray1(value)) { + Object[] array = (Object[]) value; + for (Object t : array) { + toString(t, builder).append(','); + } + + } else if (value instanceof Object[]) { + Object[] array = (Object[]) value; + for (Object t : array) { + builder.append(t).append(','); + } + } + + int end = builder.length(); + if (end <= start) { + builder.append(']'); + } else { + builder.setCharAt(end - 1, ']'); + } + return builder; + } + + private static boolean isArray1(Object value) { + Class componentType = value.getClass().getComponentType(); + if (componentType == null) { + return false; + } + return componentType.isArray(); + } + + public static String leftPad(String str, int size, char ch) { + int length = str.length(); + int pads = size - length; + if (pads > 0) { + char[] result = new char[size]; + str.getChars(0, length, result, pads); + while (pads > 0) { + result[--pads] = ch; + } + return new String(result); + } + return str; + } + + /** + * 获取字符串中的数字 + * @param str + * @return + */ + public static Integer matcherNum(String str){ + Pattern pattern = Pattern.compile("\\d+"); + Matcher matcher = pattern.matcher(str); + while (matcher.find()){ + return Integer.parseInt(matcher.group()); + } + return 0; + } + + /** + * 获取字符串中的变量 + * @param variable 变量标识符合 + * @param: str 内容 + * @return java.util.List + */ + public static List getVariables(String variable, String str) { + List variables = new ArrayList<>(); + Pattern pattern = null; + switch (variable) { + case "${}": + pattern = Pattern.compile("\\$\\{([^}]+)}"); + break; + case "{{}}": + pattern = Pattern.compile("\\{\\{([^}]+)}}"); + break; + case "{}": + pattern = Pattern.compile("\\{([^}]+)}"); + break; + case "#{}": + pattern = Pattern.compile("#\\{([^}]+)}"); + break; + default: + break; + } + assert pattern != null; + Matcher matcher = pattern.matcher(str); + while (matcher.find()) { + variables.add(matcher.group(1)); + } + return variables; + } + + /** + * 获取微信小程序变量 + * @param content 内容 + * @return java.util.List + */ + public static List getWeChatMiniVariables(String content) { + List variables = new ArrayList<>(); + Pattern pattern = Pattern.compile("\\{\\{([^}]+)}}"); + Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + variables.add(matcher.group(1).replace(".DATA", "")); + } + return variables; + } + + /** + * 将字符串text中由openToken和closeToken组成的占位符依次替换为args数组中的值 + * @param openToken + * @param closeToken + * @param text + * @param args + * @return + */ + public static String parse(String openToken, String closeToken, String text, Object... args) { + if (args == null || args.length <= 0) { + return text; + } + int argsIndex = 0; + + if (text == null || text.isEmpty()) { + return ""; + } + char[] src = text.toCharArray(); + int offset = 0; + // search open token + int start = text.indexOf(openToken, offset); + if (start == -1) { + return text; + } + final StringBuilder builder = new StringBuilder(); + StringBuilder expression = null; + while (start > -1) { + if (start > 0 && src[start - 1] == '\\') { + // this open token is escaped. remove the backslash and continue. + builder.append(src, offset, start - offset - 1).append(openToken); + offset = start + openToken.length(); + } else { + // found open token. let's search close token. + if (expression == null) { + expression = new StringBuilder(); + } else { + expression.setLength(0); + } + builder.append(src, offset, start - offset); + offset = start + openToken.length(); + int end = text.indexOf(closeToken, offset); + while (end > -1) { + if (end > offset && src[end - 1] == '\\') { + // this close token is escaped. remove the backslash and continue. + expression.append(src, offset, end - offset - 1).append(closeToken); + offset = end + closeToken.length(); + end = text.indexOf(closeToken, offset); + } else { + expression.append(src, offset, end - offset); + offset = end + closeToken.length(); + break; + } + } + if (end == -1) { + // close token was not found. + builder.append(src, start, src.length - start); + offset = src.length; + } else { + ///////////////////////////////////////仅仅修改了该else分支下的个别行代码//////////////////////// + + String value = (argsIndex <= args.length - 1) ? + (args[argsIndex] == null ? "" : args[argsIndex].toString()) : expression.toString(); + builder.append(value); + offset = end + closeToken.length(); + argsIndex++; + //////////////////////////////////////////////////////////////////////////////////////////////// + } + } + start = text.indexOf(openToken, offset); + } + if (offset < src.length) { + builder.append(src, offset, src.length - offset); + } + return builder.toString(); + } + + /** + * @description: 替换 ${variable} + * @author fastb + * @date 2023-12-26 15:35 + * @version 1.0 + */ + public static String parseVariable(String text, Object... args) { + return parse("${", "}", text, args); + } + + public static String strReplaceVariable(String openIndex, String closeIndex, String content, LinkedHashMap map) { + if (StringUtils.isEmpty(content) || MapUtils.isEmpty(map)) { + return content; + } + StringBuilder sendContent = new StringBuilder(content); + for (Map.Entry m : map.entrySet()) { + sendContent = new StringBuilder(sendContent.toString().replace(openIndex + m.getKey() + closeIndex, m.getValue())); + } + return sendContent.toString(); + } + + /** + * 获取字符串中的ip + * @param str 待提取字符串 + * @return ip地址 + */ + public static String pickIp(String str) { + String regular = "\\d{3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"; + Pattern pattern = Pattern.compile(regular); + Matcher matcher = pattern.matcher(str); + if (matcher.find()){ + return matcher.group(); + } + return "不匹配"; + } + + + public static String ignoreHttp(String urlString) { + + try { + // 创建URL对象 + URL url = new URL(urlString); + + // 仅获取路径、查询和片段部分 + String modifiedUrl = url.getPath(); + + if (url.getQuery() != null) { + modifiedUrl += "?" + url.getQuery(); + } + + if (url.getRef() != null) { + modifiedUrl += "#" + url.getRef(); + } + + + return modifiedUrl; + + } catch (MalformedURLException e) { + // 处理错误URL格式的异常 + + e.printStackTrace(); + } + return null; + + } + + public static String subByLength(String str, int limit) { + if (isEmpty(str)) { + return str; + } + int length = str.length(); + if (length > limit) { + return str.substring(limit); + } + return str; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/TableUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/TableUtils.java new file mode 100644 index 0000000..d6e0249 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/TableUtils.java @@ -0,0 +1,23 @@ +package com.bnhz.common.utils; + + +/** + * @author Leo + * @date 2024/7/4 22:06 + */ + +public class TableUtils { + public static String tableColumnName(String originalName) { + if (StringUtils.isEmpty(originalName)) { + return originalName; + } + return originalName.toLowerCase().replace("-", "_"); + } + + public static String tableName(String originalName) { + if (StringUtils.isEmpty(originalName)) { + return originalName; + } + return originalName.toUpperCase().replace("-", "_"); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/Threads.java b/bnhz-common/src/main/java/com/bnhz/common/utils/Threads.java new file mode 100644 index 0000000..ee3e7ae --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/Threads.java @@ -0,0 +1,99 @@ +package com.bnhz.common.utils; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 线程相关工具类. + * + * @author ruoyi + */ +public class Threads +{ + private static final Logger logger = LoggerFactory.getLogger(Threads.class); + + /** + * sleep等待,单位为毫秒 + */ + public static void sleep(long milliseconds) + { + try + { + Thread.sleep(milliseconds); + } + catch (InterruptedException e) + { + return; + } + } + + /** + * 停止线程池 + * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务. + * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数. + * 如果仍然超時,則強制退出. + * 另对在shutdown时线程本身被调用中断做了处理. + */ + public static void shutdownAndAwaitTermination(ExecutorService pool) + { + if (pool != null && !pool.isShutdown()) + { + pool.shutdown(); + try + { + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + pool.shutdownNow(); + if (!pool.awaitTermination(120, TimeUnit.SECONDS)) + { + logger.info("Pool did not terminate"); + } + } + } + catch (InterruptedException ie) + { + pool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + /** + * 打印线程异常信息 + */ + public static void printException(Runnable r, Throwable t) + { + if (t == null && r instanceof Future) + { + try + { + Future future = (Future) r; + if (future.isDone()) + { + future.get(); + } + } + catch (CancellationException ce) + { + t = ce; + } + catch (ExecutionException ee) + { + t = ee.getCause(); + } + catch (InterruptedException ie) + { + Thread.currentThread().interrupt(); + } + } + if (t != null) + { + logger.error(t.getMessage(), t); + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/ValidationUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/ValidationUtils.java new file mode 100644 index 0000000..cd6d85e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/ValidationUtils.java @@ -0,0 +1,62 @@ +package com.bnhz.common.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import org.springframework.util.StringUtils; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * 校验工具类 + * + * @author bnhz + */ +public class ValidationUtils { + + private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$"); + + private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); + + private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*"); + + private static final Pattern PATTERN_EMAIL = Pattern.compile("^(\\w+([-.][A-Za-z0-9]+)*){3,18}@\\w+([-.][A-Za-z0-9]+)*\\.\\w+([-.][A-Za-z0-9]+)*$"); + + public static boolean isMobile(String mobile) { + return StringUtils.hasText(mobile) + && PATTERN_MOBILE.matcher(mobile).matches(); + } + + public static boolean isURL(String url) { + return StringUtils.hasText(url) + && PATTERN_URL.matcher(url).matches(); + } + + public static boolean isXmlNCName(String str) { + return StringUtils.hasText(str) + && PATTERN_XML_NCNAME.matcher(str).matches(); + } + + public static boolean isEmail(String email) { + return StringUtils.hasText(email) + && PATTERN_EMAIL.matcher(email).matches(); + } + + public static void validate(Object object, Class... groups) { + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + Assert.notNull(validator); + validate(validator, object, groups); + } + + public static void validate(Validator validator, Object object, Class... groups) { + Set> constraintViolations = validator.validate(object, groups); + if (CollUtil.isNotEmpty(constraintViolations)) { + throw new ConstraintViolationException(constraintViolations); + } + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/VerifyCodeUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/VerifyCodeUtils.java new file mode 100644 index 0000000..cbb6def --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/VerifyCodeUtils.java @@ -0,0 +1,228 @@ +package com.bnhz.common.utils; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Random; +import javax.imageio.ImageIO; + +/** + * 验证码工具类 + * + * @author ruoyi + */ +public class VerifyCodeUtils +{ + // 使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符 + public static final String VERIFY_CODES = "123456789ABCDEFGHJKLMNPQRSTUVWXYZ"; + + private static Random random = new SecureRandom(); + + /** + * 使用系统默认字符源生成验证码 + * + * @param verifySize 验证码长度 + * @return + */ + public static String generateVerifyCode(int verifySize) + { + return generateVerifyCode(verifySize, VERIFY_CODES); + } + + /** + * 使用指定源生成验证码 + * + * @param verifySize 验证码长度 + * @param sources 验证码字符源 + * @return + */ + public static String generateVerifyCode(int verifySize, String sources) + { + if (sources == null || sources.length() == 0) + { + sources = VERIFY_CODES; + } + int codesLen = sources.length(); + Random rand = new Random(System.currentTimeMillis()); + StringBuilder verifyCode = new StringBuilder(verifySize); + for (int i = 0; i < verifySize; i++) + { + verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1))); + } + return verifyCode.toString(); + } + + /** + * 输出指定验证码图片流 + * + * @param w + * @param h + * @param os + * @param code + * @throws IOException + */ + public static void outputImage(int w, int h, OutputStream os, String code) throws IOException + { + int verifySize = code.length(); + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + Random rand = new Random(); + Graphics2D g2 = image.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + Color[] colors = new Color[5]; + Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN, Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, + Color.ORANGE, Color.PINK, Color.YELLOW }; + float[] fractions = new float[colors.length]; + for (int i = 0; i < colors.length; i++) + { + colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)]; + fractions[i] = rand.nextFloat(); + } + Arrays.sort(fractions); + + g2.setColor(Color.GRAY);// 设置边框色 + g2.fillRect(0, 0, w, h); + + Color c = getRandColor(200, 250); + g2.setColor(c);// 设置背景色 + g2.fillRect(0, 2, w, h - 4); + + // 绘制干扰线 + Random random = new Random(); + g2.setColor(getRandColor(160, 200));// 设置线条的颜色 + for (int i = 0; i < 20; i++) + { + int x = random.nextInt(w - 1); + int y = random.nextInt(h - 1); + int xl = random.nextInt(6) + 1; + int yl = random.nextInt(12) + 1; + g2.drawLine(x, y, x + xl + 40, y + yl + 20); + } + + // 添加噪点 + float yawpRate = 0.05f;// 噪声率 + int area = (int) (yawpRate * w * h); + for (int i = 0; i < area; i++) + { + int x = random.nextInt(w); + int y = random.nextInt(h); + int rgb = getRandomIntColor(); + image.setRGB(x, y, rgb); + } + + shear(g2, w, h, c);// 使图片扭曲 + + g2.setColor(getRandColor(100, 160)); + int fontSize = h - 4; + Font font = new Font("Algerian", Font.ITALIC, fontSize); + g2.setFont(font); + char[] chars = code.toCharArray(); + for (int i = 0; i < verifySize; i++) + { + AffineTransform affine = new AffineTransform(); + affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), + (w / verifySize) * i + fontSize / 2, h / 2); + g2.setTransform(affine); + g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10); + } + + g2.dispose(); + ImageIO.write(image, "jpg", os); + } + + private static Color getRandColor(int fc, int bc) + { + if (fc > 255) { + fc = 255; + } + if (bc > 255) { + bc = 255; + } + int r = fc + random.nextInt(bc - fc); + int g = fc + random.nextInt(bc - fc); + int b = fc + random.nextInt(bc - fc); + return new Color(r, g, b); + } + + private static int getRandomIntColor() + { + int[] rgb = getRandomRgb(); + int color = 0; + for (int c : rgb) + { + color = color << 8; + color = color | c; + } + return color; + } + + private static int[] getRandomRgb() + { + int[] rgb = new int[3]; + for (int i = 0; i < 3; i++) + { + rgb[i] = random.nextInt(255); + } + return rgb; + } + + private static void shear(Graphics g, int w1, int h1, Color color) + { + shearX(g, w1, h1, color); + shearY(g, w1, h1, color); + } + + private static void shearX(Graphics g, int w1, int h1, Color color) + { + + int period = random.nextInt(2); + + boolean borderGap = true; + int frames = 1; + int phase = random.nextInt(2); + + for (int i = 0; i < h1; i++) + { + double d = (double) (period >> 1) + * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); + g.copyArea(0, i, w1, 1, (int) d, 0); + if (borderGap) + { + g.setColor(color); + g.drawLine((int) d, i, 0, i); + g.drawLine((int) d + w1, i, w1, i); + } + } + + } + + private static void shearY(Graphics g, int w1, int h1, Color color) + { + + int period = random.nextInt(40) + 10; // 50; + + boolean borderGap = true; + int frames = 20; + int phase = 7; + for (int i = 0; i < w1; i++) + { + double d = (double) (period >> 1) + * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames); + g.copyArea(i, 0, 1, h1, 0, (int) d); + if (borderGap) + { + g.setColor(color); + g.drawLine(i, (int) d, i, 0); + g.drawLine(i, (int) d + h1, i, h1); + } + + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/bean/BeanUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/bean/BeanUtils.java new file mode 100644 index 0000000..d5600c8 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/bean/BeanUtils.java @@ -0,0 +1,110 @@ +package com.bnhz.common.utils.bean; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Bean 工具类 + * + * @author ruoyi + */ +public class BeanUtils extends org.springframework.beans.BeanUtils +{ + /** Bean方法名中属性名开始的下标 */ + private static final int BEAN_METHOD_PROP_INDEX = 3; + + /** * 匹配getter方法的正则表达式 */ + private static final Pattern GET_PATTERN = Pattern.compile("get(\\p{javaUpperCase}\\w*)"); + + /** * 匹配setter方法的正则表达式 */ + private static final Pattern SET_PATTERN = Pattern.compile("set(\\p{javaUpperCase}\\w*)"); + + /** + * Bean属性复制工具方法。 + * + * @param dest 目标对象 + * @param src 源对象 + */ + public static void copyBeanProp(Object dest, Object src) + { + try + { + copyProperties(src, dest); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + /** + * 获取对象的setter方法。 + * + * @param obj 对象 + * @return 对象的setter方法列表 + */ + public static List getSetterMethods(Object obj) + { + // setter方法列表 + List setterMethods = new ArrayList(); + + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + + // 查找setter方法 + + for (Method method : methods) + { + Matcher m = SET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 1)) + { + setterMethods.add(method); + } + } + // 返回setter方法列表 + return setterMethods; + } + + /** + * 获取对象的getter方法。 + * + * @param obj 对象 + * @return 对象的getter方法列表 + */ + + public static List getGetterMethods(Object obj) + { + // getter方法列表 + List getterMethods = new ArrayList(); + // 获取所有方法 + Method[] methods = obj.getClass().getMethods(); + // 查找getter方法 + for (Method method : methods) + { + Matcher m = GET_PATTERN.matcher(method.getName()); + if (m.matches() && (method.getParameterTypes().length == 0)) + { + getterMethods.add(method); + } + } + // 返回getter方法列表 + return getterMethods; + } + + /** + * 检查Bean方法名中的属性名是否相等。
+ * 如getName()和setName()属性名一样,getName()和setAge()属性名不一样。 + * + * @param m1 方法名1 + * @param m2 方法名2 + * @return 属性名一样返回true,否则返回false + */ + + public static boolean isMethodPropEquals(String m1, String m2) + { + return m1.substring(BEAN_METHOD_PROP_INDEX).equals(m2.substring(BEAN_METHOD_PROP_INDEX)); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/bean/BeanValidators.java b/bnhz-common/src/main/java/com/bnhz/common/utils/bean/BeanValidators.java new file mode 100644 index 0000000..3c3369b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/bean/BeanValidators.java @@ -0,0 +1,24 @@ +package com.bnhz.common.utils.bean; + +import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validator; + +/** + * bean对象属性验证 + * + * @author ruoyi + */ +public class BeanValidators +{ + public static void validateWithException(Validator validator, Object object, Class... groups) + throws ConstraintViolationException + { + Set> constraintViolations = validator.validate(object, groups); + if (!constraintViolations.isEmpty()) + { + throw new ConstraintViolationException(constraintViolations); + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/collection/CollectionUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/collection/CollectionUtils.java new file mode 100644 index 0000000..66dd74b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/collection/CollectionUtils.java @@ -0,0 +1,268 @@ +package com.bnhz.common.utils.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; + +import java.util.*; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * @author gsb + * @date 2022/9/15 16:52 + */ +public class CollectionUtils { + + /*数组复制*/ + public static String[] copy(String[] source){ + if(isEmpty(source)){ + return null; + } + int len = source.length; + String[] arr = new String[len]; + for(int i=0; i < len; i ++){ + arr[i] = source[i]; + } + return arr; + } + + + /*数组连接*/ + public static String concat(String[] source, String split){ + if(isEmpty(source)){ + return null; + } + String result = ""; + for(int i=0; i < source.length; i ++){ + result = result.concat(source[i]); + if(i != source.length - 1){ + result = result.concat(split); + } + } + return result; + } + + public static boolean isEmpty(String[] source){ + if(null == source){ + return true; + } + if(0 == source.length){ + return true; + } + return false; + } + + public static boolean containsAny(Object source, Object... targets) { + return Arrays.asList(targets).contains(source); + } + + public static boolean isAnyEmpty(Collection... collections) { + return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); + } + + public static List filterList(Collection from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(predicate).collect(Collectors.toList()); + } + + public static List distinct(Collection from, Function keyMapper) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return distinct(from, keyMapper, (t1, t2) -> t1); + } + + public static List distinct(Collection from, Function keyMapper, BinaryOperator cover) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); + } + + public static List convertList(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List convertList(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static Set convertSet(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Set convertSet(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Map convertMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, Function.identity()); + } + + public static Map convertMap(Collection from, Function keyFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, Function.identity(), supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream() + .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); + } + + // 暂时没想好名字,先以 2 结尾噶 + public static Map> convertMultiMap2(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); + } + + + public static boolean containsAny(Collection source, Collection candidates) { + return org.springframework.util.CollectionUtils.containsAny(source, candidates); + } + + public static T getFirst(List from) { + return !CollectionUtil.isEmpty(from) ? from.get(0) : null; + } + + public static T findFirst(List from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return null; + } + return from.stream().filter(predicate).findFirst().orElse(null); + } + + public static > V getMaxValue(List from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + T t = from.stream().max(Comparator.comparing(valueFunc)).get(); + return valueFunc.apply(t); + } + + public static > V getMinValue(List from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + T t = from.stream().min(Comparator.comparing(valueFunc)).get(); + return valueFunc.apply(t); + } + + public static > V getSumValue(List from, Function valueFunc, BinaryOperator accumulator) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + return from.stream().map(valueFunc).reduce(accumulator).get(); + } + + public static void addIfNotNull(Collection coll, T item) { + if (item == null) { + return; + } + coll.add(item); + } + + public static Collection singleton(T deptId) { + return deptId == null ? Collections.emptyList() : Collections.singleton(deptId); + } + + /** + * 开始分页 + * + * @param list 传入的list集合 + * @param pageNum 页码 + * @param pageSize 每页多少条数据 + * @return + */ + public static List startPage(List list, Integer pageNum, + Integer pageSize) { + if (list == null) { + return null; + } + if (list.size() == 0) { + return null; + } + Integer count = list.size(); // 记录总数 + Integer pageCount = 0; // 页数 + if (count % pageSize == 0) { + pageCount = count / pageSize; + } else { + pageCount = count / pageSize + 1; + } + int fromIndex = 0; // 开始索引 + int toIndex = 0; // 结束索引 + if (!pageNum.equals(pageCount)) { + fromIndex = (pageNum - 1) * pageSize; + toIndex = fromIndex + pageSize; + } else { + fromIndex = (pageNum - 1) * pageSize; + toIndex = count; + } + List pageList = list.subList(fromIndex, toIndex); + return pageList; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/date/DateUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/date/DateUtils.java new file mode 100644 index 0000000..5904a51 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/date/DateUtils.java @@ -0,0 +1,186 @@ +package com.bnhz.common.utils.date; + +import cn.hutool.core.date.LocalDateTimeUtil; +import org.apache.commons.lang3.ObjectUtils; + +import java.sql.Timestamp; +import java.time.*; +import java.util.Calendar; +import java.util.Date; + +/** + * 时间工具类 + * + * @author bnhz + */ +public class DateUtils { + + /** + * 时区 - 默认 + */ + public static final String TIME_ZONE_DEFAULT = "GMT+8"; + + /** + * 秒转换成毫秒 + */ + public static final long SECOND_MILLIS = 1000; + + public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; + + public static final String FORMAT_HOUR_MINUTE_SECOND = "HH:mm:ss"; + + /** + * 将 LocalDateTime 转换成 Date + * + * @param date LocalDateTime + * @return LocalDateTime + */ + public static Date of(LocalDateTime date) { + // 将此日期时间与时区相结合以创建 ZonedDateTime + ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault()); + // 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳 + Instant instant = zonedDateTime.toInstant(); + // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 + return Date.from(instant); + } + + /** + * 将 Date 转换成 LocalDateTime + * + * @param date Date + * @return LocalDateTime + */ + public static LocalDateTime of(Date date) { + // 转为时间戳 + Instant instant = date.toInstant(); + // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + } + + @Deprecated + public static Date addTime(Duration duration) { + return new Date(System.currentTimeMillis() + duration.toMillis()); + } + + public static boolean isExpired(Date time) { + return System.currentTimeMillis() > time.getTime(); + } + + public static boolean isExpired(LocalDateTime time) { + LocalDateTime now = LocalDateTime.now(); + return now.isAfter(time); + } + + public static long diff(Date endTime, Date startTime) { + return endTime.getTime() - startTime.getTime(); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param mouth 月 + * @param day 日 + * @return 指定时间 + */ + public static Date buildTime(int year, int mouth, int day) { + return buildTime(year, mouth, day, 0, 0, 0); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param mouth 月 + * @param day 日 + * @param hour 小时 + * @param minute 分钟 + * @param second 秒 + * @return 指定时间 + */ + public static Date buildTime(int year, int mouth, int day, + int hour, int minute, int second) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, mouth - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, second); + calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒 + return calendar.getTime(); + } + + public static Date max(Date a, Date b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.compareTo(b) > 0 ? a : b; + } + + public static LocalDateTime max(LocalDateTime a, LocalDateTime b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.isAfter(b) ? a : b; + } + + /** + * 计算当期时间相差的日期 + * + * @param field 日历字段.
eg:Calendar.MONTH,Calendar.DAY_OF_MONTH,
Calendar.HOUR_OF_DAY等. + * @param amount 相差的数值 + * @return 计算后的日志 + */ + public static Date addDate(int field, int amount) { + return addDate(null, field, amount); + } + + /** + * 计算当期时间相差的日期 + * + * @param date 设置时间 + * @param field 日历字段 例如说,{@link Calendar#DAY_OF_MONTH} 等 + * @param amount 相差的数值 + * @return 计算后的日志 + */ + public static Date addDate(Date date, int field, int amount) { + if (amount == 0) { + return date; + } + Calendar c = Calendar.getInstance(); + if (date != null) { + c.setTime(date); + } + c.add(field, amount); + return c.getTime(); + } + + /** + * 是否今天 + * + * @param date 日期 + * @return 是否 + */ + public static boolean isToday(LocalDateTime date) { + return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now()); + } + + public static Timestamp toTimestamp(LocalDateTime dateTime) { + if (ObjectUtils.isEmpty(dateTime)) { + return null; + } + return Timestamp.valueOf(dateTime); + } + + public static Date now() { + return new Date(); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/date/LocalDateTimeUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/date/LocalDateTimeUtils.java new file mode 100644 index 0000000..eda9157 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/date/LocalDateTimeUtils.java @@ -0,0 +1,63 @@ +package com.bnhz.common.utils.date; + +import cn.hutool.core.date.LocalDateTimeUtil; + +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * 时间工具类,用于 {@link LocalDateTime} + * + * @author bnhz + */ +public class LocalDateTimeUtils { + + /** + * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值 + */ + public static LocalDateTime EMPTY = buildTime(1970, 1, 1); + + public static LocalDateTime addTime(Duration duration) { + return LocalDateTime.now().plus(duration); + } + + public static boolean beforeNow(LocalDateTime date) { + return date.isBefore(LocalDateTime.now()); + } + + public static boolean afterNow(LocalDateTime date) { + return date.isAfter(LocalDateTime.now()); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param mouth 月 + * @param day 日 + * @return 指定时间 + */ + public static LocalDateTime buildTime(int year, int mouth, int day) { + return LocalDateTime.of(year, mouth, day, 0, 0, 0); + } + + public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1, + int year2, int mouth2, int day2) { + return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)}; + } + + /** + * 判断当前时间是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 是否 + */ + public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) { + if (startTime == null || endTime == null) { + return false; + } + return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/file/FileTypeUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/file/FileTypeUtils.java new file mode 100644 index 0000000..cb68d8b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/file/FileTypeUtils.java @@ -0,0 +1,76 @@ +package com.bnhz.common.utils.file; + +import java.io.File; +import org.apache.commons.lang3.StringUtils; + +/** + * 文件类型工具类 + * + * @author ruoyi + */ +public class FileTypeUtils +{ + /** + * 获取文件类型 + *

+ * 例如: bnhz.txt, 返回: txt + * + * @param file 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(File file) + { + if (null == file) + { + return StringUtils.EMPTY; + } + return getFileType(file.getName()); + } + + /** + * 获取文件类型 + *

+ * 例如: bnhz.txt, 返回: txt + * + * @param fileName 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(String fileName) + { + int separatorIndex = fileName.lastIndexOf("."); + if (separatorIndex < 0) + { + return ""; + } + return fileName.substring(separatorIndex + 1).toLowerCase(); + } + + /** + * 获取文件类型 + * + * @param photoByte 文件字节码 + * @return 后缀(不含".") + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "JPG"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "GIF"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "JPG"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "BMP"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "PNG"; + } + return strFileExtendName; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/file/FileUploadUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/file/FileUploadUtils.java new file mode 100644 index 0000000..dc06536 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/file/FileUploadUtils.java @@ -0,0 +1,232 @@ +package com.bnhz.common.utils.file; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Objects; +import org.apache.commons.io.FilenameUtils; +import org.springframework.web.multipart.MultipartFile; +import com.bnhz.common.config.DaQiConfig; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.exception.file.FileNameLengthLimitExceededException; +import com.bnhz.common.exception.file.FileSizeLimitExceededException; +import com.bnhz.common.exception.file.InvalidExtensionException; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.uuid.Seq; + +/** + * 文件上传工具类 + * + * @author ruoyi + */ +public class FileUploadUtils +{ + /** + * 默认大小 50M + */ + public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024; + + /** + * 默认的文件名最大长度 100 + */ + public static final int DEFAULT_FILE_NAME_LENGTH = 100; + + /** + * 默认上传的地址 + */ + private static String defaultBaseDir = DaQiConfig.getProfile(); + + public static void setDefaultBaseDir(String defaultBaseDir) + { + FileUploadUtils.defaultBaseDir = defaultBaseDir; + } + + public static String getDefaultBaseDir() + { + return defaultBaseDir; + } + + /** + * 以默认配置进行文件上传 + * + * @param file 上传的文件 + * @return 文件名称 + * @throws Exception + */ + public static final String upload(MultipartFile file) throws IOException + { + try + { + return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 根据文件路径上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @return 文件名称 + * @throws IOException + */ + public static final String upload(String baseDir, MultipartFile file) throws IOException + { + try + { + return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 文件上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @param allowedExtension 上传文件类型 + * @return 返回上传成功的文件名 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws FileNameLengthLimitExceededException 文件名太长 + * @throws IOException 比如读写文件出错时 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, + InvalidExtensionException + { + int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length(); + if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) + { + throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH); + } + + assertAllowed(file, allowedExtension); + + String fileName = extractFilename(file); + + String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath(); + file.transferTo(Paths.get(absPath)); + return getPathFileName(baseDir, fileName); + } + + /** + * 编码文件名 + */ + public static final String extractFilename(MultipartFile file) + { + return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(), + FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file)); + } + + public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException + { + File desc = new File(uploadDir + File.separator + fileName); + + if (!desc.exists()) + { + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + } + return desc; + } + + public static final String getPathFileName(String uploadDir, String fileName) throws IOException + { + int dirLastIndex = DaQiConfig.getProfile().length() + 1; + String currentDir = StringUtils.substring(uploadDir, dirLastIndex); + return Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName; + } + + /** + * 文件大小校验 + * + * @param file 上传的文件 + * @return + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws InvalidExtensionException + */ + public static final void assertAllowed(MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, InvalidExtensionException + { + long size = file.getSize(); + if (size > DEFAULT_MAX_SIZE) + { + throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024); + } + + String fileName = file.getOriginalFilename(); + String extension = getExtension(file); + if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) + { + if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) + { + throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) + { + throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) + { + throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION) + { + throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension, + fileName); + } + else + { + throw new InvalidExtensionException(allowedExtension, extension, fileName); + } + } + } + + /** + * 判断MIME类型是否是允许的MIME类型 + * + * @param extension + * @param allowedExtension + * @return + */ + public static final boolean isAllowedExtension(String extension, String[] allowedExtension) + { + for (String str : allowedExtension) + { + if (str.equalsIgnoreCase(extension)) + { + return true; + } + } + return false; + } + + /** + * 获取文件名的后缀 + * + * @param file 表单文件 + * @return 后缀名 + */ + public static final String getExtension(MultipartFile file) + { + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + if (StringUtils.isEmpty(extension)) + { + extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType())); + } + return extension; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/file/FileUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/file/FileUtils.java new file mode 100644 index 0000000..5a95199 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/file/FileUtils.java @@ -0,0 +1,340 @@ +package com.bnhz.common.utils.file; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import lombok.SneakyThrows; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ArrayUtils; +import com.bnhz.common.config.DaQiConfig; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.uuid.IdUtils; +import org.apache.commons.io.FilenameUtils; + +/** + * 文件处理工具类 + * + * @author ruoyi + */ +public class FileUtils +{ + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 输出指定文件的byte数组 + * + * @param filePath 文件路径 + * @param os 输出流 + * @return + */ + public static void writeBytes(String filePath, OutputStream os) throws IOException + { + FileInputStream fis = null; + try + { + File file = new File(filePath); + if (!file.exists()) + { + throw new FileNotFoundException(filePath); + } + fis = new FileInputStream(file); + byte[] b = new byte[1024]; + int length; + while ((length = fis.read(b)) > 0) + { + os.write(b, 0, length); + } + } + catch (IOException e) + { + throw e; + } + finally + { + IOUtils.close(os); + IOUtils.close(fis); + } + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeImportBytes(byte[] data) throws IOException + { + return writeBytes(data, DaQiConfig.getImportPath()); + } + + /** + * 写数据到文件中 + * + * @param data 数据 + * @param uploadDir 目标文件 + * @return 目标文件 + * @throws IOException IO异常 + */ + public static String writeBytes(byte[] data, String uploadDir) throws IOException + { + FileOutputStream fos = null; + String pathName = ""; + try + { + String extension = getFileExtendName(data); + pathName = DateUtils.datePath() + "/" + IdUtils.fastUUID() + "." + extension; + File file = FileUploadUtils.getAbsoluteFile(uploadDir, pathName); + fos = new FileOutputStream(file); + fos.write(data); + } + finally + { + IOUtils.close(fos); + } + return FileUploadUtils.getPathFileName(uploadDir, pathName); + } + + /** + * 删除文件 + * + * @param filePath 文件 + * @return + */ + public static boolean deleteFile(String filePath) + { + boolean flag = false; + File file = new File(filePath); + // 路径为文件且不为空则进行删除 + if (file.isFile() && file.exists()) + { + flag = file.delete(); + } + return flag; + } + + /** + * 文件名称验证 + * + * @param filename 文件名称 + * @return true 正常 false 非法 + */ + public static boolean isValidFilename(String filename) + { + return filename.matches(FILENAME_PATTERN); + } + + /** + * 检查文件是否可下载 + * + * @param resource 需要下载的文件 + * @return true 正常 false 非法 + */ + public static boolean checkAllowDownload(String resource) + { + // 禁止目录上跳级别 + if (StringUtils.contains(resource, "..")) + { + return false; + } + + // 检查允许下载的文件规则 + if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) + { + return true; + } + + // 不在允许下载的文件规则 + return false; + } + + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException + { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) + { + // IE浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + filename = filename.replace("+", " "); + } + else if (agent.contains("Firefox")) + { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } + else if (agent.contains("Chrome")) + { + // google浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + else + { + // 其它浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + return filename; + } + + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException + { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue.append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename"); + response.setHeader("Content-disposition", contentDispositionValue.toString()); + response.setHeader("download-filename", percentEncodedFileName); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException + { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } + + /** + * 获取图像后缀 + * + * @param photoByte 图像数据 + * @return 后缀名 + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "jpg"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "gif"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "jpg"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "bmp"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "png"; + } + return strFileExtendName; + } + + /** + * 获取文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi.png + * + * @param fileName 路径名称 + * @return 没有文件路径的名称 + */ + public static String getName(String fileName) + { + if (fileName == null) + { + return null; + } + int lastUnixPos = fileName.lastIndexOf('/'); + int lastWindowsPos = fileName.lastIndexOf('\\'); + int index = Math.max(lastUnixPos, lastWindowsPos); + return fileName.substring(index + 1); + } + + /** + * 获取不带后缀文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi + * + * @param fileName 路径名称 + * @return 没有文件路径和后缀的名称 + */ + public static String getNameNotSuffix(String fileName) + { + if (fileName == null) + { + return null; + } + String baseName = FilenameUtils.getBaseName(fileName); + return baseName; + } + + /** + * 创建临时文件 + * 该文件会在 JVM 退出时,进行删除 + * + * @param data 文件内容 + * @return 文件 + */ + @SneakyThrows + public static File createTempFile(String data) { + File file = createTempFile(); + // 写入内容 + FileUtil.writeUtf8String(data, file); + return file; + } + + /** + * 创建临时文件 + * 该文件会在 JVM 退出时,进行删除 + * + * @param data 文件内容 + * @return 文件 + */ + @SneakyThrows + public static File createTempFile(byte[] data) { + File file = createTempFile(); + // 写入内容 + FileUtil.writeBytes(data, file); + return file; + } + + /** + * 创建临时文件,无内容 + * 该文件会在 JVM 退出时,进行删除 + * + * @return 文件 + */ + @SneakyThrows + public static File createTempFile() { + // 创建文件,通过 UUID 保证唯一 + File file = File.createTempFile(IdUtil.simpleUUID(), null); + // 标记 JVM 退出时,自动删除 + file.deleteOnExit(); + return file; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/file/ImageUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/file/ImageUtils.java new file mode 100644 index 0000000..fa680ec --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/file/ImageUtils.java @@ -0,0 +1,98 @@ +package com.bnhz.common.utils.file; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import org.apache.poi.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.bnhz.common.config.DaQiConfig; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.utils.StringUtils; + +/** + * 图片处理工具类 + * + * @author ruoyi + */ +public class ImageUtils +{ + private static final Logger log = LoggerFactory.getLogger(ImageUtils.class); + + public static byte[] getImage(String imagePath) + { + InputStream is = getFile(imagePath); + try + { + return IOUtils.toByteArray(is); + } + catch (Exception e) + { + log.error("图片加载异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(is); + } + } + + public static InputStream getFile(String imagePath) + { + try + { + byte[] result = readFile(imagePath); + result = Arrays.copyOf(result, result.length); + return new ByteArrayInputStream(result); + } + catch (Exception e) + { + log.error("获取图片异常 {}", e); + } + return null; + } + + /** + * 读取文件为字节数据 + * + * @param url 地址 + * @return 字节数据 + */ + public static byte[] readFile(String url) + { + InputStream in = null; + try + { + if (url.startsWith("http")) + { + // 网络地址 + URL urlObj = new URL(url); + URLConnection urlConnection = urlObj.openConnection(); + urlConnection.setConnectTimeout(30 * 1000); + urlConnection.setReadTimeout(60 * 1000); + urlConnection.setDoInput(true); + in = urlConnection.getInputStream(); + } + else + { + // 本机地址 + String localPath = DaQiConfig.getProfile(); + String downloadPath = localPath + StringUtils.substringAfter(url, Constants.RESOURCE_PREFIX); + in = new FileInputStream(downloadPath); + } + return IOUtils.toByteArray(in); + } + catch (Exception e) + { + log.error("获取文件路径异常 {}", e); + return null; + } + finally + { + IOUtils.closeQuietly(in); + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/file/MimeTypeUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/file/MimeTypeUtils.java new file mode 100644 index 0000000..b4c3029 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/file/MimeTypeUtils.java @@ -0,0 +1,59 @@ +package com.bnhz.common.utils.file; + +/** + * 媒体类型工具类 + * + * @author ruoyi + */ +public class MimeTypeUtils +{ + public static final String IMAGE_PNG = "image/png"; + + public static final String IMAGE_JPG = "image/jpg"; + + public static final String IMAGE_JPEG = "image/jpeg"; + + public static final String IMAGE_BMP = "image/bmp"; + + public static final String IMAGE_GIF = "image/gif"; + + public static final String[] IMAGE_EXTENSION = { "bmp", "gif", "jpg", "jpeg", "png" }; + + public static final String[] FLASH_EXTENSION = { "swf", "flv" }; + + public static final String[] MEDIA_EXTENSION = { "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", + "asf", "rm", "rmvb" }; + + public static final String[] VIDEO_EXTENSION = { "mp4", "avi", "rmvb" }; + + public static final String[] DEFAULT_ALLOWED_EXTENSION = { + // 图片 + "bmp", "gif", "jpg", "jpeg", "png", + // word excel powerpoint + "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", + // 压缩文件 + "rar", "zip", "gz", "bz2", + // 视频格式 + "mp4", "avi", "rmvb", + // pdf + "pdf" }; + + public static String getExtension(String prefix) + { + switch (prefix) + { + case IMAGE_PNG: + return "png"; + case IMAGE_JPG: + return "jpg"; + case IMAGE_JPEG: + return "jpeg"; + case IMAGE_BMP: + return "bmp"; + case IMAGE_GIF: + return "gif"; + default: + return ""; + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/CRC16Utils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/CRC16Utils.java new file mode 100644 index 0000000..9789ede --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/CRC16Utils.java @@ -0,0 +1,139 @@ +package com.bnhz.common.utils.gateway; + + +import com.bnhz.common.utils.CaculateUtils; +import com.bnhz.common.utils.gateway.protocol.ByteUtils; + +public class CRC16Utils { + + //ff + private static int CRC_FF = 0x000000ff; + //01 + private static int CRC_01 = 0x00000001; + //04 + private static final int LENGTH_04 = 4; + //16进制 + private static final int OXFF = 0xff; + + /** + * 低位在前,高位在后 + * + * @param bytes + * @return + */ + public static String getCRC(byte[] bytes) { + return getCRC(bytes, true); + } + + /** + * @param bytes + * @param lb 是否低位在前, 高位在后 + * @return + */ + public static String getCRC(byte[] bytes, boolean lb) { + int CRC = 0x0000ffff; + int POLYNOMIAL = 0x0000a001; + + int i, j; + for (i = 0; i < bytes.length; i++) { + CRC ^= ((int) bytes[i] & 0x000000ff); + for (j = 0; j < 8; j++) { + if ((CRC & 0x00000001) != 0) { + CRC >>= 1; + CRC ^= POLYNOMIAL; + } else { + CRC >>= 1; + } + } + } + + //结果转换为16进制 + String result = Integer.toHexString(CRC).toUpperCase(); + if (result.length() != 4) { + StringBuffer sb = new StringBuffer("0000"); + result = sb.replace(4 - result.length(), 4, result).toString(); + } + + if (lb) { // 低位在前, 高位在后 + result = result.substring(2, 4) + result.substring(0, 2); + } + + return result; + } + + /** + * 计算CRC校验和 + * + * @param bytes + * @return 返回 byte[] + */ + public static byte[] getCrc16Byte(byte[] bytes) { + + //寄存器全为1 + int CRC_16 = 0x0000ffff; + // 多项式校验值 + int POLYNOMIAL = 0x0000a001; + for (byte aByte : bytes) { + CRC_16 ^= ((int) aByte & CRC_FF); + for (int j = 0; j < 8; j++) { + if ((CRC_16 & CRC_01) != 0) { + CRC_16 >>= 1; + CRC_16 ^= POLYNOMIAL; + } else { + CRC_16 >>= 1; + } + } + } + // 低8位 ,高8位 + return new byte[]{(byte) (CRC_16 & OXFF), (byte) (CRC_16 >> 8 & OXFF)}; + } + + public static byte[] AddCRC(byte[] source) { + byte[] result = new byte[source.length + 2]; + byte[] crc16Byte = CRC16Utils.getCrc16Byte(source); + System.arraycopy(source, 0, result, 0, source.length); + System.arraycopy(crc16Byte, 0, result, result.length - 2, 2); + return result; + } + + public static byte[] CRC(byte[] source) { + source[2] = (byte) ((int) source[2] * 2); + byte[] result = new byte[source.length + 2]; + byte[] crc16Byte = CRC16Utils.getCrc16Byte(source); + System.arraycopy(source, 0, result, 0, source.length); + System.arraycopy(crc16Byte, 0, result, result.length - 2, 2); + return result; + } + + public static byte CRC8(byte[] buffer) { + int crci = 0xFF; //起始字节FF + for (int j = 0; j < buffer.length; j++) { + crci ^= buffer[j] & 0xFF; + for (int i = 0; i < 8; i++) { + if ((crci & 1) != 0) { + crci >>= 1; + crci ^= 0xB8; //多项式当中的那个啥的,不同多项式不一样 + } else { + crci >>= 1; + } + } + } + return (byte) crci; + } + + + public static void main(String[] args)throws Exception { + String hex = "01000002"; + byte[] bytes = ByteUtils.hexToByte(hex); + String crc = getCRC(bytes); + System.out.println(crc); + String crc8 = "680868333701120008C100"; + byte[] byte8 = ByteUtils.hexToByte(crc8); + int b = CRC8(byte8); + System.out.println((int) b); + float v = CaculateUtils.toFloat32_ABCD(bytes); + System.out.println(v); + } + + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/CRC8Utils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/CRC8Utils.java new file mode 100644 index 0000000..c6c155b --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/CRC8Utils.java @@ -0,0 +1,93 @@ +package com.bnhz.common.utils.gateway; + +import com.bnhz.common.utils.gateway.protocol.ByteUtils; + +/** + * @author gsb + * @date 2023/5/19 11:33 + */ +public class CRC8Utils { + + + //TODO:-----------------根据C写法转译-------------------------------------- + /* CRC-8, poly = x^8 + x^2 + x^1 + x^0, init = 0 */ + + /** + * CRC8 校验 多项式 x8+x2+x+1 + * @param data + * @return 校验和 + */ + public static byte calcCrc8_E5(byte[] data){ + byte crc = 0; + for (int j = 0; j < data.length; j++) { + crc ^= data[j]; + for (int i = 0; i < 8; i++) { + if ((crc & 0x80) != 0) { + crc = (byte) ((crc)<< 1); + crc ^= 0xE5; + } else { + crc = (byte) ((crc)<< 1); + } + } + } + return crc; + } + + + + + + static byte[] crc8_tab = {(byte) 0, (byte) 94, (byte) 188, (byte) 226, (byte) 97, (byte) 63, (byte) 221, (byte) 131, (byte) 194, (byte) 156, (byte) 126, (byte) 32, (byte) 163, (byte) 253, (byte) 31, (byte) 65, (byte) 157, (byte) 195, (byte) 33, (byte) 127, (byte) 252, (byte) 162, (byte) 64, (byte) 30, (byte) 95, (byte) 1, (byte) 227, (byte) 189, (byte) 62, (byte) 96, (byte) 130, (byte) 220, (byte) 35, (byte) 125, (byte) 159, (byte) 193, (byte) 66, (byte) 28, (byte) 254, (byte) 160, (byte) 225, (byte) 191, (byte) 93, (byte) 3, (byte) 128, (byte) 222, (byte) 60, (byte) 98, (byte) 190, (byte) 224, (byte) 2, (byte) 92, (byte) 223, (byte) 129, (byte) 99, (byte) 61, (byte) 124, (byte) 34, (byte) 192, (byte) 158, (byte) 29, (byte) 67, (byte) 161, (byte) 255, (byte) 70, (byte) 24, + (byte) 250, (byte) 164, (byte) 39, (byte) 121, (byte) 155, (byte) 197, (byte) 132, (byte) 218, (byte) 56, (byte) 102, (byte) 229, (byte) 187, (byte) 89, (byte) 7, (byte) 219, (byte) 133, (byte) 103, (byte) 57, (byte) 186, (byte) 228, (byte) 6, (byte) 88, (byte) 25, (byte) 71, (byte) 165, (byte) 251, (byte) 120, (byte) 38, (byte) 196, (byte) 154, (byte) 101, (byte) 59, (byte) 217, (byte) 135, (byte) 4, (byte) 90, (byte) 184, (byte) 230, (byte) 167, (byte) 249, (byte) 27, (byte) 69, (byte) 198, (byte) 152, (byte) 122, (byte) 36, (byte) 248, (byte) 166, (byte) 68, (byte) 26, (byte) 153, (byte) 199, (byte) 37, (byte) 123, (byte) 58, (byte) 100, (byte) 134, (byte) 216, (byte) 91, (byte) 5, (byte) 231, (byte) 185, (byte) 140, (byte) 210, (byte) 48, (byte) 110, (byte) 237, + (byte) 179, (byte) 81, (byte) 15, (byte) 78, (byte) 16, (byte) 242, (byte) 172, (byte) 47, (byte) 113, (byte) 147, (byte) 205, (byte) 17, (byte) 79, (byte) 173, (byte) 243, (byte) 112, (byte) 46, (byte) 204, (byte) 146, (byte) 211, (byte) 141, (byte) 111, (byte) 49, (byte) 178, (byte) 236, (byte) 14, (byte) 80, (byte) 175, (byte) 241, (byte) 19, (byte) 77, (byte) 206, (byte) 144, (byte) 114, (byte) 44, (byte) 109, (byte) 51, (byte) 209, (byte) 143, (byte) 12, (byte) 82, (byte) 176, (byte) 238, (byte) 50, (byte) 108, (byte) 142, (byte) 208, (byte) 83, (byte) 13, (byte) 239, (byte) 177, (byte) 240, (byte) 174, (byte) 76, (byte) 18, (byte) 145, (byte) 207, (byte) 45, (byte) 115, (byte) 202, (byte) 148, (byte) 118, (byte) 40, (byte) 171, (byte) 245, (byte) 23, (byte) 73, (byte) 8, + (byte) 86, (byte) 180, (byte) 234, (byte) 105, (byte) 55, (byte) 213, (byte) 139, (byte) 87, (byte) 9, (byte) 235, (byte) 181, (byte) 54, (byte) 104, (byte) 138, (byte) 212, (byte) 149, (byte) 203, (byte) 41, (byte) 119, (byte) 244, (byte) 170, (byte) 72, (byte) 22, (byte) 233, (byte) 183, (byte) 85, (byte) 11, (byte) 136, (byte) 214, (byte) 52, (byte) 106, (byte) 43, (byte) 117, (byte) 151, (byte) 201, (byte) 74, (byte) 20, (byte) 246, (byte) 168, (byte) 116, (byte) 42, (byte) 200, (byte) 150, (byte) 21, (byte) 75, (byte) 169, (byte) 247, (byte) 182, (byte) 232, (byte) 10, (byte) 84, (byte) 215, (byte) 137, (byte) 107, 53}; + + /** + * 计算数组的CRC8校验值 + * + * @param data 需要计算的数组 + * @return CRC8校验值 + */ + public static byte calcCrc8(byte[] data) { + return calcCrc8(data, 0, data.length, (byte) 0); + } + + /** + * 计算CRC8校验值 + * + * @param data 数据 + * @param offset 起始位置 + * @param len 长度 + * @return 校验值 + */ + public static byte calcCrc8(byte[] data, int offset, int len) { + return calcCrc8(data, offset, len, (byte) 0); + } + + /** + * 计算CRC8校验值 + * + * @param data 数据 + * @param offset 起始位置 + * @param len 长度 + * @param preval 之前的校验值 + * @return 校验值 + */ + public static byte calcCrc8(byte[] data, int offset, int len, byte preval) { + byte ret = preval; + for (int i = offset; i < (offset + len); ++i) { + ret = crc8_tab[(0x00ff & (ret ^ data[i]))]; + } + return ret; + } + + // 测试 + public static void main(String[] args) { + String hex = "333701120008C100"; + byte[] bytes = ByteUtils.hexToByte(hex); + byte crc = CRC8Utils.calcCrc8_E5(bytes); + System.out.println("" + Integer.toHexString(0x00ff & crc)); + } + + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/mq/Topics.java b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/mq/Topics.java new file mode 100644 index 0000000..c9c6122 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/mq/Topics.java @@ -0,0 +1,16 @@ +package com.bnhz.common.utils.gateway.mq; + +import lombok.Data; + +/** + * @author bill + */ +@Data +public class Topics { + + + private String topicName; + private Integer qos =0; + private String desc; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/mq/TopicsPost.java b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/mq/TopicsPost.java new file mode 100644 index 0000000..3127f33 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/mq/TopicsPost.java @@ -0,0 +1,14 @@ +package com.bnhz.common.utils.gateway.mq; + +import lombok.Data; + +/** + * @author gsb + * @date 2023/2/27 13:41 + */ +@Data +public class TopicsPost { + + private String[] topics; + private int[] qos; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/mq/TopicsUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/mq/TopicsUtils.java new file mode 100644 index 0000000..0db0902 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/mq/TopicsUtils.java @@ -0,0 +1,308 @@ +package com.bnhz.common.utils.gateway.mq; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.enums.TopicType; +import com.bnhz.common.utils.collection.CollectionUtils; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + + +import java.util.*; + +/** + * topic工具类 + * + * @author gsb + * @date 2022/9/15 16:49 + */ +@Slf4j +@Component +public class TopicsUtils { + + @Value("${server.broker.enabled}") + private Boolean enabled; + + /** + * 拼接topic + * + * @param productId 产品id + * @param serialNumber 设备编号 + * @param type 主题类型 + * @return topic + */ + public String buildTopic(Long productId, String serialNumber, TopicType type) { + /* + * 订阅属性: + * 如果启动emq 则为 /+/+/property/post + * 如果启动netty的mqttBroker 则为 /{productId}/{serialNumber}/property/post + * + * 发布都为:/{productId}/{serialNumber}/property/get + */ + String product = String.valueOf(productId); + if (null == productId || productId == -1L || productId == 0L) { + product = "+"; + } + if (com.bnhz.common.utils.StringUtils.isEmpty(serialNumber)) { + serialNumber = "+"; + } + if (type.getType() == 0) { + return enabled ? "/" + product + "/" + serialNumber + type.getTopicSuffix() : BnhzConstant.MQTT.PREDIX + type.getTopicSuffix(); + } else { + return "/" + product + "/" + serialNumber + type.getTopicSuffix(); + } + } + + + /** + * 获取所有可订阅的主题 + * + * @return 订阅主题列表 + */ + public TopicsPost getAllPost() { + List qos = new ArrayList<>(); + List topics = new ArrayList<>(); + TopicsPost post = new TopicsPost(); + for (TopicType topicType : TopicType.values()) { + if (topicType.getType() == 0) { + String topic = this.buildTopic(0L, null, topicType); + topics.add(topic); + qos.add(1); + } + } + post.setTopics(topics.toArray(new String[0])); + int[] ints = Arrays.stream(qos.toArray(new Integer[0])).mapToInt(Integer::valueOf).toArray(); + post.setQos(ints); + return post; + } + + /** + * 获取所有get topic + * + * @param isSimulate 是否是设备模拟 + * @return list + */ + public static List getAllGet(boolean isSimulate) { + List result = new ArrayList<>(); + for (TopicType type : TopicType.values()) { + if (type.getType() == 4) { + Topics topics = new Topics(); + topics.setTopicName(type.getTopicSuffix()); + topics.setDesc(type.getMsg()); + topics.setQos(1); + result.add(topics); + if (isSimulate && type == TopicType.PROPERTY_GET) { + result.remove(topics); + } + } + } + return result; + } + + + /** + * 替换topic中的产品编码和设备编码,唯一作用是在系统收到来自网关设备上报子设备消息时将topic进行替换 + * + * @param orgTopic String 原始topic + * @param productId String 目标产品编码 + * @param serialNumber String 目标设备编码 + * @return 替换产品编码和设备编码后的新topic + */ + public String topicSubDevice(String orgTopic, Long productId, String serialNumber) { + if (com.bnhz.common.utils.StringUtils.isEmpty(orgTopic)) { + return orgTopic; + } + String[] splits = orgTopic.split("/"); + StringBuilder sb = new StringBuilder(splits[0]) + .append("/") + .append(productId) + .append("/") + .append(serialNumber); + for (int index = 3; index < splits.length; index++) { + sb.append("/").append(splits[index]); + } + return sb.toString(); + } + + /** + * 从topic中获取IMEI号 IMEI即是设备编号 + * + * @param topic /{productId}/{serialNumber}/property/post + * @return serialNumber + */ + @SneakyThrows + public Long parseProductId(String topic) { + String[] values = topic.split("/"); + return Long.parseLong(values[1]); + } + + + /** + * 从topic中获取IMEI号 IMEI即是设备编号 + * + * @param topic /{productId}/{serialNumber}/property/post + * @return serialNumber + */ + @SneakyThrows + public String parseSerialNumber(String topic) { + String[] values = topic.split("/"); + return values[2]; + } + + /** + * 获取topic 判断字段 name + **/ + public String parseTopicName(String topic) { + String[] values = topic.split("/"); + if (values.length >2){ + return "/"+ values[3] + "/" + values[4]; + }else { + return null; + } + } + + /** + * 获取topic 判断字段 name + **/ + public String parseTopicName4(String topic) { + String[] values = topic.split("/"); + return values[4]; + } + + /** + * 从topic解析物模型类型 + * + * @param topic /{productId}/{serialNumber}/property/post + * @return 物模型类型 + */ + @SneakyThrows + public String getThingsModel(String topic) { + String[] split = topic.split("/"); + return split[2].toUpperCase(); + } + + /** + * 检查topic的合法性 + * + * @param topicNameList 主题list + * @return 验证结果 + */ + public static boolean validTopicFilter(List topicNameList) { + for (String topicName : topicNameList) { + if (com.bnhz.common.utils.StringUtils.isEmpty(topicName)) { + return false; + } + /*以#或+符号开头的、以/符号结尾的及不存在/符号的订阅按非法订阅处理*/ + if (StringUtils.startsWithIgnoreCase(topicName, "#") || StringUtils.startsWithIgnoreCase(topicName, "+") || StringUtils.endsWithIgnoreCase(topicName, "/") || !topicName.contains("/")) { + return false; + } + if (topicName.contains("#")) { + /*不是以/#字符串结尾的订阅按非法订阅处理*/ + if (!StringUtils.endsWithIgnoreCase(topicName, "/#")) { + return false; + } + /*如果出现多个#符号的订阅按非法订阅处理*/ + if (StringUtils.countOccurrencesOf(topicName, "#") > 1) { + return false; + } + } + if (topicName.contains("+")) { + /*如果+符号和/+字符串出现的次数不等的情况按非法订阅处理*/ + if (StringUtils.countOccurrencesOf(topicName, "+") != StringUtils.countOccurrencesOf(topicName, "/+")) { + return false; + } + } + } + return true; + } + + /** + * 判断topic与topicFilter是否匹配,topic与topicFilter需要符合协议规范 + * + * @param topic: 主题 + * @param topicFilter: 主题过滤器 + * @return boolean + * @author ZhangJun + * @date 23:57 2021/2/27 + */ + public static boolean matchTopic(String topic, String topicFilter) { + if (topic.contains("+") || topic.contains("#")) { + + String[] topicSpilts = topic.split("/"); + String[] filterSpilts = topicFilter.split("/"); + + if (!topic.contains("#") && topicSpilts.length < filterSpilts.length) { + return false; + } + + String level; + for (int i = 0; i < topicSpilts.length; i++) { + level = topicSpilts[i]; + if (!level.equals(filterSpilts[i]) && !level.equals("+") && !level.equals("#")) { + return false; + } + } + } else { + return topic.equals(topicFilter); + } + return true; + } + + /** + * 根据指定topic搜索所有订阅的topic + * 指定的topic没有通配符,但是订阅的时候可能会存在通配符,所以有个查找的过程 + * + * @param topic 主题 + * @return 返回的所有主题 + */ + public static List searchTopic(String topic) { + try { + List topicList = new ArrayList<>(); + topicList.add(topic); + /*先处理#通配符*/ + String[] filterDup = topic.split("/"); + int[] source = new int[filterDup.length]; + String itemTopic = ""; + for (int i = 0; i < filterDup.length; i++) { + String item = itemTopic.concat("#"); + topicList.add(item); + itemTopic = itemTopic.concat(filterDup[i]).concat("/"); + source[i] = i; + continue; + } + /*处理+通配符*/ + Map, Boolean> map = TopicsUtils.handle(source); + for (List key : map.keySet()) { + String[] arr = CollectionUtils.copy(filterDup); + for (Integer index : key) { + arr[index] = "+"; + } + String newTopic = CollectionUtils.concat(arr, "/"); + topicList.add(newTopic); + } + return topicList; + } catch (Exception e) { + log.error("=>查询topic异常", e); + return null; + } + } + + + public static Map, Boolean> handle(int[] src) { + int nCnt = src.length; + int nBit = (0xFFFFFFFF >>> (32 - nCnt)); + Map, Boolean> map = new HashMap<>(); + for (int i = 1; i <= nBit; i++) { + List list = new ArrayList<>(); + for (int j = 0; j < nCnt; j++) { + if ((i << (31 - j)) >> 31 == -1) { + list.add(j); + } + } + map.put(list, true); + } + return map; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/protocol/ByteUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/protocol/ByteUtils.java new file mode 100644 index 0000000..b253b60 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/protocol/ByteUtils.java @@ -0,0 +1,958 @@ +package com.bnhz.common.utils.gateway.protocol; + + +import com.bnhz.common.exception.ServiceException; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + + +public class ByteUtils { + + + // public static Payload resolvePayload(byte[] content, short start, ModbusCode code) { + // Payload payload; + // switch (code) { + // case Read01: + // case Read02: + // payload = new RealCoilPayload(start, content); break; + // case Read03: + // case Read04: + // payload = new ReadPayload(content, start); break; + // default: + // payload = WritePayload.getInstance(); + // } + // + // return payload; + // } + + public static Write10Build write10Build(Object... args) { + int num = 0; List bytes = new ArrayList<>(); + for(Object arg : args) { + if(arg instanceof Integer) { + num += 2; + bytes.add(getBytes((Integer) arg)); + } else if(arg instanceof Long) { + num += 4; + bytes.add(getBytes((Long) arg)); + } else if(arg instanceof Float) { + num += 2; + bytes.add(getBytes((Float) arg)); + } else if(arg instanceof Double) { + num += 4; + bytes.add(getBytes((Double) arg)); + } else if(arg instanceof Short) { + num += 1; + bytes.add(getBytesOfReverse((Short) arg)); + } else if(arg instanceof Byte) { + num += 1; + bytes.add(new byte[]{0x00, (byte) arg}); + } else if(arg instanceof String) { + byte[] bytes1 = arg.toString().getBytes(StandardCharsets.UTF_8); + if(bytes1.length % 2 != 0) { + num += bytes1.length / 2 + 1; + byte[] addMessage = new byte[bytes1.length + 1]; + addBytes(addMessage, bytes1, 0); + bytes.add(addMessage); + } else { + num += bytes1.length / 2; + bytes.add(bytes1); + } + } else { + throw new ServiceException("不支持的数据类型"); + } + } + + Integer length = bytes.stream().map(item -> item.length).reduce((a, b) -> a + b).get(); + byte[] write = new byte[length]; + + int index = 0; + for(int i=0; i> 8); + return bytes; + } + + /** + * 将short数据类型转化成Byte数组 + * @see ByteOrder#BIG_ENDIAN + * @param data short值 + * @return byte[]数组 + */ + public static byte[] getBytesOfReverse(short data) { + byte[] bytes = new byte[2]; + bytes[1] = (byte) (data & 0xff); + bytes[0] = (byte) ((data & 0xff00) >> 8); + return bytes; + } + + /** + * @see ByteOrder#LITTLE_ENDIAN + * @param bytes + * @param offset + * @return + */ + public static short bytesToShort(byte[] bytes, int offset) { + return (short) ((0xff & bytes[0 + offset]) | (0xff00 & (bytes[1 + offset] << 8))); + } + + /** + * 将字节数组转换成short数据 + * @see ByteOrder#BIG_ENDIAN + * @param bytes 字节数组 + * @return short值 + */ + public static short bytesToShortOfReverse(byte[] bytes) { + return ByteUtils.bytesToShortOfReverse(bytes, 0); + } + + /** + * 将字节数组转换成short数据,采用倒序的表达方式 + * @param bytes 字节数组 + * @param offset 起始位置 + * @return short值 + */ + public static short bytesToShortOfReverse(byte[] bytes, int offset) { + return (short) ((0xff & bytes[1 + offset]) | (0xff00 & (bytes[0 + offset] << 8))); + } + + /** + * 将int数据类型转化成Byte数组 + * @see ByteOrder#LITTLE_ENDIAN + * @param data int值 + * @return byte[]数组 + */ + public static byte[] getBytes(int data) { + byte[] bytes = new byte[4]; + bytes[0] = (byte) (data & 0xff); + bytes[1] = (byte) ((data >> 8) & 0xff); + bytes[2] = (byte) ((data >> 16) & 0xff); + bytes[3] = (byte) ((data >> 24) & 0xff); + return bytes; + } + + /** + * 将int数据类型转化成Byte数组 倒序 + * @see ByteOrder#BIG_ENDIAN + * @param data int值 + * @return byte[]数组 + */ + public static byte[] getBytesOfReverse(int data) { + byte[] src = new byte[4]; + src[0] = (byte) ((data >> 24) & 0xFF); + src[1] = (byte) ((data >> 16) & 0xFF); + src[2] = (byte) ((data >> 8) & 0xFF); + src[3] = (byte) (data & 0xFF); + return src; + } + + /** + * 将long数据类型转化成Byte数组 + * @see ByteOrder#LITTLE_ENDIAN + * @param data long值 + * @return byte[]数组 + */ + public static byte[] getBytes(long data) { + byte[] bytes = new byte[8]; + bytes[0] = (byte) (data & 0xff); + bytes[1] = (byte) ((data >> 8) & 0xff); + bytes[2] = (byte) ((data >> 16) & 0xff); + bytes[3] = (byte) ((data >> 24) & 0xff); + bytes[4] = (byte) ((data >> 32) & 0xff); + bytes[5] = (byte) ((data >> 40) & 0xff); + bytes[6] = (byte) ((data >> 48) & 0xff); + bytes[7] = (byte) ((data >> 56) & 0xff); + return bytes; + } + + /** + * 将long数据类型转化成Byte数组 倒序 + * @see ByteOrder#BIG_ENDIAN + * @param data long值 + * @return byte[]数组 + */ + public static byte[] getBytesOfReverse(long data) { + byte[] bytes = new byte[8]; + bytes[7] = (byte) (data & 0xff); + bytes[6] = (byte) ((data >> 8) & 0xff); + bytes[5] = (byte) ((data >> 16) & 0xff); + bytes[4] = (byte) ((data >> 24) & 0xff); + bytes[3] = (byte) ((data >> 32) & 0xff); + bytes[2] = (byte) ((data >> 40) & 0xff); + bytes[1] = (byte) ((data >> 48) & 0xff); + bytes[0] = (byte) ((data >> 56) & 0xff); + return bytes; + } + + /** + * 将float数据类型转化成Byte数组 + * @see ByteOrder#LITTLE_ENDIAN + * @param data float值 + * @return byte[]数组 + */ + public static byte[] getBytes(float data) { + int intBits = Float.floatToIntBits(data); + return getBytes(intBits); + } + + /** + * 将float数据类型转化成Byte数组 倒序 + * @see ByteOrder#BIG_ENDIAN + * @param data float值 + * @return byte[]数组 + */ + public static byte[] getBytesOfReverse(float data) { + int intBits = Float.floatToIntBits(data); + return getBytesOfReverse(intBits); + } + + /** + * 将double数据类型转化成Byte数组 + * @see ByteOrder#LITTLE_ENDIAN + * @param data double值 + * @return byte[]数组 + */ + public static byte[] getBytes(double data) { + long intBits = Double.doubleToLongBits(data); + return getBytes(intBits); + } + + /** + * 将double数据类型转化成Byte数组 倒序 + * @see ByteOrder#BIG_ENDIAN + * @param data double值 + * @return byte[]数组 + */ + public static byte[] getBytesOfReverse(double data) { + long intBits = Double.doubleToLongBits(data); + return getBytesOfReverse(intBits); + } + + /** + * 字符串转字节数组(UTF-8) + * @param data + * @return + */ + public static byte[] getBytes(String data) { + return data.getBytes(StandardCharsets.UTF_8); + } + + /** + * 将字符串转换成byte[]数组 + * @param data 字符串值 + * @param charsetName 编码方式 + * @return 字节数组 + */ + public static byte[] getBytes(String data, String charsetName) { + Charset charset = Charset.forName(charsetName); + return data.getBytes(charset); + } + + /* + * 把16进制字符串转换成字节数组 + * @param hex + * @return + */ + public static byte[] hexToByte(String hexStr) { + if(StringUtils.isBlank(hexStr)) { + return null; + } + if(hexStr.length()%2 != 0) {//长度为单数 + hexStr = "0" + hexStr;//前面补0 + } + + char[] chars = hexStr.toCharArray(); + int len = chars.length/2; + byte[] bytes = new byte[len]; + for (int i = 0; i < len; i++) { + int x = i*2; + bytes[i] = (byte)Integer.parseInt(String.valueOf(new char[]{chars[x], chars[x+1]}), 16); + } + return bytes; + + } + + public static byte getByte(byte[] src, int offset) { + return src[offset]; + } + + /** + * 字节数组转16进制 + * @param bArray + * @return + */ + public static final String bytesToHex(byte[] bArray) { + StringBuffer sb = new StringBuffer(bArray.length); + String sTemp; + for (int i = 0; i < bArray.length; i++) { + sTemp = Integer.toHexString(0xFF & bArray[i]); + if (sTemp.length() < 2) sb.append(0); + sb.append(sTemp.toUpperCase()); + } + return sb.toString(); + } + + /** + * 字节数组转16进制且格式化十六进制 + * @param bArray + * @return + */ + public static final String bytesToHexByFormat(byte[] bArray) { + StringBuffer sb = new StringBuffer(bArray.length); + String sTemp; + for (int i = 0; i < bArray.length; i++) { + sTemp = Integer.toHexString(0xFF & bArray[i]); + if (sTemp.length() < 2) sb.append(0); + sb.append(sTemp.toUpperCase()).append(' '); + } + return sb.toString(); + } + + /** + * 字节数组转16进制 + * @param src + * @return + */ + public static final String bytesToHex(byte[] src, int offset, int length) { + byte[] bArray = ArrayUtils.subarray(src, offset, offset + length); + return bytesToHex(bArray); + } + + public static final String byteToHex(byte value) { + String s = Integer.toHexString(0xff & value); + if(s.length() == 1) return "0"+s; + return s; + } + + public static final String shortToHex(short value) { + String s = Integer.toHexString(value); + switch (s.length()) { + case 1: return "000" + s; + case 2: return "00" + s; + case 3: return "0" + s; + default: return s; + } + } + + public static final String intToHex(int value) { + StringBuilder s = new StringBuilder(Integer.toHexString(value)); + String v1 = s.toString().replace("f",""); + if (v1.length() <4){ + for (int i = 0; i < 4 - v1.length(); i++) { + s.insert(0, "0"); + } + return s.toString().replace("f",""); + } + else return s.toString().replace("f",""); + } + + public static final String hexTo8Bit(int value,int index){ + String s = Integer.toBinaryString(value); + StringBuilder result = new StringBuilder(s); + if (s.length() < index){ + for (int i = 0; i < index - s.length(); i++) { + result.insert(0,"0"); + } + } + return result.toString(); + } + + /** + * @函数功能: BCD码转为10进制串(阿拉伯数据) + * @输入参数: BCD码 + * @输出结果: 10进制串 + */ + public static String bcdToStr(byte[] bytes){ + StringBuffer temp=new StringBuffer(bytes.length*2); + + for(int i=0;i>>4)); + temp.append((byte)(bytes[i] & 0x0f)); + } + + return temp.toString(); + } + + /** + * + * @param src 原报文 + * @param offset 起始位置 + * @param length 长度 + * @return + */ + public static String bcdToStr(byte[] src, int offset, int length){ + byte[] bArray = ArrayUtils.subarray(src, offset, offset + length); + return bcdToStr(bArray); + } + + public static byte[] str2Bcd(String asc) { + int len = asc.length(); + int mod = len % 2; + + if (mod != 0) { + asc = "0" + asc; + len = asc.length(); + } + + byte abt[]; + if (len >= 2) { + len = len / 2; + } + + byte bbt[] = new byte[len]; + abt = asc.getBytes(); + int j, k; + + for (int p = 0; p < asc.length()/2; p++) { + if ( (abt[2 * p] >= '0') && (abt[2 * p] <= '9')) { + j = abt[2 * p] - '0'; + } else if ( (abt[2 * p] >= 'a') && (abt[2 * p] <= 'z')) { + j = abt[2 * p] - 'a' + 0x0a; + } else { + j = abt[2 * p] - 'A' + 0x0a; + } + + if ( (abt[2 * p + 1] >= '0') && (abt[2 * p + 1] <= '9')) { + k = abt[2 * p + 1] - '0'; + } else if ( (abt[2 * p + 1] >= 'a') && (abt[2 * p + 1] <= 'z')) { + k = abt[2 * p + 1] - 'a' + 0x0a; + }else { + k = abt[2 * p + 1] - 'A' + 0x0a; + } + + int a = (j << 4) + k; + byte b = (byte) a; + bbt[p] = b; + } + return bbt; + } + + private static byte toByte(char c) { + return (byte) c; + } + + /** + * 将字节数组转换成 ushort 数据 + * @param bytes 字节数组 + * @param offset 起始位置 + * @return short值 + */ + public static int bytesToUShort(byte[] bytes, int offset) { + return ((0xff & bytes[0 + offset]) | (0xff00 & (bytes[1 + offset] << 8))); + } + + /** + * 将字节数组转换成 ushort 数据,采用倒序的方式 + * @param bytes 字节数组 + * @return short值 + */ + public static int bytesToUShortOfReverse(byte[] bytes) { + return ByteUtils.bytesToShortOfReverse(bytes, 0); + } + + /** + * 将字节数组转换成 ushort 数据,采用倒序的方式 + * @param bytes 字节数组 + * @param offset 起始位置 + * @return short值 + */ + public static int bytesToUShortOfReverse(byte[] bytes, int offset) { + return ((0xff & bytes[1 + offset]) | (0xff00 & (bytes[0 + offset] << 8))); + } + + /** + * byte数组中取int数值,本方法适用于(低位在前,高位在后)的顺序,和intToBytes配套使用 + * + * @param src + * byte数组 + * @param offset + * 从数组的第offset位开始 + * @return int数值 + */ + public static int bytesToInt(byte[] src, int offset) { + return ((src[offset] & 0xFF) | ((src[offset + 1] & 0xFF) << 8) + | ((src[offset + 2] & 0xFF) << 16) | ((src[offset + 3] & 0xFF) << 24)); + } + + /** + * byte数组中取int数值,本方法适用于(低位在前,高位在后)的顺序,和intToBytes配套使用 + * @param src byte数组 + * @return int数值 + */ + public static int bytesToInt(byte[] src) { + return ByteUtils.bytesToInt(src, 0); + } + + /** + * 将字节数组转换成int数据,采用倒序的方式 + * @param bytes 字节数组 + * @param offset 起始位置 + * @return int值 + */ + public static int bytesToIntOfReverse(byte[] bytes, int offset) { + return (0xff & bytes[3 + offset]) | + (0xff00 & (bytes[2 + offset] << 8)) | + (0xff0000 & (bytes[1 + offset] << 16)) | + (0xff000000 & (bytes[0 + offset] << 24)); + } + + /** + * 将字节数组转换成int数据,采用倒序的方式 + * @param bytes 字节数组 + * @return int值 + */ + public static int bytesToIntOfReverse(byte[] bytes) { + return ByteUtils.bytesToIntOfReverse(bytes, 0); + } + + /** + * 将字节数组转换成uint数据 + * @param bytes 字节数组 + * @param offset 起始位置 + * @return int值 + */ + public static long bytesToUInt(byte[] bytes, int offset) { + int value = bytesToInt(bytes, offset); + if (value >= 0) return value; + return 65536L * 65536L + value; + } + + /** + * 将字节数组转换成uint数据 + * @param bytes 字节数组 + * @return int值 + */ + public static long bytesToUInt(byte[] bytes) { + return ByteUtils.bytesToUInt(bytes, 0); + } + + /** + * 将字节数组转换成uint数据 + * @param bytes 字节数组 + * @param offset 起始位置 + * @return int值 + */ + public static long bytesToUIntOfReverse(byte[] bytes, int offset) { + int value = bytesToIntOfReverse(bytes, offset); + if (value >= 0) return value; + return 65536L * 65536L + value; + } + + /** + * 将字节数组转换成uint数据 倒序 + * @param bytes 字节数组 + * @return int值 + */ + public static long bytesToUIntOfReverse(byte[] bytes) { + return ByteUtils.bytesToUIntOfReverse(bytes, 0); + } + + /** + * 将字节数组转换成float数据 + * @param bytes 字节数组 + * @return float值 + */ + public static float bytesToFloat(byte[] bytes) { + return Float.intBitsToFloat(bytesToInt(bytes, 0)); + } + + /** + * 将字节数组转换成float数据 + * @param bytes 字节数组 + * @param offset 起始位置 + * @return float值 + */ + public static float bytesToFloat(byte[] bytes, int offset) { + return Float.intBitsToFloat(bytesToInt(bytes,offset)); + } + + /** + * 将字节数组转换成float数据 + * @param bytes 字节数组 + * @return float值 + */ + public static float bytesToFloatOfReverse(byte[] bytes) { + return bytesToFloatOfReverse(bytes, 0); + } + + /** + * 将字节数组转换成float数据 + * @param bytes 字节数组 + * @param offset 偏移量 + * @return float值 + */ + public static float bytesToFloatOfReverse(byte[] bytes, int offset) { + return Float.intBitsToFloat(bytesToIntOfReverse(bytes, offset)); + } + + + /** + * byte数组中取double数值 + * @param src byte数组 + * @return double数值 + */ + public static double bytesToDouble(byte[] src) { + return Double.longBitsToDouble(bytesToLong(src)); + } + + /** + * byte数组中取double数值 + * @param src byte数组 + * @param offset 从数组的第offset位开始 + * @return double数值 + */ + public static double bytesToDouble(byte[] src, int offset) { + return Double.longBitsToDouble(bytesToLong(src, offset)); + } + + /** + * byte数组中取double数值 + * @param src byte数组 + * @return double数值 + */ + public static double bytesToDoubleOfReverse(byte[] src) { + return bytesToDoubleOfReverse(src, 0); + } + + /** + * byte数组中取double数值 + * @param src byte数组 + * @param offset 从数组的第offset位开始 + * @return double数值 + */ + public static double bytesToDoubleOfReverse(byte[] src, int offset) { + return Double.longBitsToDouble(bytesToLongOfReverse(src, offset)); + } + + /** + * 去掉字节数组尾数为零的字节,并将其转成字符串 + * @param src + * @param charset + * @return + */ + public static String bytesToString(byte[] src, Charset charset){ + int search = Arrays.binarySearch(src, (byte) 0); + return new String(Arrays.copyOf(src, search), charset); + } + + /** + * 去掉字节数组尾数为零的字节,并将其转成字符串 + * @param src + * @return + */ + public static String bytesToString(byte[] src){ + return new String(wipeLastZero(src)); + } + + /** + * 去掉字节数组尾数为零的字节,并将其转成字符串 + * @param src + * @return + */ + public static String bytesToString(byte[] src, int startIndex, int endIndex){ + return new String(wipeLastZero(subBytes(src, startIndex, endIndex))); + } + + /** + * 去掉字节数组尾数为零的字节,并将其转成字符串 + * @param src + * @return + */ + public static String bytesToString(byte[] src, int startIndex, int endIndex, Charset charset){ + return new String(wipeLastZero(subBytes(src, startIndex, endIndex)), charset); + } + + /** + * 将byte[]数组的数据进行翻转 + * @param reverse 等待反转的字符串 + */ + public static void bytesReverse(byte[] reverse) { + if (reverse != null) { + byte tmp = 0; + for (int i = 0; i < reverse.length / 2; i++) { + tmp = reverse[i]; + reverse[i] = reverse[reverse.length - 1 - i]; + reverse[reverse.length - 1 - i] = tmp; + } + } + } + + /** + * 去除包含0的字节 + * @param src + * @return + */ + private static byte[] wipeLastZero(byte[] src){ + int index = 0; + for(int i=0; i 0 ? 1 : 0); + } + + /** + *将bool数组转换到byte数组
+ * @param array bool数组 + * @return 字节数组 + */ + public static byte[] boolArrayToByte(boolean[] array) { + if (array == null) return null; + + int length = array.length % 8 == 0 ? array.length / 8 : array.length / 8 + 1; + byte[] buffer = new byte[length]; + + for (int i = 0; i < array.length; i++) { + if (array[i]) { + buffer[i / 8] += (1 << i % 8); + } + } + + return buffer; + } + + public static Integer cutMessageHexTo(byte[] source, int startIndex, int endIndex){ + byte[] subarray = ArrayUtils.subarray(source, startIndex, endIndex); + String s = bytesToHexString(subarray); + return Integer.parseInt(s,16); + } + + /** + * byte数组转换炒年糕十六进制字符串 + * + * @param bArray byte数组 + * @return hex字符串 + */ + public static String bytesToHexString(byte[] bArray) { + StringBuilder sb = new StringBuilder(bArray.length); + for (int i = 0; i < bArray.length; i++) { + String hexStr = Integer.toHexString(0xFF & bArray[i]); + if (hexStr.length() < 2) { + sb.append(0); + } + sb.append(hexStr.toUpperCase()); + } + return sb.toString(); + } + + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/protocol/NettyUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/protocol/NettyUtils.java new file mode 100644 index 0000000..47dac99 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/gateway/protocol/NettyUtils.java @@ -0,0 +1,22 @@ +package com.bnhz.common.utils.gateway.protocol; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; + +/** + * @author gsb + * @date 2022/9/15 14:44 + */ +public class NettyUtils { + + /** + * ByteBuf转 byte[] + * @param buf buffer + * @return byte[] + */ + public static byte[] readBytesFromByteBuf(ByteBuf buf){ + return ByteBufUtil.getBytes(buf); + } + + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/html/EscapeUtil.java b/bnhz-common/src/main/java/com/bnhz/common/utils/html/EscapeUtil.java new file mode 100644 index 0000000..0fb3787 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/html/EscapeUtil.java @@ -0,0 +1,167 @@ +package com.bnhz.common.utils.html; + +import com.bnhz.common.utils.StringUtils; + +/** + * 转义和反转义工具类 + * + * @author ruoyi + */ +public class EscapeUtil +{ + public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)"; + + private static final char[][] TEXT = new char[64][]; + + static + { + for (int i = 0; i < 64; i++) + { + TEXT[i] = new char[] { (char) i }; + } + + // special HTML characters + TEXT['\''] = "'".toCharArray(); // 单引号 + TEXT['"'] = """.toCharArray(); // 双引号 + TEXT['&'] = "&".toCharArray(); // &符 + TEXT['<'] = "<".toCharArray(); // 小于号 + TEXT['>'] = ">".toCharArray(); // 大于号 + } + + /** + * 转义文本中的HTML字符为安全的字符 + * + * @param text 被转义的文本 + * @return 转义后的文本 + */ + public static String escape(String text) + { + return encode(text); + } + + /** + * 还原被转义的HTML特殊字符 + * + * @param content 包含转义符的HTML内容 + * @return 转换后的字符串 + */ + public static String unescape(String content) + { + return decode(content); + } + + /** + * 清除所有HTML标签,但是不删除标签内的内容 + * + * @param content 文本 + * @return 清除标签后的文本 + */ + public static String clean(String content) + { + return new HTMLFilter().filter(content); + } + + /** + * Escape编码 + * + * @param text 被编码的文本 + * @return 编码后的字符 + */ + private static String encode(String text) + { + if (StringUtils.isEmpty(text)) + { + return StringUtils.EMPTY; + } + + final StringBuilder tmp = new StringBuilder(text.length() * 6); + char c; + for (int i = 0; i < text.length(); i++) + { + c = text.charAt(i); + if (c < 256) + { + tmp.append("%"); + if (c < 16) + { + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + else + { + tmp.append("%u"); + if (c <= 0xfff) + { + // issue#I49JU8@Gitee + tmp.append("0"); + } + tmp.append(Integer.toString(c, 16)); + } + } + return tmp.toString(); + } + + /** + * Escape解码 + * + * @param content 被转义的内容 + * @return 解码后的字符串 + */ + public static String decode(String content) + { + if (StringUtils.isEmpty(content)) + { + return content; + } + + StringBuilder tmp = new StringBuilder(content.length()); + int lastPos = 0, pos = 0; + char ch; + while (lastPos < content.length()) + { + pos = content.indexOf("%", lastPos); + if (pos == lastPos) + { + if (content.charAt(pos + 1) == 'u') + { + ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16); + tmp.append(ch); + lastPos = pos + 6; + } + else + { + ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16); + tmp.append(ch); + lastPos = pos + 3; + } + } + else + { + if (pos == -1) + { + tmp.append(content.substring(lastPos)); + lastPos = content.length(); + } + else + { + tmp.append(content.substring(lastPos, pos)); + lastPos = pos; + } + } + } + return tmp.toString(); + } + + public static void main(String[] args) + { + String html = ""; + String escape = EscapeUtil.escape(html); + // String html = "ipt>alert(\"XSS\")ipt>"; + // String html = "<123"; + // String html = "123>"; + System.out.println("clean: " + EscapeUtil.clean(html)); + System.out.println("escape: " + escape); + System.out.println("unescape: " + EscapeUtil.unescape(escape)); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/html/HTMLFilter.java b/bnhz-common/src/main/java/com/bnhz/common/utils/html/HTMLFilter.java new file mode 100644 index 0000000..6f77dc1 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/html/HTMLFilter.java @@ -0,0 +1,570 @@ +package com.bnhz.common.utils.html; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML过滤器,用于去除XSS漏洞隐患。 + * + * @author ruoyi + */ +public final class HTMLFilter +{ + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap<>(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + /** + * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "" + * becomes " text "). If set to false, unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilter() + { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList<>(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList<>(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList<>(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[] { "img" }; + vNeedClosingTags = new String[] { "a", "b", "strong", "i", "em" }; + vDisallowed = new String[] {}; + vAllowedProtocols = new String[] { "http", "mailto", "https" }; // no ftp. + vProtocolAtts = new String[] { "src", "href" }; + vRemoveBlanks = new String[] { "a", "b", "strong", "i", "em" }; + vAllowedEntities = new String[] { "amp", "gt", "lt", "quot" }; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = false; + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + @SuppressWarnings("unchecked") + public HTMLFilter(final Map conf) + { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() + { + vTagCounts.clear(); + } + + // --------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) + { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) + { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + // --------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) + { + reset(); + String s = input; + + s = escapeComments(s); + + s = balanceHTML(s); + + s = checkTags(s); + + s = processRemoveBlanks(s); + + // s = validateEntities(s); + + return s; + } + + public boolean isAlwaysMakeTags() + { + return alwaysMakeTags; + } + + public boolean isStripComments() + { + return stripComment; + } + + private String escapeComments(final String s) + { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) + { + final String match = m.group(1); // (.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) + { + if (alwaysMakeTags) + { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + // 不追加结束标签 + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } + else + { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) + { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) + { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + final StringBuilder sBuilder = new StringBuilder(buf.toString()); + for (String key : vTagCounts.keySet()) + { + for (int ii = 0; ii < vTagCounts.get(key); ii++) + { + sBuilder.append(""); + } + } + s = sBuilder.toString(); + + return s; + } + + private String processRemoveBlanks(final String s) + { + String result = s; + for (String tag : vRemoveBlanks) + { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) + { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) + { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) + { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) + { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) + { + if (!inArray(name, vSelfClosingTags)) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) + { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) + { + final StringBuilder params = new StringBuilder(); + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList<>(); + final List paramValues = new ArrayList<>(); + while (m2.find()) + { + paramNames.add(m2.group(1)); // ([a-z0-9]+) + paramValues.add(m2.group(3)); // (.*?) + } + while (m3.find()) + { + paramNames.add(m3.group(1)); // ([a-z0-9]+) + paramValues.add(m3.group(3)); // ([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) + { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + + // debug( "paramName='" + paramName + "'" ); + // debug( "paramValue='" + paramValue + "'" ); + // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) + { + if (inArray(paramName, vProtocolAtts)) + { + paramValue = processParamProtocol(paramValue); + } + params.append(' ').append(paramName).append("=\\\"").append(paramValue).append("\\\""); + } + } + + if (inArray(name, vSelfClosingTags)) + { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) + { + ending = ""; + } + + if (ending == null || ending.length() < 1) + { + if (vTagCounts.containsKey(name)) + { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } + else + { + vTagCounts.put(name, 1); + } + } + else + { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } + else + { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) + { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) + { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) + { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) + { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith("#//")) + { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) + { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) + { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) + { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // ([^&;]*) + final String two = m.group(2); // (?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) + { + if (encodeQuotes) + { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) + { + final String one = m.group(1); // (>|^) + final String two = m.group(2); // ([^<]+?) + final String three = m.group(3); // (<|$) + // 不替换双引号为",防止json格式无效 regexReplace(P_QUOTE, """, two) + m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three)); + } + m.appendTail(buf); + return buf.toString(); + } + else + { + return s; + } + } + + private String checkEntity(final String preamble, final String term) + { + + return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble; + } + + private boolean isValidEntity(final String entity) + { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) + { + for (String item : array) + { + if (item != null && item.equals(s)) + { + return true; + } + } + return false; + } + + private boolean allowed(final String name) + { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) + { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/http/HttpHelper.java b/bnhz-common/src/main/java/com/bnhz/common/utils/http/HttpHelper.java new file mode 100644 index 0000000..e6c1093 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/http/HttpHelper.java @@ -0,0 +1,55 @@ +package com.bnhz.common.utils.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import javax.servlet.ServletRequest; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 通用http工具封装 + * + * @author ruoyi + */ +public class HttpHelper +{ + private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class); + + public static String getBodyString(ServletRequest request) + { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = null; + try (InputStream inputStream = request.getInputStream()) + { + reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String line = ""; + while ((line = reader.readLine()) != null) + { + sb.append(line); + } + } + catch (IOException e) + { + LOGGER.warn("getBodyString出现问题!"); + } + finally + { + if (reader != null) + { + try + { + reader.close(); + } + catch (IOException e) + { + LOGGER.error(ExceptionUtils.getMessage(e)); + } + } + } + return sb.toString(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/http/HttpUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/http/HttpUtils.java new file mode 100644 index 0000000..fee20ce --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/http/HttpUtils.java @@ -0,0 +1,338 @@ +package com.bnhz.common.utils.http; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.utils.StringUtils; + +/** + * 通用http发送方法 + * + * @author ruoyi + */ +public class HttpUtils +{ + private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url) + { + return sendGet(url, StringUtils.EMPTY); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param) + { + return sendGet(url, param, Constants.UTF8); + } + + /** + * 向指定 URL 发送GET方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @param contentType 编码类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param, String contentType) + { + StringBuilder result = new StringBuilder(); + BufferedReader in = null; + try + { + String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url; + log.info("sendGet - {}", urlNameString); + URL realUrl = new URL(urlNameString); + URLConnection connection = realUrl.openConnection(); + connection.setRequestProperty("accept", "*/*"); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + connection.connect(); + in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (in != null) + { + in.close(); + } + } + catch (Exception ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 JSON String格式 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendPost(String url, String param) + { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try + { + log.info("sendPost - {}", url); + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("contentType", "utf-8"); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(param); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e); + } + finally + { + try + { + if (out != null) + { + out.close(); + } + if (in != null) + { + in.close(); + } + } + catch (IOException ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + public static String sendSSLPost(String url, String param) + { + StringBuilder result = new StringBuilder(); + String urlNameString = url + "?" + param; + try + { + log.info("sendSSLPost - {}", urlNameString); + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom()); + URL console = new URL(urlNameString); + HttpsURLConnection conn = (HttpsURLConnection) console.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("contentType", "utf-8"); + conn.setDoOutput(true); + conn.setDoInput(true); + + conn.setSSLSocketFactory(sc.getSocketFactory()); + conn.setHostnameVerifier(new TrustAnyHostnameVerifier()); + conn.connect(); + InputStream is = conn.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String ret = ""; + while ((ret = br.readLine()) != null) + { + if (ret != null && !"".equals(ret.trim())) + { + result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); + } + } + log.info("recv - {}", result); + conn.disconnect(); + br.close(); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e); + } + return result.toString(); + } + + public static String sendJsonPost(String url, String json) throws IOException { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try + { + log.info("sendPost - {}", url); + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(json); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) + { + result.append(line); + } + log.info("recv - {}", result); + } + catch (ConnectException e) + { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + json, e); + } + catch (SocketTimeoutException e) + { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + json, e); + } + catch (IOException e) + { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + json, e); + } + catch (Exception e) + { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + json, e); + } + finally + { + try + { + if (out != null) + { + out.close(); + } + if (in != null) + { + in.close(); + } + } + catch (IOException ex) + { + log.error("调用in.close Exception, url=" + url + ",param=" + json, ex); + } + } + return result.toString(); + } + + private static class TrustAnyTrustManager implements X509TrustManager + { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + { + } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return new X509Certificate[] {}; + } + } + + private static class TrustAnyHostnameVerifier implements HostnameVerifier + { + @Override + public boolean verify(String hostname, SSLSession session) + { + return true; + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/ip/AddressUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/ip/AddressUtils.java new file mode 100644 index 0000000..7ff4886 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/ip/AddressUtils.java @@ -0,0 +1,56 @@ +package com.bnhz.common.utils.ip; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.config.DaQiConfig; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.http.HttpUtils; + +/** + * 获取地址类 + * + * @author ruoyi + */ +public class AddressUtils +{ + private static final Logger log = LoggerFactory.getLogger(AddressUtils.class); + + // IP地址查询 + public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp"; + + // 未知地址 + public static final String UNKNOWN = "XX XX"; + + public static String getRealAddressByIP(String ip) + { + // 内网不查询 + if (IpUtils.internalIp(ip)) + { + return "内网IP"; + } + if (DaQiConfig.isAddressEnabled()) + { + try + { + String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", Constants.GBK); + if (StringUtils.isEmpty(rspStr)) + { + log.error("获取地理位置异常 {}", ip); + return UNKNOWN; + } + JSONObject obj = JSON.parseObject(rspStr); + String region = obj.getString("pro"); + String city = obj.getString("city"); + return String.format("%s %s", region, city); + } + catch (Exception e) + { + log.error("获取地理位置异常 {}", ip); + } + } + return UNKNOWN; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/ip/IpUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/ip/IpUtils.java new file mode 100644 index 0000000..35285b1 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/ip/IpUtils.java @@ -0,0 +1,264 @@ +package com.bnhz.common.utils.ip; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import javax.servlet.http.HttpServletRequest; +import com.bnhz.common.utils.StringUtils; + +/** + * 获取IP方法 + * + * @author ruoyi + */ +public class IpUtils +{ + /** + * 获取客户端IP + * + * @param request 请求对象 + * @return IP地址 + */ + public static String getIpAddr(HttpServletRequest request) + { + if (request == null) + { + return "unknown"; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Forwarded-For"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) + { + ip = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) + { + byte[] addr = textToNumericFormatV4(ip); + return internalIp(addr) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param addr byte地址 + * @return 结果 + */ + private static boolean internalIp(byte[] addr) + { + if (StringUtils.isNull(addr) || addr.length < 2) + { + return true; + } + final byte b0 = addr[0]; + final byte b1 = addr[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) + { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) + { + return true; + } + case SECTION_5: + switch (b1) + { + case SECTION_6: + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + */ + public static byte[] textToNumericFormatV4(String text) + { + if (text.length() == 0) + { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try + { + long l; + int i; + switch (elements.length) + { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) + { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) + { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) + { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) + { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) + { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } + catch (NumberFormatException e) + { + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + */ + public static String getHostIp() + { + try + { + return InetAddress.getLocalHost().getHostAddress(); + } + catch (UnknownHostException e) + { + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + */ + public static String getHostName() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException e) + { + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + */ + public static String getMultistageReverseProxyIp(String ip) + { + // 多级反向代理检测 + if (ip != null && ip.indexOf(",") > 0) + { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) + { + if (false == isUnknown(subIp)) + { + ip = subIp; + break; + } + } + } + return ip; + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + */ + public static boolean isUnknown(String checkString) + { + return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/json/JsonUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/json/JsonUtils.java new file mode 100644 index 0000000..67aedd0 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/json/JsonUtils.java @@ -0,0 +1,159 @@ +package com.bnhz.common.utils.json; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * JSON 工具类 + * + * @author bnhz + */ +@UtilityClass +@Slf4j +public class JsonUtils { + + private static ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化 + } + + /** + * 初始化 objectMapper 属性 + *

+ * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean + * + * @param objectMapper ObjectMapper 对象 + */ + public static void init(ObjectMapper objectMapper) { + JsonUtils.objectMapper = objectMapper; + } + + @SneakyThrows + public static String toJsonString(Object object) { + return objectMapper.writeValueAsString(object); + } + + @SneakyThrows + public static byte[] toJsonByte(Object object) { + return objectMapper.writeValueAsBytes(object); + } + + @SneakyThrows + public static String toJsonPrettyString(Object object) { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); + } + + public static T parseObject(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, Type type) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + /** + * 将字符串解析成指定类型的对象 + * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下, + * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。 + * + * @param text 字符串 + * @param clazz 类型 + * @return 对象 + */ + public static T parseObject2(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + return JSONUtil.toBean(text, clazz); + } + + public static T parseObject(byte[] bytes, Class clazz) { + if (ArrayUtil.isEmpty(bytes)) { + return null; + } + try { + return objectMapper.readValue(bytes, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", bytes, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static List parseArray(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(String text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(byte[] text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static boolean isJson(String text) { + return JSONUtil.isTypeJSON(text); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/object/ObjectUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/object/ObjectUtils.java new file mode 100644 index 0000000..03f7871 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/object/ObjectUtils.java @@ -0,0 +1,63 @@ +package com.bnhz.common.utils.object; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.function.Consumer; + +/** + * Object 工具类 + * + * @author bnhz + */ +public class ObjectUtils { + + /** + * 复制对象,并忽略 Id 编号 + * + * @param object 被复制对象 + * @param consumer 消费者,可以二次编辑被复制对象 + * @return 复制后的对象 + */ + public static T cloneIgnoreId(T object, Consumer consumer) { + T result = ObjectUtil.clone(object); + // 忽略 id 编号 + Field field = ReflectUtil.getField(object.getClass(), "id"); + if (field != null) { + ReflectUtil.setFieldValue(result, field, null); + } + // 二次编辑 + if (result != null) { + consumer.accept(result); + } + return result; + } + + public static > T max(T obj1, T obj2) { + if (obj1 == null) { + return obj2; + } + if (obj2 == null) { + return obj1; + } + return obj1.compareTo(obj2) > 0 ? obj1 : obj2; + } + + @SafeVarargs + public static T defaultIfNull(T... array) { + for (T item : array) { + if (item != null) { + return item; + } + } + return null; + } + + @SafeVarargs + public static boolean equalsAny(T obj, T... array) { + return Arrays.asList(array).contains(obj); + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/poi/ExcelHandlerAdapter.java b/bnhz-common/src/main/java/com/bnhz/common/utils/poi/ExcelHandlerAdapter.java new file mode 100644 index 0000000..1c9b042 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/poi/ExcelHandlerAdapter.java @@ -0,0 +1,19 @@ +package com.bnhz.common.utils.poi; + +/** + * Excel数据格式处理适配器 + * + * @author ruoyi + */ +public interface ExcelHandlerAdapter +{ + /** + * 格式化 + * + * @param value 单元格数据值 + * @param args excel注解args参数组 + * + * @return 处理后的值 + */ + Object format(Object value, String[] args); +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/poi/ExcelUtil.java b/bnhz-common/src/main/java/com/bnhz/common/utils/poi/ExcelUtil.java new file mode 100644 index 0000000..4c436cc --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/poi/ExcelUtil.java @@ -0,0 +1,1734 @@ +package com.bnhz.common.utils.poi; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.RegExUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.poi.hssf.usermodel.HSSFClientAnchor; +import org.apache.poi.hssf.usermodel.HSSFPicture; +import org.apache.poi.hssf.usermodel.HSSFPictureData; +import org.apache.poi.hssf.usermodel.HSSFShape; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ooxml.POIXMLDocumentPart; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationConstraint; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Name; +import org.apache.poi.ss.usermodel.PictureData; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFDataValidation; +import org.apache.poi.xssf.usermodel.XSSFDrawing; +import org.apache.poi.xssf.usermodel.XSSFPicture; +import org.apache.poi.xssf.usermodel.XSSFShape; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.bnhz.common.annotation.Excel; +import com.bnhz.common.annotation.Excel.ColumnType; +import com.bnhz.common.annotation.Excel.Type; +import com.bnhz.common.annotation.Excels; +import com.bnhz.common.config.DaQiConfig; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.text.Convert; +import com.bnhz.common.exception.UtilException; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.DictUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.file.FileTypeUtils; +import com.bnhz.common.utils.file.FileUtils; +import com.bnhz.common.utils.file.ImageUtils; +import com.bnhz.common.utils.reflect.ReflectUtils; + +/** + * Excel相关处理 + * + * @author ruoyi + */ +public class ExcelUtil +{ + private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class); + + public static final String FORMULA_REGEX_STR = "=|-|\\+|@"; + + public static final String[] FORMULA_STR = { "=", "-", "+", "@" }; + + /** + * Excel sheet最大行数,默认65536 + */ + public static final int sheetSize = 65536; + + /** + * 工作表名称 + */ + private String sheetName; + + /** + * 导出类型(EXPORT:导出数据;IMPORT:导入模板) + */ + private Type type; + + /** + * 工作薄对象 + */ + private Workbook wb; + + /** + * 工作表对象 + */ + private Sheet sheet; + + /** + * 样式列表 + */ + private Map styles; + + /** + * 导入导出数据列表 + */ + private List list; + + /** + * 注解列表 + */ + private List fields; + + /** + * 当前行号 + */ + private int rownum; + + /** + * 标题 + */ + private String title; + + /** + * 最大高度 + */ + private short maxHeight; + + /** + * 合并后最后行数 + */ + private int subMergedLastRowNum = 0; + + /** + * 合并后开始行数 + */ + private int subMergedFirstRowNum = 1; + + /** + * 对象的子列表方法 + */ + private Method subMethod; + + /** + * 对象的子列表属性 + */ + private List subFields; + + /** + * 统计列表 + */ + private Map statistics = new HashMap(); + + /** + * 数字格式 + */ + private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00"); + + /** + * 实体对象 + */ + public Class clazz; + + /** + * 需要排除列属性 + */ + public String[] excludeFields; + + public ExcelUtil(Class clazz) + { + this.clazz = clazz; + } + + /** + * 隐藏Excel中列属性 + * + * @param fields 列属性名 示例[单个"name"/多个"id","name"] + * @throws Exception + */ + public void hideColumn(String... fields) + { + this.excludeFields = fields; + } + + public void init(List list, String sheetName, String title, Type type) + { + if (list == null) + { + list = new ArrayList(); + } + this.list = list; + this.sheetName = sheetName; + this.type = type; + this.title = title; + createExcelField(); + createWorkbook(); + createTitle(); + createSubHead(); + } + + /** + * 创建excel第一行标题 + */ + public void createTitle() + { + if (StringUtils.isNotEmpty(title)) + { + subMergedFirstRowNum++; + subMergedLastRowNum++; + int titleLastCol = this.fields.size() - 1; + if (isSubList()) + { + titleLastCol = titleLastCol + subFields.size() - 1; + } + Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0); + titleRow.setHeightInPoints(30); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellStyle(styles.get("title")); + titleCell.setCellValue(title); + sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(), titleLastCol)); + } + } + + /** + * 创建对象的子列表名称 + */ + public void createSubHead() + { + if (isSubList()) + { + subMergedFirstRowNum++; + subMergedLastRowNum++; + Row subRow = sheet.createRow(rownum); + int excelNum = 0; + for (Object[] objects : fields) + { + Excel attr = (Excel) objects[1]; + Cell headCell1 = subRow.createCell(excelNum); + headCell1.setCellValue(attr.name()); + headCell1.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + excelNum++; + } + int headFirstRow = excelNum - 1; + int headLastRow = headFirstRow + subFields.size() - 1; + if (headLastRow > headFirstRow) + { + sheet.addMergedRegion(new CellRangeAddress(rownum, rownum, headFirstRow, headLastRow)); + } + rownum++; + } + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(InputStream is) throws Exception + { + return importExcel(is, 0); + } + + /** + * 对excel表单默认第一个索引名转换成list + * + * @param is 输入流 + * @param titleNum 标题占用行数 + * @return 转换后集合 + */ + public List importExcel(InputStream is, int titleNum) throws Exception + { + return importExcel(StringUtils.EMPTY, is, titleNum); + } + + /** + * 对excel表单指定表格索引名转换成list + * + * @param sheetName 表格索引名 + * @param titleNum 标题占用行数 + * @param is 输入流 + * @return 转换后集合 + */ + public List importExcel(String sheetName, InputStream is, int titleNum) throws Exception + { + this.type = Type.IMPORT; + this.wb = WorkbookFactory.create(is); + List list = new ArrayList(); + // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet + Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0); + if (sheet == null) + { + throw new IOException("文件sheet不存在"); + } + boolean isXSSFWorkbook = !(wb instanceof HSSFWorkbook); + Map pictures; + if (isXSSFWorkbook) + { + pictures = getSheetPictures07((XSSFSheet) sheet, (XSSFWorkbook) wb); + } + else + { + pictures = getSheetPictures03((HSSFSheet) sheet, (HSSFWorkbook) wb); + } + // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1 + int rows = sheet.getLastRowNum(); + + if (rows > 0) + { + // 定义一个map用于存放excel列的序号和field. + Map cellMap = new HashMap(); + // 获取表头 + Row heard = sheet.getRow(titleNum); + for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++) + { + Cell cell = heard.getCell(i); + if (StringUtils.isNotNull(cell)) + { + String value = this.getCellValue(heard, i).toString(); + cellMap.put(value, i); + } + else + { + cellMap.put(null, i); + } + } + // 有数据时才处理 得到类的所有field. + List fields = this.getFields(); + Map fieldsMap = new HashMap(); + for (Object[] objects : fields) + { + Excel attr = (Excel) objects[1]; + Integer column = cellMap.get(attr.name()); + if (column != null) + { + fieldsMap.put(column, objects); + } + } + for (int i = titleNum + 1; i <= rows; i++) + { + // 从第2行开始取数据,默认第一行是表头. + Row row = sheet.getRow(i); + // 判断当前行是否是空行 + if (isRowEmpty(row)) + { + continue; + } + T entity = null; + for (Map.Entry entry : fieldsMap.entrySet()) + { + Object val = this.getCellValue(row, entry.getKey()); + + // 如果不存在实例则新建. + entity = (entity == null ? clazz.newInstance() : entity); + // 从map中得到对应列的field. + Field field = (Field) entry.getValue()[0]; + Excel attr = (Excel) entry.getValue()[1]; + // 取得类型,并根据对象类型设置值. + Class fieldType = field.getType(); + if (String.class == fieldType) + { + String s = Convert.toStr(val); + if (StringUtils.endsWith(s, ".0")) + { + val = StringUtils.substringBefore(s, ".0"); + } + else + { + String dateFormat = field.getAnnotation(Excel.class).dateFormat(); + if (StringUtils.isNotEmpty(dateFormat)) + { + val = parseDateToStr(dateFormat, val); + } + else + { + val = Convert.toStr(val); + } + } + } + else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toInt(val); + } + else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val))) + { + val = Convert.toLong(val); + } + else if (Double.TYPE == fieldType || Double.class == fieldType) + { + val = Convert.toDouble(val); + } + else if (Float.TYPE == fieldType || Float.class == fieldType) + { + val = Convert.toFloat(val); + } + else if (BigDecimal.class == fieldType) + { + val = Convert.toBigDecimal(val); + } + else if (Date.class == fieldType) + { + if (val instanceof String) + { + val = DateUtils.parseDate(val); + } + else if (val instanceof Double) + { + val = DateUtil.getJavaDate((Double) val); + } + } + else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) + { + val = Convert.toBool(val, false); + } + if (StringUtils.isNotNull(fieldType)) + { + String propertyName = field.getName(); + if (StringUtils.isNotEmpty(attr.targetAttr())) + { + propertyName = field.getName() + "." + attr.targetAttr(); + } + else if (StringUtils.isNotEmpty(attr.readConverterExp())) + { + val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator()); + } + else if (StringUtils.isNotEmpty(attr.dictType())) + { + val = reverseDictByExp(Convert.toStr(val), attr.dictType(), attr.separator()); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + val = dataFormatHandlerAdapter(val, attr); + } + else if (ColumnType.IMAGE == attr.cellType() && StringUtils.isNotEmpty(pictures)) + { + PictureData image = pictures.get(row.getRowNum() + "_" + entry.getKey()); + if (image == null) + { + val = ""; + } + else + { + byte[] data = image.getData(); + val = FileUtils.writeImportBytes(data); + } + } + ReflectUtils.invokeSetter(entity, propertyName, val); + } + } + list.add(entity); + } + } + return list; + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName) + { + return exportExcel(list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult exportExcel(List list, String sheetName, String title) + { + this.init(list, sheetName, title, Type.EXPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName) + { + exportExcel(response, list, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param response 返回数据 + * @param list 导出数据集合 + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void exportExcel(HttpServletResponse response, List list, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(list, sheetName, title, Type.EXPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName) + { + return importTemplateExcel(sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public AjaxResult importTemplateExcel(String sheetName, String title) + { + this.init(null, sheetName, title, Type.IMPORT); + return exportExcel(); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName) + { + importTemplateExcel(response, sheetName, StringUtils.EMPTY); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @param sheetName 工作表的名称 + * @param title 标题 + * @return 结果 + */ + public void importTemplateExcel(HttpServletResponse response, String sheetName, String title) + { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + this.init(null, sheetName, title, Type.IMPORT); + exportExcel(response); + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public void exportExcel(HttpServletResponse response) + { + try + { + writeSheet(); + wb.write(response.getOutputStream()); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + } + finally + { + IOUtils.closeQuietly(wb); + } + } + + /** + * 对list数据源将其里面的数据导入到excel表单 + * + * @return 结果 + */ + public AjaxResult exportExcel() + { + OutputStream out = null; + try + { + writeSheet(); + String filename = encodingFilename(sheetName); + out = new FileOutputStream(getAbsoluteFile(filename)); + wb.write(out); + return AjaxResult.success(filename); + } + catch (Exception e) + { + log.error("导出Excel异常{}", e.getMessage()); + throw new UtilException("导出Excel失败,请联系网站管理员!"); + } + finally + { + IOUtils.closeQuietly(wb); + IOUtils.closeQuietly(out); + } + } + + /** + * 创建写入数据到Sheet + */ + public void writeSheet() + { + // 取出一共有多少个sheet. + int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize)); + for (int index = 0; index < sheetNo; index++) + { + createSheet(sheetNo, index); + + // 产生一行 + Row row = sheet.createRow(rownum); + int column = 0; + // 写入各个字段的列头名称 + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType())) + { + for (Field subField : subFields) + { + Excel subExcel = subField.getAnnotation(Excel.class); + this.createHeadCell(subExcel, row, column++); + } + } + else + { + this.createHeadCell(excel, row, column++); + } + } + if (Type.EXPORT.equals(type)) + { + fillExcelData(index, row); + addStatisticsRow(); + } + } + } + + /** + * 填充excel数据 + * + * @param index 序号 + * @param row 单元格行 + */ + @SuppressWarnings("unchecked") + public void fillExcelData(int index, Row row) + { + int startNo = index * sheetSize; + int endNo = Math.min(startNo + sheetSize, list.size()); + int rowNo = (1 + rownum) - startNo; + for (int i = startNo; i < endNo; i++) + { + rowNo = isSubList() ? (i > 1 ? rowNo + 1 : rowNo + i) : i + 1 + rownum - startNo; + row = sheet.createRow(rowNo); + // 得到导出对象. + T vo = (T) list.get(i); + Collection subList = null; + if (isSubList()) + { + if (isSubListValue(vo)) + { + subList = getListCellValue(vo); + subMergedLastRowNum = subMergedLastRowNum + subList.size(); + } + else + { + subMergedFirstRowNum++; + subMergedLastRowNum++; + } + } + int column = 0; + for (Object[] os : fields) + { + Field field = (Field) os[0]; + Excel excel = (Excel) os[1]; + if (Collection.class.isAssignableFrom(field.getType()) && StringUtils.isNotNull(subList)) + { + boolean subFirst = false; + for (Object obj : subList) + { + if (subFirst) + { + rowNo++; + row = sheet.createRow(rowNo); + } + List subFields = FieldUtils.getFieldsListWithAnnotation(obj.getClass(), Excel.class); + int subIndex = 0; + for (Field subField : subFields) + { + if (subField.isAnnotationPresent(Excel.class)) + { + subField.setAccessible(true); + Excel attr = subField.getAnnotation(Excel.class); + this.addCell(attr, row, (T) obj, subField, column + subIndex); + } + subIndex++; + } + subFirst = true; + } + this.subMergedFirstRowNum = this.subMergedFirstRowNum + subList.size(); + } + else + { + this.addCell(excel, row, vo, field, column++); + } + } + } + } + + /** + * 创建表格样式 + * + * @param wb 工作薄对象 + * @return 样式列表 + */ + private Map createStyles(Workbook wb) + { + // 写入各条记录,每条记录对应excel表中的一行 + Map styles = new HashMap(); + CellStyle style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font titleFont = wb.createFont(); + titleFont.setFontName("Arial"); + titleFont.setFontHeightInPoints((short) 16); + titleFont.setBold(true); + style.setFont(titleFont); + styles.put("title", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + style.setFont(dataFont); + styles.put("data", style); + + style = wb.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + Font totalFont = wb.createFont(); + totalFont.setFontName("Arial"); + totalFont.setFontHeightInPoints((short) 10); + style.setFont(totalFont); + styles.put("total", style); + + styles.putAll(annotationHeaderStyles(wb, styles)); + + styles.putAll(annotationDataStyles(wb)); + + return styles; + } + + /** + * 根据Excel注解创建表格头样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationHeaderStyles(Workbook wb, Map styles) + { + Map headerStyles = new HashMap(); + for (Object[] os : fields) + { + Excel excel = (Excel) os[1]; + String key = StringUtils.format("header_{}_{}", excel.headerColor(), excel.headerBackgroundColor()); + if (!headerStyles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.cloneStyleFrom(styles.get("data")); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setFillForegroundColor(excel.headerBackgroundColor().index); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font headerFont = wb.createFont(); + headerFont.setFontName("Arial"); + headerFont.setFontHeightInPoints((short) 10); + headerFont.setBold(true); + headerFont.setColor(excel.headerColor().index); + style.setFont(headerFont); + headerStyles.put(key, style); + } + } + return headerStyles; + } + + /** + * 根据Excel注解创建表格列样式 + * + * @param wb 工作薄对象 + * @return 自定义样式列表 + */ + private Map annotationDataStyles(Workbook wb) + { + Map styles = new HashMap(); + for (Object[] os : fields) + { + Excel excel = (Excel) os[1]; + String key = StringUtils.format("data_{}_{}_{}", excel.align(), excel.color(), excel.backgroundColor()); + if (!styles.containsKey(key)) + { + CellStyle style = wb.createCellStyle(); + style.setAlignment(excel.align()); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderRight(BorderStyle.THIN); + style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderLeft(BorderStyle.THIN); + style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderTop(BorderStyle.THIN); + style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setBorderBottom(BorderStyle.THIN); + style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setFillForegroundColor(excel.backgroundColor().getIndex()); + Font dataFont = wb.createFont(); + dataFont.setFontName("Arial"); + dataFont.setFontHeightInPoints((short) 10); + dataFont.setColor(excel.color().index); + style.setFont(dataFont); + styles.put(key, style); + } + } + return styles; + } + + /** + * 创建单元格 + */ + public Cell createHeadCell(Excel attr, Row row, int column) + { + // 创建列 + Cell cell = row.createCell(column); + // 写入列信息 + cell.setCellValue(attr.name()); + setDataValidation(attr, row, column); + cell.setCellStyle(styles.get(StringUtils.format("header_{}_{}", attr.headerColor(), attr.headerBackgroundColor()))); + if (isSubList()) + { + // 填充默认样式,防止合并单元格样式失效 + sheet.setDefaultColumnStyle(column, styles.get(StringUtils.format("data_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor()))); + if (attr.needMerge()) + { + sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum, column, column)); + } + } + return cell; + } + + /** + * 设置单元格信息 + * + * @param value 单元格值 + * @param attr 注解相关 + * @param cell 单元格信息 + */ + public void setCellVo(Object value, Excel attr, Cell cell) + { + if (ColumnType.STRING == attr.cellType()) + { + String cellValue = Convert.toStr(value); + // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。 + if (StringUtils.startsWithAny(cellValue, FORMULA_STR)) + { + cellValue = RegExUtils.replaceFirst(cellValue, FORMULA_REGEX_STR, "\t$0"); + } + if (value instanceof Collection && StringUtils.equals("[]", cellValue)) + { + cellValue = StringUtils.EMPTY; + } + cell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix()); + } + else if (ColumnType.NUMERIC == attr.cellType()) + { + if (StringUtils.isNotNull(value)) + { + cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value)); + } + } + else if (ColumnType.IMAGE == attr.cellType()) + { + ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1); + String imagePath = Convert.toStr(value); + if (StringUtils.isNotEmpty(imagePath)) + { + byte[] data = ImageUtils.getImage(imagePath); + getDrawingPatriarch(cell.getSheet()).createPicture(anchor, + cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); + } + } + } + + /** + * 获取画布 + */ + public static Drawing getDrawingPatriarch(Sheet sheet) + { + if (sheet.getDrawingPatriarch() == null) + { + sheet.createDrawingPatriarch(); + } + return sheet.getDrawingPatriarch(); + } + + /** + * 获取图片类型,设置图片插入类型 + */ + public int getImageType(byte[] value) + { + String type = FileTypeUtils.getFileExtendName(value); + if ("JPG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_JPEG; + } + else if ("PNG".equalsIgnoreCase(type)) + { + return Workbook.PICTURE_TYPE_PNG; + } + return Workbook.PICTURE_TYPE_JPEG; + } + + /** + * 创建表格样式 + */ + public void setDataValidation(Excel attr, Row row, int column) + { + if (attr.name().indexOf("注:") >= 0) + { + sheet.setColumnWidth(column, 6000); + } + else + { + // 设置列宽 + sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256)); + } + if (StringUtils.isNotEmpty(attr.prompt()) || attr.combo().length > 0) + { + if (attr.combo().length > 15 || StringUtils.join(attr.combo()).length() > 255) + { + // 如果下拉数大于15或字符串长度大于255,则使用一个新sheet存储,避免生成的模板下拉值获取不到 + setXSSFValidationWithHidden(sheet, attr.combo(), attr.prompt(), 1, 100, column, column); + } + else + { + // 提示信息或只能选择不能输入的列内容. + setPromptOrValidation(sheet, attr.combo(), attr.prompt(), 1, 100, column, column); + } + } + } + + /** + * 添加单元格 + */ + public Cell addCell(Excel attr, Row row, T vo, Field field, int column) + { + Cell cell = null; + try + { + // 设置行高 + row.setHeight(maxHeight); + // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列. + if (attr.isExport()) + { + // 创建cell + cell = row.createCell(column); + if (isSubListValue(vo) && getListCellValue(vo).size() > 1 && attr.needMerge()) + { + CellRangeAddress cellAddress = new CellRangeAddress(subMergedFirstRowNum, subMergedLastRowNum, column, column); + sheet.addMergedRegion(cellAddress); + } + cell.setCellStyle(styles.get(StringUtils.format("data_{}_{}_{}", attr.align(), attr.color(), attr.backgroundColor()))); + + // 用于读取对象中的属性 + Object value = getTargetValue(vo, field, attr); + String dateFormat = attr.dateFormat(); + String readConverterExp = attr.readConverterExp(); + String separator = attr.separator(); + String dictType = attr.dictType(); + if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value)) + { + cell.setCellValue(parseDateToStr(dateFormat, value)); + } + else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value)) + { + cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator)); + } + else if (StringUtils.isNotEmpty(dictType) && StringUtils.isNotNull(value)) + { + cell.setCellValue(convertDictByExp(Convert.toStr(value), dictType, separator)); + } + else if (value instanceof BigDecimal && -1 != attr.scale()) + { + cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).doubleValue()); + } + else if (!attr.handler().equals(ExcelHandlerAdapter.class)) + { + cell.setCellValue(dataFormatHandlerAdapter(value, attr)); + } + else + { + // 设置列类型 + setCellVo(value, attr, cell); + } + addStatisticsData(column, Convert.toStr(value), attr); + } + } + catch (Exception e) + { + log.error("导出Excel失败{}", e); + } + return cell; + } + + /** + * 设置 POI XSSFSheet 单元格提示或选择框 + * + * @param sheet 表单 + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setPromptOrValidation(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, + int firstCol, int endCol) + { + DataValidationHelper helper = sheet.getDataValidationHelper(); + DataValidationConstraint constraint = textlist.length > 0 ? helper.createExplicitListConstraint(textlist) : helper.createCustomConstraint("DD1"); + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + sheet.addValidationData(dataValidation); + } + + /** + * 设置某些列的值只能输入预制的数据,显示下拉框(兼容超出一定数量的下拉框). + * + * @param sheet 要设置的sheet. + * @param textlist 下拉框显示的内容 + * @param promptContent 提示内容 + * @param firstRow 开始行 + * @param endRow 结束行 + * @param firstCol 开始列 + * @param endCol 结束列 + */ + public void setXSSFValidationWithHidden(Sheet sheet, String[] textlist, String promptContent, int firstRow, int endRow, int firstCol, int endCol) + { + String hideSheetName = "combo_" + firstCol + "_" + endCol; + Sheet hideSheet = wb.createSheet(hideSheetName); // 用于存储 下拉菜单数据 + for (int i = 0; i < textlist.length; i++) + { + hideSheet.createRow(i).createCell(0).setCellValue(textlist[i]); + } + // 创建名称,可被其他单元格引用 + Name name = wb.createName(); + name.setNameName(hideSheetName + "_data"); + name.setRefersToFormula(hideSheetName + "!$A$1:$A$" + textlist.length); + DataValidationHelper helper = sheet.getDataValidationHelper(); + // 加载下拉列表内容 + DataValidationConstraint constraint = helper.createFormulaListConstraint(hideSheetName + "_data"); + // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 + CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol); + // 数据有效性对象 + DataValidation dataValidation = helper.createValidation(constraint, regions); + if (StringUtils.isNotEmpty(promptContent)) + { + // 如果设置了提示信息则鼠标放上去提示 + dataValidation.createPromptBox("", promptContent); + dataValidation.setShowPromptBox(true); + } + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) + { + dataValidation.setSuppressDropDownArrow(true); + dataValidation.setShowErrorBox(true); + } + else + { + dataValidation.setSuppressDropDownArrow(false); + } + + sheet.addValidationData(dataValidation); + // 设置hiddenSheet隐藏 + wb.setSheetHidden(wb.getSheetIndex(hideSheet), true); + } + + /** + * 解析导出值 0=男,1=女,2=未知 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String convertByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(","); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[0].equals(value)) + { + propertyString.append(itemArray[1] + separator); + break; + } + } + } + else + { + if (itemArray[0].equals(propertyValue)) + { + return itemArray[1]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 反向解析值 男=0,女=1,未知=2 + * + * @param propertyValue 参数值 + * @param converterExp 翻译注解 + * @param separator 分隔符 + * @return 解析后值 + */ + public static String reverseByExp(String propertyValue, String converterExp, String separator) + { + StringBuilder propertyString = new StringBuilder(); + String[] convertSource = converterExp.split(","); + for (String item : convertSource) + { + String[] itemArray = item.split("="); + if (StringUtils.containsAny(propertyValue, separator)) + { + for (String value : propertyValue.split(separator)) + { + if (itemArray[1].equals(value)) + { + propertyString.append(itemArray[0] + separator); + break; + } + } + } + else + { + if (itemArray[1].equals(propertyValue)) + { + return itemArray[0]; + } + } + } + return StringUtils.stripEnd(propertyString.toString(), separator); + } + + /** + * 解析字典值 + * + * @param dictValue 字典值 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典标签 + */ + public static String convertDictByExp(String dictValue, String dictType, String separator) + { + return DictUtils.getDictLabel(dictType, dictValue, separator); + } + + /** + * 反向解析值字典值 + * + * @param dictLabel 字典标签 + * @param dictType 字典类型 + * @param separator 分隔符 + * @return 字典值 + */ + public static String reverseDictByExp(String dictLabel, String dictType, String separator) + { + return DictUtils.getDictValue(dictType, dictLabel, separator); + } + + /** + * 数据处理器 + * + * @param value 数据值 + * @param excel 数据注解 + * @return + */ + public String dataFormatHandlerAdapter(Object value, Excel excel) + { + try + { + Object instance = excel.handler().newInstance(); + Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class }); + value = formatMethod.invoke(instance, value, excel.args()); + } + catch (Exception e) + { + log.error("不能格式化数据 " + excel.handler(), e.getMessage()); + } + return Convert.toStr(value); + } + + /** + * 合计统计信息 + */ + private void addStatisticsData(Integer index, String text, Excel entity) + { + if (entity != null && entity.isStatistics()) + { + Double temp = 0D; + if (!statistics.containsKey(index)) + { + statistics.put(index, temp); + } + try + { + temp = Double.valueOf(text); + } + catch (NumberFormatException e) + { + } + statistics.put(index, statistics.get(index) + temp); + } + } + + /** + * 创建统计行 + */ + public void addStatisticsRow() + { + if (statistics.size() > 0) + { + Row row = sheet.createRow(sheet.getLastRowNum() + 1); + Set keys = statistics.keySet(); + Cell cell = row.createCell(0); + cell.setCellStyle(styles.get("total")); + cell.setCellValue("合计"); + + for (Integer key : keys) + { + cell = row.createCell(key); + cell.setCellStyle(styles.get("total")); + cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key))); + } + statistics.clear(); + } + } + + /** + * 编码文件名 + */ + public String encodingFilename(String filename) + { + filename = UUID.randomUUID().toString() + "_" + filename + ".xlsx"; + return filename; + } + + /** + * 获取下载路径 + * + * @param filename 文件名称 + */ + public String getAbsoluteFile(String filename) + { + String downloadPath = DaQiConfig.getDownloadPath() + filename; + File desc = new File(downloadPath); + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + return downloadPath; + } + + /** + * 获取bean中的属性值 + * + * @param vo 实体对象 + * @param field 字段 + * @param excel 注解 + * @return 最终的属性值 + * @throws Exception + */ + private Object getTargetValue(T vo, Field field, Excel excel) throws Exception + { + Object o = field.get(vo); + if (StringUtils.isNotEmpty(excel.targetAttr())) + { + String target = excel.targetAttr(); + if (target.contains(".")) + { + String[] targets = target.split("[.]"); + for (String name : targets) + { + o = getValue(o, name); + } + } + else + { + o = getValue(o, target); + } + } + return o; + } + + /** + * 以类的属性的get方法方法形式获取值 + * + * @param o + * @param name + * @return value + * @throws Exception + */ + private Object getValue(Object o, String name) throws Exception + { + if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name)) + { + Class clazz = o.getClass(); + Field field = clazz.getDeclaredField(name); + field.setAccessible(true); + o = field.get(o); + } + return o; + } + + /** + * 得到所有定义字段 + */ + private void createExcelField() + { + this.fields = getFields(); + this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList()); + this.maxHeight = getRowHeight(); + } + + /** + * 获取字段注解信息 + */ + public List getFields() + { + List fields = new ArrayList(); + List tempFields = new ArrayList<>(); + tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields())); + tempFields.addAll(Arrays.asList(clazz.getDeclaredFields())); + for (Field field : tempFields) + { + if (!ArrayUtils.contains(this.excludeFields, field.getName())) + { + // 单注解 + if (field.isAnnotationPresent(Excel.class)) + { + Excel attr = field.getAnnotation(Excel.class); + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) + { + field.setAccessible(true); + fields.add(new Object[] { field, attr }); + } + if (Collection.class.isAssignableFrom(field.getType())) + { + subMethod = getSubMethod(field.getName(), clazz); + ParameterizedType pt = (ParameterizedType) field.getGenericType(); + Class subClass = (Class) pt.getActualTypeArguments()[0]; + this.subFields = FieldUtils.getFieldsListWithAnnotation(subClass, Excel.class); + } + } + + // 多注解 + if (field.isAnnotationPresent(Excels.class)) + { + Excels attrs = field.getAnnotation(Excels.class); + Excel[] excels = attrs.value(); + for (Excel attr : excels) + { + if (attr != null && (attr.type() == Type.ALL || attr.type() == type)) + { + field.setAccessible(true); + fields.add(new Object[] { field, attr }); + } + } + } + } + } + return fields; + } + + /** + * 根据注解获取最大行高 + */ + public short getRowHeight() + { + double maxHeight = 0; + for (Object[] os : this.fields) + { + Excel excel = (Excel) os[1]; + maxHeight = Math.max(maxHeight, excel.height()); + } + return (short) (maxHeight * 20); + } + + /** + * 创建一个工作簿 + */ + public void createWorkbook() + { + this.wb = new SXSSFWorkbook(500); + this.sheet = wb.createSheet(); + wb.setSheetName(0, sheetName); + this.styles = createStyles(wb); + } + + /** + * 创建工作表 + * + * @param sheetNo sheet数量 + * @param index 序号 + */ + public void createSheet(int sheetNo, int index) + { + // 设置工作表的名称. + if (sheetNo > 1 && index > 0) + { + this.sheet = wb.createSheet(); + this.createTitle(); + wb.setSheetName(index, sheetName + index); + } + } + + /** + * 获取单元格值 + * + * @param row 获取的行 + * @param column 获取单元格列号 + * @return 单元格值 + */ + public Object getCellValue(Row row, int column) + { + if (row == null) + { + return row; + } + Object val = ""; + try + { + Cell cell = row.getCell(column); + if (StringUtils.isNotNull(cell)) + { + if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) + { + val = cell.getNumericCellValue(); + if (DateUtil.isCellDateFormatted(cell)) + { + val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换 + } + else + { + if ((Double) val % 1 != 0) + { + val = new BigDecimal(val.toString()); + } + else + { + val = new DecimalFormat("0").format(val); + } + } + } + else if (cell.getCellType() == CellType.STRING) + { + val = cell.getStringCellValue(); + } + else if (cell.getCellType() == CellType.BOOLEAN) + { + val = cell.getBooleanCellValue(); + } + else if (cell.getCellType() == CellType.ERROR) + { + val = cell.getErrorCellValue(); + } + + } + } + catch (Exception e) + { + return val; + } + return val; + } + + /** + * 判断是否是空行 + * + * @param row 判断的行 + * @return + */ + private boolean isRowEmpty(Row row) + { + if (row == null) + { + return true; + } + for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) + { + Cell cell = row.getCell(i); + if (cell != null && cell.getCellType() != CellType.BLANK) + { + return false; + } + } + return true; + } + + /** + * 获取Excel2003图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map getSheetPictures03(HSSFSheet sheet, HSSFWorkbook workbook) + { + Map sheetIndexPicMap = new HashMap(); + List pictures = workbook.getAllPictures(); + if (!pictures.isEmpty()) + { + for (HSSFShape shape : sheet.getDrawingPatriarch().getChildren()) + { + HSSFClientAnchor anchor = (HSSFClientAnchor) shape.getAnchor(); + if (shape instanceof HSSFPicture) + { + HSSFPicture pic = (HSSFPicture) shape; + int pictureIndex = pic.getPictureIndex() - 1; + HSSFPictureData picData = pictures.get(pictureIndex); + String picIndex = String.valueOf(anchor.getRow1()) + "_" + String.valueOf(anchor.getCol1()); + sheetIndexPicMap.put(picIndex, picData); + } + } + return sheetIndexPicMap; + } + else + { + return sheetIndexPicMap; + } + } + + /** + * 获取Excel2007图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map getSheetPictures07(XSSFSheet sheet, XSSFWorkbook workbook) + { + Map sheetIndexPicMap = new HashMap(); + for (POIXMLDocumentPart dr : sheet.getRelations()) + { + if (dr instanceof XSSFDrawing) + { + XSSFDrawing drawing = (XSSFDrawing) dr; + List shapes = drawing.getShapes(); + for (XSSFShape shape : shapes) + { + if (shape instanceof XSSFPicture) + { + XSSFPicture pic = (XSSFPicture) shape; + XSSFClientAnchor anchor = pic.getPreferredSize(); + CTMarker ctMarker = anchor.getFrom(); + String picIndex = ctMarker.getRow() + "_" + ctMarker.getCol(); + sheetIndexPicMap.put(picIndex, pic.getPictureData()); + } + } + } + } + return sheetIndexPicMap; + } + + /** + * 格式化不同类型的日期对象 + * + * @param dateFormat 日期格式 + * @param val 被格式化的日期对象 + * @return 格式化后的日期字符 + */ + public String parseDateToStr(String dateFormat, Object val) + { + if (val == null) + { + return ""; + } + String str; + if (val instanceof Date) + { + str = DateUtils.parseDateToStr(dateFormat, (Date) val); + } + else if (val instanceof LocalDateTime) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDateTime) val)); + } + else if (val instanceof LocalDate) + { + str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDate) val)); + } + else + { + str = val.toString(); + } + return str; + } + + /** + * 是否有对象的子列表 + */ + public boolean isSubList() + { + return StringUtils.isNotNull(subFields) && subFields.size() > 0; + } + + /** + * 是否有对象的子列表,集合不为空 + */ + public boolean isSubListValue(T vo) + { + return StringUtils.isNotNull(subFields) && subFields.size() > 0 && StringUtils.isNotNull(getListCellValue(vo)) && getListCellValue(vo).size() > 0; + } + + /** + * 获取集合的值 + */ + public Collection getListCellValue(Object obj) + { + Object value; + try + { + value = subMethod.invoke(obj, new Object[] {}); + } + catch (Exception e) + { + return new ArrayList(); + } + return (Collection) value; + } + + /** + * 获取对象的子列表方法 + * + * @param name 名称 + * @param pojoClass 类对象 + * @return 子列表方法 + */ + public Method getSubMethod(String name, Class pojoClass) + { + StringBuffer getMethodName = new StringBuffer("get"); + getMethodName.append(name.substring(0, 1).toUpperCase()); + getMethodName.append(name.substring(1)); + Method method = null; + try + { + method = pojoClass.getMethod(getMethodName.toString(), new Class[] {}); + } + catch (Exception e) + { + log.error("获取对象异常{}", e.getMessage()); + } + return method; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/reflect/ReflectUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/reflect/ReflectUtils.java new file mode 100644 index 0000000..0d8e04a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/reflect/ReflectUtils.java @@ -0,0 +1,494 @@ +package com.bnhz.common.utils.reflect; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import com.bnhz.common.annotation.Length; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.poi.ss.usermodel.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.bnhz.common.core.text.Convert; +import com.bnhz.common.utils.DateUtils; + +/** + * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数. + * + * @author ruoyi + */ +@SuppressWarnings("rawtypes") +public class ReflectUtils +{ + private static final String SETTER_PREFIX = "set"; + + private static final String GETTER_PREFIX = "get"; + + private static final String CGLIB_CLASS_SEPARATOR = "$$"; + + private static Logger logger = LoggerFactory.getLogger(ReflectUtils.class); + + /** + * 调用Getter方法. + * 支持多级,如:对象名.对象名.方法 + */ + @SuppressWarnings("unchecked") + public static E invokeGetter(Object obj, String propertyName) + { + Object object = obj; + for (String name : StringUtils.split(propertyName, ".")) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + return (E) object; + } + + /** + * 调用Setter方法, 仅匹配方法名。 + * 支持多级,如:对象名.对象名.方法 + */ + public static void invokeSetter(Object obj, String propertyName, E value) + { + Object object = obj; + String[] names = StringUtils.split(propertyName, "."); + for (int i = 0; i < names.length; i++) + { + if (i < names.length - 1) + { + String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(names[i]); + object = invokeMethod(object, getterMethodName, new Class[] {}, new Object[] {}); + } + else + { + String setterMethodName = SETTER_PREFIX + StringUtils.capitalize(names[i]); + invokeMethodByName(object, setterMethodName, new Object[] { value }); + } + } + } + + /** + * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数. + */ + @SuppressWarnings("unchecked") + public static E getFieldValue(final Object obj, final String fieldName) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return null; + } + E result = null; + try + { + result = (E) field.get(obj); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常{}", e.getMessage()); + } + return result; + } + + /** + * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数. + */ + public static void setFieldValue(final Object obj, final String fieldName, final E value) + { + Field field = getAccessibleField(obj, fieldName); + if (field == null) + { + // throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 "); + return; + } + try + { + field.set(obj, value); + } + catch (IllegalAccessException e) + { + logger.error("不可能抛出的异常: {}", e.getMessage()); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符. + * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用. + * 同时匹配方法名+参数类型, + */ + @SuppressWarnings("unchecked") + public static E invokeMethod(final Object obj, final String methodName, final Class[] parameterTypes, + final Object[] args) + { + if (obj == null || methodName == null) + { + return null; + } + Method method = getAccessibleMethod(obj, methodName, parameterTypes); + if (method == null) + { + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 直接调用对象方法, 无视private/protected修饰符, + * 用于一次性调用的情况,否则应使用getAccessibleMethodByName()函数获得Method后反复调用. + * 只匹配函数名,如果有多个同名函数调用第一个。 + */ + @SuppressWarnings("unchecked") + public static E invokeMethodByName(final Object obj, final String methodName, final Object[] args) + { + Method method = getAccessibleMethodByName(obj, methodName, args.length); + if (method == null) + { + // 如果为空不报错,直接返回空。 + logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 "); + return null; + } + try + { + // 类型转换(将参数数据类型转换为目标方法参数类型) + Class[] cs = method.getParameterTypes(); + for (int i = 0; i < cs.length; i++) + { + if (args[i] != null && !args[i].getClass().equals(cs[i])) + { + if (cs[i] == String.class) + { + args[i] = Convert.toStr(args[i]); + if (StringUtils.endsWith((String) args[i], ".0")) + { + args[i] = StringUtils.substringBefore((String) args[i], ".0"); + } + } + else if (cs[i] == Integer.class) + { + args[i] = Convert.toInt(args[i]); + } + else if (cs[i] == Long.class) + { + args[i] = Convert.toLong(args[i]); + } + else if (cs[i] == Double.class) + { + args[i] = Convert.toDouble(args[i]); + } + else if (cs[i] == Float.class) + { + args[i] = Convert.toFloat(args[i]); + } + else if (cs[i] == Date.class) + { + if (args[i] instanceof String) + { + args[i] = DateUtils.parseDate(args[i]); + } + else + { + args[i] = DateUtil.getJavaDate((Double) args[i]); + } + } + else if (cs[i] == boolean.class || cs[i] == Boolean.class) + { + args[i] = Convert.toBool(args[i]); + } + } + } + return (E) method.invoke(obj, args); + } + catch (Exception e) + { + String msg = "method: " + method + ", obj: " + obj + ", args: " + args + ""; + throw convertReflectionExceptionToUnchecked(msg, e); + } + } + + /** + * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + */ + public static Field getAccessibleField(final Object obj, final String fieldName) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(fieldName, "fieldName can't be blank"); + for (Class superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) + { + try + { + Field field = superClass.getDeclaredField(fieldName); + makeAccessible(field); + return field; + } + catch (NoSuchFieldException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 匹配函数名+参数类型。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethod(final Object obj, final String methodName, + final Class... parameterTypes) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + try + { + Method method = searchType.getDeclaredMethod(methodName, parameterTypes); + makeAccessible(method); + return method; + } + catch (NoSuchMethodException e) + { + continue; + } + } + return null; + } + + /** + * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问. + * 如向上转型到Object仍无法找到, 返回null. + * 只匹配函数名。 + * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args) + */ + public static Method getAccessibleMethodByName(final Object obj, final String methodName, int argsNum) + { + // 为空不报错。直接返回 null + if (obj == null) + { + return null; + } + Validate.notBlank(methodName, "methodName can't be blank"); + for (Class searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) + { + Method[] methods = searchType.getDeclaredMethods(); + for (Method method : methods) + { + if (method.getName().equals(methodName) && method.getParameterTypes().length == argsNum) + { + makeAccessible(method); + return method; + } + } + } + return null; + } + + /** + * 改变private/protected的方法为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Method method) + { + if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) + && !method.isAccessible()) + { + method.setAccessible(true); + } + } + + /** + * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。 + */ + public static void makeAccessible(Field field) + { + if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) + || Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) + { + field.setAccessible(true); + } + } + + /** + * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处 + * 如无法找到, 返回Object.class. + */ + @SuppressWarnings("unchecked") + public static Class getClassGenricType(final Class clazz) + { + return getClassGenricType(clazz, 0); + } + + /** + * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. + * 如无法找到, 返回Object.class. + */ + public static Class getClassGenricType(final Class clazz, final int index) + { + Type genType = clazz.getGenericSuperclass(); + + if (!(genType instanceof ParameterizedType)) + { + logger.debug(clazz.getSimpleName() + "'s superclass not ParameterizedType"); + return Object.class; + } + + Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); + + if (index >= params.length || index < 0) + { + logger.debug("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + + params.length); + return Object.class; + } + if (!(params[index] instanceof Class)) + { + logger.debug(clazz.getSimpleName() + " not set the actual class on superclass generic parameter"); + return Object.class; + } + + return (Class) params[index]; + } + + public static Class getUserClass(Object instance) + { + if (instance == null) + { + throw new RuntimeException("Instance must not be null"); + } + Class clazz = instance.getClass(); + if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) + { + Class superClass = clazz.getSuperclass(); + if (superClass != null && !Object.class.equals(superClass)) + { + return superClass; + } + } + return clazz; + + } + + /** + * 将反射时的checked exception转换为unchecked exception. + */ + public static RuntimeException convertReflectionExceptionToUnchecked(String msg, Exception e) + { + if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException + || e instanceof NoSuchMethodException) + { + return new IllegalArgumentException(msg, e); + } + else if (e instanceof InvocationTargetException) + { + return new RuntimeException(msg, ((InvocationTargetException) e).getTargetException()); + } + return new RuntimeException(msg, e); + } + + + /** + * 获取对象中所有字段属性的名称和值以及@ApiModelProperty注解信息 + * + * @param obj 待分析的对象 + * @return 包含字段名称、值及注解信息的Map + * @throws IllegalAccessException + */ + public static Map getAllFields(Object obj) throws IllegalAccessException { + Map fieldMap = new HashMap<>(); + + if (obj == null) { + return fieldMap; + } + + Class clazz = obj.getClass(); + // 便利所有父类,获取所有字段 + while (clazz != null) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); // 覆盖Java的访问控制检查 + Object fieldValue = field.get(obj); + String fieldAnnotation = getComment(field); + int length = getLength(field); + FieldInfo fieldInfo = new FieldInfo(fieldValue, fieldAnnotation, length, field.getType()); + fieldMap.put(field.getName(), fieldInfo); + } + clazz = clazz.getSuperclass(); // 获取父类,继续循环 + } + return fieldMap; + } + + /** + * 获取字段的@ApiModelProperty注解内容 + */ + private static String getComment(Field field) { + ApiModelProperty annotation = field.getAnnotation(ApiModelProperty.class); + if (annotation != null) { + return annotation.value(); + } + return null; + } + + /** + * 获取字段的@Length注解内容 + */ + private static int getLength(Field field) { + Length annotation = field.getAnnotation(Length.class); + if (annotation != null) { + return annotation.value(); + } + return 0; + } + + + @Data + @NoArgsConstructor + public static class FieldInfo { + + private Object value; + + private String comment; + + private int length; + + private Class type; + + public FieldInfo(Object value, String comment, int length, Class type) { + this.value = value; + this.comment = comment; + this.length = length; + this.type = type; + } + } + + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/sign/Base64.java b/bnhz-common/src/main/java/com/bnhz/common/utils/sign/Base64.java new file mode 100644 index 0000000..5a6f40e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/sign/Base64.java @@ -0,0 +1,291 @@ +package com.bnhz.common.utils.sign; + +/** + * Base64工具类 + * + * @author ruoyi + */ +public final class Base64 +{ + static private final int BASELENGTH = 128; + static private final int LOOKUPLENGTH = 64; + static private final int TWENTYFOURBITGROUP = 24; + static private final int EIGHTBIT = 8; + static private final int SIXTEENBIT = 16; + static private final int FOURBYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static final private byte[] base64Alphabet = new byte[BASELENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH]; + + static + { + for (int i = 0; i < BASELENGTH; ++i) + { + base64Alphabet[i] = -1; + } + for (int i = 'Z'; i >= 'A'; i--) + { + base64Alphabet[i] = (byte) (i - 'A'); + } + for (int i = 'z'; i >= 'a'; i--) + { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) + { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) + { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) + { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + lookUpBase64Alphabet[62] = (char) '+'; + lookUpBase64Alphabet[63] = (char) '/'; + } + + private static boolean isWhiteSpace(char octect) + { + return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9); + } + + private static boolean isPad(char octect) + { + return (octect == PAD); + } + + private static boolean isData(char octect) + { + return (octect < BASELENGTH && base64Alphabet[octect] != -1); + } + + /** + * Encodes hex octects into Base64 + * + * @param binaryData Array containing binaryData + * @return Encoded Base64 array + */ + public static String encode(byte[] binaryData) + { + if (binaryData == null) + { + return null; + } + + int lengthDataBits = binaryData.length * EIGHTBIT; + if (lengthDataBits == 0) + { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP; + int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char encodedData[] = null; + + encodedData = new char[numberQuartet * 4]; + + byte k = 0, l = 0, b1 = 0, b2 = 0, b3 = 0; + + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) + { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + // form integral number of 6-bit groups + if (fewerThan24bits == EIGHTBIT) + { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } + else if (fewerThan24bits == SIXTEENBIT) + { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + return new String(encodedData); + } + + /** + * Decodes Base64 data into octects + * + * @param encoded string containing Base64 data + * @return Array containind decoded data. + */ + public static byte[] decode(String encoded) + { + if (encoded == null) + { + return null; + } + + char[] base64Data = encoded.toCharArray(); + // remove white spaces + int len = removeWhiteSpace(base64Data); + + if (len % FOURBYTE != 0) + { + return null;// should be divisible by four + } + + int numberQuadruple = (len / FOURBYTE); + + if (numberQuadruple == 0) + { + return new byte[0]; + } + + byte decodedData[] = null; + byte b1 = 0, b2 = 0, b3 = 0, b4 = 0; + char d1 = 0, d2 = 0, d3 = 0, d4 = 0; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) + { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) + { + return null; + } // if found "no data" just return null + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) + { + return null;// if found "no data" just return null + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) + {// Check if they are PAD characters + if (isPad(d3) && isPad(d4)) + { + if ((b2 & 0xf) != 0)// last 4 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } + else if (!isPad(d3) && isPad(d4)) + { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0)// last 2 bits should be zero + { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } + else + { + return null; + } + } + else + { // No PAD e.g 3cQl + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + + } + return decodedData; + } + + /** + * remove WhiteSpace from MIME containing encoded Base64 data. + * + * @param data the byte array of base64 data (with WS) + * @return the new length + */ + private static int removeWhiteSpace(char[] data) + { + if (data == null) + { + return 0; + } + + // count characters that's not whitespace + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) + { + if (!isWhiteSpace(data[i])) + { + data[newSize++] = data[i]; + } + } + return newSize; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/sign/Md5Utils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/sign/Md5Utils.java new file mode 100644 index 0000000..0b9308e --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/sign/Md5Utils.java @@ -0,0 +1,67 @@ +package com.bnhz.common.utils.sign; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Md5加密方法 + * + * @author ruoyi + */ +public class Md5Utils +{ + private static final Logger log = LoggerFactory.getLogger(Md5Utils.class); + + private static byte[] md5(String s) + { + MessageDigest algorithm; + try + { + algorithm = MessageDigest.getInstance("MD5"); + algorithm.reset(); + algorithm.update(s.getBytes("UTF-8")); + byte[] messageDigest = algorithm.digest(); + return messageDigest; + } + catch (Exception e) + { + log.error("MD5 Error...", e); + } + return null; + } + + private static final String toHex(byte hash[]) + { + if (hash == null) + { + return null; + } + StringBuffer buf = new StringBuffer(hash.length * 2); + int i; + + for (i = 0; i < hash.length; i++) + { + if ((hash[i] & 0xff) < 0x10) + { + buf.append("0"); + } + buf.append(Long.toString(hash[i] & 0xff, 16)); + } + return buf.toString(); + } + + public static String hash(String s) + { + try + { + return new String(toHex(md5(s)).getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + } + catch (Exception e) + { + log.error("not supported charset...{}", e); + return s; + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/sign/SignUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/sign/SignUtils.java new file mode 100644 index 0000000..8919104 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/sign/SignUtils.java @@ -0,0 +1,66 @@ +package com.bnhz.common.utils.sign; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +/** + * @author fastb + * @version 1.0 + * @description: 签名验证 + * @date 2024-03-12 15:54 + */ +public class SignUtils { + + /** + * 验证签名 + */ + public static boolean checkSignature(String token, String signature, String timestamp,String nonce) { + // 1.将token、timestamp、nonce三个参数进行字典序排序 + String[] arr = new String[] { token, timestamp, nonce }; + Arrays.sort(arr); + + // 2. 将三个参数字符串拼接成一个字符串进行sha1加密 + StringBuilder content = new StringBuilder(); + for (int i = 0; i < arr.length; i++) { + content.append(arr[i]); + } + MessageDigest md = null; + String tmpStr = null; + try { + md = MessageDigest.getInstance("SHA-1"); + // 将三个参数字符串拼接成一个字符串进行sha1加密 + byte[] digest = md.digest(content.toString().getBytes()); + tmpStr = byteToStr(digest); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + + content = null; + // 3.将sha1加密后的字符串可与signature对比,标识该请求来源于微信 + return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false; + } + + /** + * 将字节数组转换为十六进制字符串 + */ + private static String byteToStr(byte[] byteArray) { + String strDigest = ""; + for (int i = 0; i < byteArray.length; i++) { + strDigest += byteToHexStr(byteArray[i]); + } + return strDigest; + } + + /** + * 将字节转换为十六进制字符串 + */ + private static String byteToHexStr(byte mByte) { + char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A','B', 'C', 'D', 'E', 'F' }; + char[] tempArr = new char[2]; + tempArr[0] = Digit[(mByte >>> 4) & 0X0F]; + tempArr[1] = Digit[mByte & 0X0F]; + String s = new String(tempArr); + return s; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/spring/SpringUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/spring/SpringUtils.java new file mode 100644 index 0000000..a47531a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/spring/SpringUtils.java @@ -0,0 +1,183 @@ +package com.bnhz.common.utils.spring; + +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; +import com.bnhz.common.utils.StringUtils; +import org.springframework.util.CollectionUtils; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; + + +/** + * spring工具类 方便在非spring管理环境中获取bean + * + * @author ruoyi + */ +@Component +public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware +{ + /** Spring应用上下文环境 */ + private static ConfigurableListableBeanFactory beanFactory; + + private static ApplicationContext applicationContext; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException + { + SpringUtils.beanFactory = beanFactory; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException + { + SpringUtils.applicationContext = applicationContext; + } + + /** + * 获取对象 + * + * @param name + * @return Object 一个以所给名字注册的bean的实例 + * @throws org.springframework.beans.BeansException + * + */ + @SuppressWarnings("unchecked") + public static T getBean(String name) throws BeansException + { + return (T) beanFactory.getBean(name); + } + + /** + * 获取类型为requiredType的对象 + * + * @param clz + * @return + * @throws org.springframework.beans.BeansException + * + */ + public static T getBean(Class clz) throws BeansException + { + T result = (T) beanFactory.getBean(clz); + return result; + } + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + * + * @param name + * @return boolean + */ + public static boolean containsBean(String name) + { + return beanFactory.containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + * + * @param name + * @return boolean + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.isSingleton(name); + } + + /** + * @param name + * @return Class 注册对象的类型 + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static Class getType(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + * + * @param name + * @return + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException + { + return beanFactory.getAliases(name); + } + + /** + * 获取aop代理对象 + * + * @param invoker + * @return + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) + { + return (T) AopContext.currentProxy(); + } + + /** + * 获取当前的环境配置,无配置返回null + * + * @return 当前的环境配置 + */ + public static String[] getActiveProfiles() + { + return applicationContext.getEnvironment().getActiveProfiles(); + } + + /** + * 获取当前的环境配置,当有多个环境配置时,只获取第一个 + * + * @return 当前的环境配置 + */ + public static String getActiveProfile() + { + final String[] activeProfiles = getActiveProfiles(); + return StringUtils.isNotEmpty(activeProfiles) ? activeProfiles[0] : null; + } + + /** + * 获取配置文件中的值 + * + * @param key 配置文件的key + * @return 当前的配置文件的值 + * + */ + public static String getRequiredProperty(String key) + { + return applicationContext.getEnvironment().getRequiredProperty(key); + } + + /** + * 获取带有annotation注解的所有bean集合 + * @param annotation 注解 + * @param + * @return 集合 + */ + public static Map getBeanWithAnnotation(Class annotation){ + Map resultMap = new HashMap<>(); + Map beanMap = applicationContext.getBeansWithAnnotation(annotation); + if (CollectionUtils.isEmpty(beanMap)){ + return resultMap; + } + beanMap.forEach((key,value)->{ + resultMap.put(key,(T) value); + }); + return resultMap; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/sql/SqlUtil.java b/bnhz-common/src/main/java/com/bnhz/common/utils/sql/SqlUtil.java new file mode 100644 index 0000000..940e87a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/sql/SqlUtil.java @@ -0,0 +1,61 @@ +package com.bnhz.common.utils.sql; + +import com.bnhz.common.exception.UtilException; +import com.bnhz.common.utils.StringUtils; + +/** + * sql操作工具类 + * + * @author ruoyi + */ +public class SqlUtil +{ + /** + * 定义常用的 sql关键字 + */ + public static String SQL_REGEX = "and |extractvalue|updatexml|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |+|user()"; + + /** + * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序) + */ + public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+"; + + /** + * 检查字符,防止注入绕过 + */ + public static String escapeOrderBySql(String value) + { + if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) + { + throw new UtilException("参数不符合规范,不能进行查询"); + } + return value; + } + + /** + * 验证 order by 语法是否符合规范 + */ + public static boolean isValidOrderBySql(String value) + { + return value.matches(SQL_PATTERN); + } + + /** + * SQL关键字检查 + */ + public static void filterKeyword(String value) + { + if (StringUtils.isEmpty(value)) + { + return; + } + String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|"); + for (String sqlKeyword : sqlKeywords) + { + if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1) + { + throw new UtilException("参数存在SQL注入风险"); + } + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/uuid/IdUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/uuid/IdUtils.java new file mode 100644 index 0000000..03d9aee --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/uuid/IdUtils.java @@ -0,0 +1,128 @@ +package com.bnhz.common.utils.uuid; + +import lombok.extern.slf4j.Slf4j; +import com.bnhz.common.utils.Md5Utils; +import java.util.Random; + +/** + * ID生成器工具类 + * + * @author ruoyi + */ +@Slf4j +public class IdUtils +{ + private static long lastTimestamp = -1L; + private long sequence = 0L; + private final long workerId; + private final long datacenterId; + private static Integer startIndex=0; + private static Integer endIndex=6; + + public IdUtils(long workerId, long datacenterId) { + if(workerId <= 31L && workerId >= 0L) { + this.workerId = workerId; + } else { + if(workerId != -1L) { + throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0"); + } + + this.workerId = (long)(new Random()).nextInt(31); + } + + if(datacenterId <= 31L && datacenterId >= 0L) { + this.datacenterId = datacenterId; + } else { + if(datacenterId != -1L) { + throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0"); + } + + this.datacenterId = (long)(new Random()).nextInt(31); + } + + } + + public synchronized long nextId() { + long timestamp = this.timeGen(); + if(timestamp < lastTimestamp) { + try { + throw new Exception("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds"); + } catch (Exception e) { + log.warn("生成ID异常", e); + } + } + + if(lastTimestamp == timestamp) { + this.sequence = this.sequence + 1L & 4095L; + if(this.sequence == 0L) { + timestamp = this.tilNextMillis(lastTimestamp); + } + } else { + this.sequence = 0L; + } + + lastTimestamp = timestamp; + return timestamp - 1288834974657L << 22 | this.datacenterId << 17 | this.workerId << 12 | this.sequence; + } + + private long tilNextMillis(long lastTimestamp) { + long timestamp; + for(timestamp = this.timeGen(); timestamp <= lastTimestamp; timestamp = this.timeGen()) { + ; + } + return timestamp; + } + + private long timeGen() { + return System.currentTimeMillis(); + } + + public static String uuid() { + return java.util.UUID.randomUUID().toString().replaceAll("-", ""); + } + + public static String getNextCode() { + return Md5Utils.md5(IdUtils.uuid() + System.currentTimeMillis()).substring(startIndex,endIndex); + } + + + /** + * 获取随机UUID + * + * @return 随机UUID + */ + public static String randomUUID() + { + return UUID.randomUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线 + * + * @return 简化的UUID,去掉了横线 + */ + public static String simpleUUID() + { + return UUID.randomUUID().toString(true); + } + + /** + * 获取随机UUID,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 随机UUID + */ + public static String fastUUID() + { + return UUID.fastUUID().toString(); + } + + /** + * 简化的UUID,去掉了横线,使用性能更好的ThreadLocalRandom生成UUID + * + * @return 简化的UUID,去掉了横线 + */ + public static String fastSimpleUUID() + { + return UUID.fastUUID().toString(true); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/uuid/Seq.java b/bnhz-common/src/main/java/com/bnhz/common/utils/uuid/Seq.java new file mode 100644 index 0000000..be3ff22 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/uuid/Seq.java @@ -0,0 +1,86 @@ +package com.bnhz.common.utils.uuid; + +import java.util.concurrent.atomic.AtomicInteger; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.StringUtils; + +/** + * @author ruoyi 序列生成类 + */ +public class Seq +{ + // 通用序列类型 + public static final String commSeqType = "COMMON"; + + // 上传序列类型 + public static final String uploadSeqType = "UPLOAD"; + + // 通用接口序列数 + private static AtomicInteger commSeq = new AtomicInteger(1); + + // 上传接口序列数 + private static AtomicInteger uploadSeq = new AtomicInteger(1); + + // 机器标识 + private static String machineCode = "A"; + + /** + * 获取通用序列号 + * + * @return 序列值 + */ + public static String getId() + { + return getId(commSeqType); + } + + /** + * 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串 + * + * @return 序列值 + */ + public static String getId(String type) + { + AtomicInteger atomicInt = commSeq; + if (uploadSeqType.equals(type)) + { + atomicInt = uploadSeq; + } + return getId(atomicInt, 3); + } + + /** + * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串 + * + * @param atomicInt 序列数 + * @param length 数值长度 + * @return 序列值 + */ + public static String getId(AtomicInteger atomicInt, int length) + { + String result = DateUtils.dateTimeNow(); + result += machineCode; + result += getSeq(atomicInt, length); + return result; + } + + /** + * 序列循环递增字符串[1, 10 的 (length)幂次方), 用0左补齐length位数 + * + * @return 序列值 + */ + private synchronized static String getSeq(AtomicInteger atomicInt, int length) + { + // 先取值再+1 + int value = atomicInt.getAndIncrement(); + + // 如果更新后值>=10 的 (length)幂次方则重置为1 + int maxSeq = (int) Math.pow(10, length); + if (atomicInt.get() >= maxSeq) + { + atomicInt.set(1); + } + // 转字符串,用0左补齐 + return StringUtils.padl(value, length); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/uuid/UUID.java b/bnhz-common/src/main/java/com/bnhz/common/utils/uuid/UUID.java new file mode 100644 index 0000000..30dcfa8 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/uuid/UUID.java @@ -0,0 +1,484 @@ +package com.bnhz.common.utils.uuid; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import com.bnhz.common.exception.UtilException; + +/** + * 提供通用唯一识别码(universally unique identifier)(UUID)实现 + * + * @author ruoyi + */ +public final class UUID implements java.io.Serializable, Comparable +{ + private static final long serialVersionUID = -1185015143654744140L; + + /** + * SecureRandom 的单例 + * + */ + private static class Holder + { + static final SecureRandom numberGenerator = getSecureRandom(); + } + + /** 此UUID的最高64有效位 */ + private final long mostSigBits; + + /** 此UUID的最低64有效位 */ + private final long leastSigBits; + + /** + * 私有构造 + * + * @param data 数据 + */ + private UUID(byte[] data) + { + long msb = 0; + long lsb = 0; + assert data.length == 16 : "data must be 16 bytes in length"; + for (int i = 0; i < 8; i++) + { + msb = (msb << 8) | (data[i] & 0xff); + } + for (int i = 8; i < 16; i++) + { + lsb = (lsb << 8) | (data[i] & 0xff); + } + this.mostSigBits = msb; + this.leastSigBits = lsb; + } + + /** + * 使用指定的数据构造新的 UUID。 + * + * @param mostSigBits 用于 {@code UUID} 的最高有效 64 位 + * @param leastSigBits 用于 {@code UUID} 的最低有效 64 位 + */ + public UUID(long mostSigBits, long leastSigBits) + { + this.mostSigBits = mostSigBits; + this.leastSigBits = leastSigBits; + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的本地线程伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID fastUUID() + { + return randomUUID(false); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID() + { + return randomUUID(true); + } + + /** + * 获取类型 4(伪随机生成的)UUID 的静态工厂。 使用加密的强伪随机数生成器生成该 UUID。 + * + * @param isSecure 是否使用{@link SecureRandom}如果是可以获得更安全的随机码,否则可以得到更好的性能 + * @return 随机生成的 {@code UUID} + */ + public static UUID randomUUID(boolean isSecure) + { + final Random ng = isSecure ? Holder.numberGenerator : getRandom(); + + byte[] randomBytes = new byte[16]; + ng.nextBytes(randomBytes); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(randomBytes); + } + + /** + * 根据指定的字节数组获取类型 3(基于名称的)UUID 的静态工厂。 + * + * @param name 用于构造 UUID 的字节数组。 + * + * @return 根据指定数组生成的 {@code UUID} + */ + public static UUID nameUUIDFromBytes(byte[] name) + { + MessageDigest md; + try + { + md = MessageDigest.getInstance("MD5"); + } + catch (NoSuchAlgorithmException nsae) + { + throw new InternalError("MD5 not supported"); + } + byte[] md5Bytes = md.digest(name); + md5Bytes[6] &= 0x0f; /* clear version */ + md5Bytes[6] |= 0x30; /* set to version 3 */ + md5Bytes[8] &= 0x3f; /* clear variant */ + md5Bytes[8] |= 0x80; /* set to IETF variant */ + return new UUID(md5Bytes); + } + + /** + * 根据 {@link #toString()} 方法中描述的字符串标准表示形式创建{@code UUID}。 + * + * @param name 指定 {@code UUID} 字符串 + * @return 具有指定值的 {@code UUID} + * @throws IllegalArgumentException 如果 name 与 {@link #toString} 中描述的字符串表示形式不符抛出此异常 + * + */ + public static UUID fromString(String name) + { + String[] components = name.split("-"); + if (components.length != 5) + { + throw new IllegalArgumentException("Invalid UUID string: " + name); + } + for (int i = 0; i < 5; i++) + { + components[i] = "0x" + components[i]; + } + + long mostSigBits = Long.decode(components[0]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[1]).longValue(); + mostSigBits <<= 16; + mostSigBits |= Long.decode(components[2]).longValue(); + + long leastSigBits = Long.decode(components[3]).longValue(); + leastSigBits <<= 48; + leastSigBits |= Long.decode(components[4]).longValue(); + + return new UUID(mostSigBits, leastSigBits); + } + + /** + * 返回此 UUID 的 128 位值中的最低有效 64 位。 + * + * @return 此 UUID 的 128 位值中的最低有效 64 位。 + */ + public long getLeastSignificantBits() + { + return leastSigBits; + } + + /** + * 返回此 UUID 的 128 位值中的最高有效 64 位。 + * + * @return 此 UUID 的 128 位值中最高有效 64 位。 + */ + public long getMostSignificantBits() + { + return mostSigBits; + } + + /** + * 与此 {@code UUID} 相关联的版本号. 版本号描述此 {@code UUID} 是如何生成的。 + *

+ * 版本号具有以下含意: + *

    + *
  • 1 基于时间的 UUID + *
  • 2 DCE 安全 UUID + *
  • 3 基于名称的 UUID + *
  • 4 随机生成的 UUID + *
+ * + * @return 此 {@code UUID} 的版本号 + */ + public int version() + { + // Version is bits masked by 0x000000000000F000 in MS long + return (int) ((mostSigBits >> 12) & 0x0f); + } + + /** + * 与此 {@code UUID} 相关联的变体号。变体号描述 {@code UUID} 的布局。 + *

+ * 变体号具有以下含意: + *

    + *
  • 0 为 NCS 向后兼容保留 + *
  • 2 IETF RFC 4122(Leach-Salz), 用于此类 + *
  • 6 保留,微软向后兼容 + *
  • 7 保留供以后定义使用 + *
+ * + * @return 此 {@code UUID} 相关联的变体号 + */ + public int variant() + { + // This field is composed of a varying number of bits. + // 0 - - Reserved for NCS backward compatibility + // 1 0 - The IETF aka Leach-Salz variant (used by this class) + // 1 1 0 Reserved, Microsoft backward compatibility + // 1 1 1 Reserved for future definition. + return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62))) & (leastSigBits >> 63)); + } + + /** + * 与此 UUID 相关联的时间戳值。 + * + *

+ * 60 位的时间戳值根据此 {@code UUID} 的 time_low、time_mid 和 time_hi 字段构造。
+ * 所得到的时间戳以 100 毫微秒为单位,从 UTC(通用协调时间) 1582 年 10 月 15 日零时开始。 + * + *

+ * 时间戳值仅在在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 {@code UUID} 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @throws UnsupportedOperationException 如果此 {@code UUID} 不是 version 为 1 的 UUID。 + */ + public long timestamp() throws UnsupportedOperationException + { + checkTimeBase(); + return (mostSigBits & 0x0FFFL) << 48// + | ((mostSigBits >> 16) & 0x0FFFFL) << 32// + | mostSigBits >>> 32; + } + + /** + * 与此 UUID 相关联的时钟序列值。 + * + *

+ * 14 位的时钟序列值根据此 UUID 的 clock_seq 字段构造。clock_seq 字段用于保证在基于时间的 UUID 中的时间唯一性。 + *

+ * {@code clockSequence} 值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。 如果此 UUID 不是基于时间的 UUID,则此方法抛出 + * UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的时钟序列 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public int clockSequence() throws UnsupportedOperationException + { + checkTimeBase(); + return (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); + } + + /** + * 与此 UUID 相关的节点值。 + * + *

+ * 48 位的节点值根据此 UUID 的 node 字段构造。此字段旨在用于保存机器的 IEEE 802 地址,该地址用于生成此 UUID 以保证空间唯一性。 + *

+ * 节点值仅在基于时间的 UUID(其 version 类型为 1)中才有意义。
+ * 如果此 UUID 不是基于时间的 UUID,则此方法抛出 UnsupportedOperationException。 + * + * @return 此 {@code UUID} 的节点值 + * + * @throws UnsupportedOperationException 如果此 UUID 的 version 不为 1 + */ + public long node() throws UnsupportedOperationException + { + checkTimeBase(); + return leastSigBits & 0x0000FFFFFFFFFFFFL; + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @return 此{@code UUID} 的字符串表现形式 + * @see #toString(boolean) + */ + @Override + public String toString() + { + return toString(false); + } + + /** + * 返回此{@code UUID} 的字符串表现形式。 + * + *

+ * UUID 的字符串表示形式由此 BNF 描述: + * + *

+     * {@code
+     * UUID                   = ----
+     * time_low               = 4*
+     * time_mid               = 2*
+     * time_high_and_version  = 2*
+     * variant_and_sequence   = 2*
+     * node                   = 6*
+     * hexOctet               = 
+     * hexDigit               = [0-9a-fA-F]
+     * }
+     * 
+ * + * + * + * @param isSimple 是否简单模式,简单模式为不带'-'的UUID字符串 + * @return 此{@code UUID} 的字符串表现形式 + */ + public String toString(boolean isSimple) + { + final StringBuilder builder = new StringBuilder(isSimple ? 32 : 36); + // time_low + builder.append(digits(mostSigBits >> 32, 8)); + if (!isSimple) + { + builder.append('-'); + } + // time_mid + builder.append(digits(mostSigBits >> 16, 4)); + if (!isSimple) + { + builder.append('-'); + } + // time_high_and_version + builder.append(digits(mostSigBits, 4)); + if (!isSimple) + { + builder.append('-'); + } + // variant_and_sequence + builder.append(digits(leastSigBits >> 48, 4)); + if (!isSimple) + { + builder.append('-'); + } + // node + builder.append(digits(leastSigBits, 12)); + + return builder.toString(); + } + + /** + * 返回此 UUID 的哈希码。 + * + * @return UUID 的哈希码值。 + */ + @Override + public int hashCode() + { + long hilo = mostSigBits ^ leastSigBits; + return ((int) (hilo >> 32)) ^ (int) hilo; + } + + /** + * 将此对象与指定对象比较。 + *

+ * 当且仅当参数不为 {@code null}、而是一个 UUID 对象、具有与此 UUID 相同的 varriant、包含相同的值(每一位均相同)时,结果才为 {@code true}。 + * + * @param obj 要与之比较的对象 + * + * @return 如果对象相同,则返回 {@code true};否则返回 {@code false} + */ + @Override + public boolean equals(Object obj) + { + if ((null == obj) || (obj.getClass() != UUID.class)) + { + return false; + } + UUID id = (UUID) obj; + return (mostSigBits == id.mostSigBits && leastSigBits == id.leastSigBits); + } + + // Comparison Operations + + /** + * 将此 UUID 与指定的 UUID 比较。 + * + *

+ * 如果两个 UUID 不同,且第一个 UUID 的最高有效字段大于第二个 UUID 的对应字段,则第一个 UUID 大于第二个 UUID。 + * + * @param val 与此 UUID 比较的 UUID + * + * @return 在此 UUID 小于、等于或大于 val 时,分别返回 -1、0 或 1。 + * + */ + @Override + public int compareTo(UUID val) + { + // The ordering is intentionally set up so that the UUIDs + // can simply be numerically compared as two numbers + return (this.mostSigBits < val.mostSigBits ? -1 : // + (this.mostSigBits > val.mostSigBits ? 1 : // + (this.leastSigBits < val.leastSigBits ? -1 : // + (this.leastSigBits > val.leastSigBits ? 1 : // + 0)))); + } + + // ------------------------------------------------------------------------------------------------------------------- + // Private method start + /** + * 返回指定数字对应的hex值 + * + * @param val 值 + * @param digits 位 + * @return 值 + */ + private static String digits(long val, int digits) + { + long hi = 1L << (digits * 4); + return Long.toHexString(hi | (val & (hi - 1))).substring(1); + } + + /** + * 检查是否为time-based版本UUID + */ + private void checkTimeBase() + { + if (version() != 1) + { + throw new UnsupportedOperationException("Not a time-based UUID"); + } + } + + /** + * 获取{@link SecureRandom},类提供加密的强随机数生成器 (RNG) + * + * @return {@link SecureRandom} + */ + public static SecureRandom getSecureRandom() + { + try + { + return SecureRandom.getInstance("SHA1PRNG"); + } + catch (NoSuchAlgorithmException e) + { + throw new UtilException(e); + } + } + + /** + * 获取随机数生成器对象
+ * ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争争夺。 + * + * @return {@link ThreadLocalRandom} + */ + public static ThreadLocalRandom getRandom() + { + return ThreadLocalRandom.current(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/AesException.java b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/AesException.java new file mode 100644 index 0000000..170569c --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/AesException.java @@ -0,0 +1,56 @@ +package com.bnhz.common.utils.wechat; + +/** + * 加解密异常类 + */ +public class AesException extends Exception { + public final static int OK = 0; + public final static int ValidateSignatureError = -40001; + public final static int ParseXmlError = -40002; + public final static int ComputeSignatureError = -40003; + public final static int IllegalAesKey = -40004; + public final static int ValidateCorpidError = -40005; + public final static int EncryptAESError = -40006; + public final static int DecryptAESError = -40007; + public final static int IllegalBuffer = -40008; + //public final static int EncodeBase64Error = -40009; +//public final static int DecodeBase64Error = -40010; +//public final static int GenReturnXmlError = -40011; + private int code; + + private static String getMessage(int code) { + switch (code) { + case ValidateSignatureError: + return "签名验证错误"; + case ParseXmlError: + return "xml解析失败"; + case ComputeSignatureError: + return "sha加密生成签名失败"; + case IllegalAesKey: + return "SymmetricKey非法"; + case ValidateCorpidError: + return "corpid校验失败"; + case EncryptAESError: + return "aes加密失败"; + case DecryptAESError: + return "aes解密失败"; + case IllegalBuffer: + return "解密后得到的buffer非法"; +// case EncodeBase64Error: +// return "base64加密错误"; +// case DecodeBase64Error: +// return "base64解密错误"; +// case GenReturnXmlError: +// return "xml生成失败"; + default: + return null; // cannot be + } + } + public int getCode() { + return code; + } + AesException(int code) { + super(getMessage(code)); + this.code = code; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/ByteGroup.java b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/ByteGroup.java new file mode 100644 index 0000000..52fd57a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/ByteGroup.java @@ -0,0 +1,26 @@ +package com.bnhz.common.utils.wechat; + +import java.util.ArrayList; + +class ByteGroup { + ArrayList byteContainer = new ArrayList(); + + public byte[] toBytes() { + byte[] bytes = new byte[byteContainer.size()]; + for (int i = 0; i < byteContainer.size(); i++) { + bytes[i] = byteContainer.get(i); + } + return bytes; + } + + public ByteGroup addBytes(byte[] bytes) { + for (byte b : bytes) { + byteContainer.add(b); + } + return this; + } + + public int size() { + return byteContainer.size(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/PKCS7Encoder.java b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/PKCS7Encoder.java new file mode 100644 index 0000000..408e8c7 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/PKCS7Encoder.java @@ -0,0 +1,59 @@ +package com.bnhz.common.utils.wechat; + +import java.nio.charset.Charset; +import java.util.Arrays; + +/** + * 提供基于PKCS7算法的加解密接口. + */ +class PKCS7Encoder { + static Charset CHARSET = Charset.forName("utf-8"); + static int BLOCK_SIZE = 32; + + /** + * 获得对明文进行补位填充的字节. + * + * @param count 需要进行填充补位操作的明文字节个数 + * @return 补齐用的字节数组 + */ + static byte[] encode(int count) { + // 计算需要填充的位数 + int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE); + if (amountToPad == 0) { + amountToPad = BLOCK_SIZE; + } + // 获得补位所用的字符 + char padChr = chr(amountToPad); + String tmp = new String(); + for (int index = 0; index < amountToPad; index++) { + tmp += padChr; + } + return tmp.getBytes(CHARSET); + } + + /** + * 删除解密后明文的补位字符 + * + * @param decrypted 解密后的明文 + * @return 删除补位字符后的明文 + */ + static byte[] decode(byte[] decrypted) { + int pad = (int) decrypted[decrypted.length - 1]; + if (pad < 1 || pad > 32) { + pad = 0; + } + return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad); + } + + /** + * 将数字转化成ASCII码对应的字符,用于对明文进行补码 + * + * @param a 需要转化的数字 + * @return 转化得到的字符 + */ + static char chr(int a) { + byte target = (byte) (a & 0xFF); + return (char) target; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/SHA1.java b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/SHA1.java new file mode 100644 index 0000000..501cd91 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/SHA1.java @@ -0,0 +1,57 @@ +package com.bnhz.common.utils.wechat; + +import java.security.MessageDigest; +import java.util.Arrays; +/** + * 对企业微信发送给企业后台的消息加解密示例代码. + * + * @copyright Copyright (c) 1998-2014 Tencent Inc. + */ +/** + * SHA1 class + * + * 计算消息签名接口. + */ +public class SHA1 { + + /** + * 用SHA1算法生成安全签名 + * @param token 票据 + * @param timestamp 时间戳 + * @param nonce 随机字符串 + * @param encrypt 密文 + * @return 安全签名 + * @throws AesException + */ + public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws AesException + { + try { + String[] array = new String[] { token, timestamp, nonce, encrypt }; + StringBuffer sb = new StringBuffer(); + // 字符串排序 + Arrays.sort(array); + for (int i = 0; i < 4; i++) { + sb.append(array[i]); + } + String str = sb.toString(); + // SHA1签名生成 + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(str.getBytes()); + byte[] digest = md.digest(); + + StringBuffer hexstr = new StringBuffer(); + String shaHex = ""; + for (int i = 0; i < digest.length; i++) { + shaHex = Integer.toHexString(digest[i] & 0xFF); + if (shaHex.length() < 2) { + hexstr.append(0); + } + hexstr.append(shaHex); + } + return hexstr.toString(); + } catch (Exception e) { + e.printStackTrace(); + throw new AesException(AesException.ComputeSignatureError); + } + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/WXBizMsgCrypt.java b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/WXBizMsgCrypt.java new file mode 100644 index 0000000..65f2038 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/WXBizMsgCrypt.java @@ -0,0 +1,289 @@ +package com.bnhz.common.utils.wechat; + +/** + * 对企业微信发送给企业后台的消息加解密示例代码. + * + * @copyright Copyright (c) 1998-2014 Tencent Inc. + */ + +// ------------------------------------------------------------------------ + +/** + * 针对org.apache.commons.codec.binary.Base64, + * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本) + * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi + */ + +import org.apache.commons.codec.binary.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Random; + +/** + * 提供接收和推送给企业微信消息的加解密接口(UTF8编码的字符串). + *

    + *
  1. 第三方回复加密消息给企业微信
  2. + *
  3. 第三方收到企业微信发送的消息,验证消息的安全性,并对消息进行解密。
  4. + *
+ * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案 + *
    + *
  1. 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址: + * http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
  2. + *
  3. 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
  4. + *
  5. 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
  6. + *
  7. 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
  8. + *
+ */ +public class WXBizMsgCrypt { + static Charset CHARSET = Charset.forName("utf-8"); + Base64 base64 = new Base64(); + byte[] aesKey; + String token; + String receiveid; + + /** + * 构造函数 + * @param token 企业微信后台,开发者设置的token + * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey + * @param receiveid, 不同场景含义不同,详见文档 + * + * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 + */ + public WXBizMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException { + if (encodingAesKey.length() != 43) { + throw new AesException(AesException.IllegalAesKey); + } + + this.token = token; + this.receiveid = receiveid; + aesKey = Base64.decodeBase64(encodingAesKey + "="); + } + + // 生成4个字节的网络字节序 + byte[] getNetworkBytesOrder(int sourceNumber) { + byte[] orderBytes = new byte[4]; + orderBytes[3] = (byte) (sourceNumber & 0xFF); + orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF); + orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF); + orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF); + return orderBytes; + } + + // 还原4个字节的网络字节序 + int recoverNetworkBytesOrder(byte[] orderBytes) { + int sourceNumber = 0; + for (int i = 0; i < 4; i++) { + sourceNumber <<= 8; + sourceNumber |= orderBytes[i] & 0xff; + } + return sourceNumber; + } + + // 随机生成16位字符串 + String getRandomStr() { + String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 16; i++) { + int number = random.nextInt(base.length()); + sb.append(base.charAt(number)); + } + return sb.toString(); + } + + /** + * 对明文进行加密. + * + * @param text 需要加密的明文 + * @return 加密后base64编码的字符串 + * @throws AesException aes加密失败 + */ + String encrypt(String randomStr, String text) throws AesException { + ByteGroup byteCollector = new ByteGroup(); + byte[] randomStrBytes = randomStr.getBytes(CHARSET); + byte[] textBytes = text.getBytes(CHARSET); + byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length); + byte[] receiveidBytes = receiveid.getBytes(CHARSET); + + // randomStr + networkBytesOrder + text + receiveid + byteCollector.addBytes(randomStrBytes); + byteCollector.addBytes(networkBytesOrder); + byteCollector.addBytes(textBytes); + byteCollector.addBytes(receiveidBytes); + + // ... + pad: 使用自定义的填充方式对明文进行补位填充 + byte[] padBytes = PKCS7Encoder.encode(byteCollector.size()); + byteCollector.addBytes(padBytes); + + // 获得最终的字节流, 未加密 + byte[] unencrypted = byteCollector.toBytes(); + + try { + // 设置加密模式为AES的CBC模式 + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); + IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); + + // 加密 + byte[] encrypted = cipher.doFinal(unencrypted); + + // 使用BASE64对加密后的字符串进行编码 + String base64Encrypted = base64.encodeToString(encrypted); + + return base64Encrypted; + } catch (Exception e) { + e.printStackTrace(); + throw new AesException(AesException.EncryptAESError); + } + } + + /** + * 对密文进行解密. + * + * @param text 需要解密的密文 + * @return 解密得到的明文 + * @throws AesException aes解密失败 + */ + String decrypt(String text) throws AesException { + byte[] original; + try { + // 设置解密模式为AES的CBC模式 + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES"); + IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16)); + cipher.init(Cipher.DECRYPT_MODE, key_spec, iv); + + // 使用BASE64对密文进行解码 + byte[] encrypted = Base64.decodeBase64(text); + + // 解密 + original = cipher.doFinal(encrypted); + } catch (Exception e) { + e.printStackTrace(); + throw new AesException(AesException.DecryptAESError); + } + + String xmlContent, from_receiveid; + try { + // 去除补位字符 + byte[] bytes = PKCS7Encoder.decode(original); + + // 分离16位随机字符串,网络字节序和receiveid + byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20); + + int xmlLength = recoverNetworkBytesOrder(networkOrder); + + xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET); + from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), + CHARSET); + } catch (Exception e) { + e.printStackTrace(); + throw new AesException(AesException.IllegalBuffer); + } + + // receiveid不相同的情况 + if (!from_receiveid.equals(receiveid)) { + throw new AesException(AesException.ValidateCorpidError); + } + return xmlContent; + + } + + /** + * 将企业微信回复用户的消息加密打包. + *
    + *
  1. 对要发送的消息进行AES-CBC加密
  2. + *
  3. 生成安全签名
  4. + *
  5. 将消息密文和安全签名打包成xml格式
  6. + *
+ * + * @param replyMsg 企业微信待回复用户的消息,xml格式的字符串 + * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp + * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce + * + * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串 + * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 + */ + public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException { + // 加密 + String encrypt = encrypt(getRandomStr(), replyMsg); + + // 生成安全签名 + if (timeStamp == "") { + timeStamp = Long.toString(System.currentTimeMillis()); + } + + String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt); + + // System.out.println("发送给平台的签名是: " + signature[1].toString()); + // 生成发送的xml + String result = XMLParse.generate(encrypt, signature, timeStamp, nonce); + return result; + } + + /** + * 检验消息的真实性,并且获取解密后的明文. + *
    + *
  1. 利用收到的密文生成安全签名,进行签名验证
  2. + *
  3. 若验证通过,则提取xml中的加密消息
  4. + *
  5. 对消息进行解密
  6. + *
+ * + * @param msgSignature 签名串,对应URL参数的msg_signature + * @param timeStamp 时间戳,对应URL参数的timestamp + * @param nonce 随机串,对应URL参数的nonce + * @param postData 密文,对应POST请求的数据 + * + * @return 解密后的原文 + * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 + */ + public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String postData) + throws AesException { + + // 密钥,公众账号的app secret + // 提取密文 + Object[] encrypt = XMLParse.extract(postData); + + // 验证安全签名 + String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString()); + + // 和URL中的签名比较是否相等 + // System.out.println("第三方收到URL中的签名:" + msg_sign); + // System.out.println("第三方校验签名:" + signature); + if (!signature.equals(msgSignature)) { + throw new AesException(AesException.ValidateSignatureError); + } + + // 解密 + String result = decrypt(encrypt[1].toString()); + return result; + } + + /** + * 验证URL + * @param msgSignature 签名串,对应URL参数的msg_signature + * @param timeStamp 时间戳,对应URL参数的timestamp + * @param nonce 随机串,对应URL参数的nonce + * @param echoStr 随机串,对应URL参数的echostr + * + * @return 解密之后的echostr + * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息 + */ + public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr) + throws AesException { + String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr); + + if (!signature.equals(msgSignature)) { + throw new AesException(AesException.ValidateSignatureError); + } + + String result = decrypt(echoStr); + return result; + } + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/WechatUtils.java b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/WechatUtils.java new file mode 100644 index 0000000..bc97bda --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/WechatUtils.java @@ -0,0 +1,123 @@ +package com.bnhz.common.utils.wechat; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.http.HttpUtils; +import com.bnhz.common.utils.spring.SpringUtils; +import com.bnhz.common.wechat.*; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +/** + * @author fastb + * @version 1.0 + * @description: 微信相关工具类 + * @date 2024-01-08 17:36 + */ +public class WechatUtils { + + /** + * 网站、移动应用获取微信用户accessToken + * @param code 用户登录code + * @param appId 微信平台appId + * @param secret 微信平台密钥 + * @return WeChatAppResult + */ + public static WeChatAppResult getAccessTokenOpenId(String code, String appId, String secret) { + String url = BnhzConstant.URL.WX_GET_ACCESS_TOKEN_URL_PREFIX + "?appid=" + appId + "&secret=" + secret + "&code=" + code + "&grant_type=authorization_code"; + String s = HttpUtils.sendGet(url); + return JSON.parseObject(s, WeChatAppResult.class); + } + + /** + * 获取微信用户信息 + * @param accessToken 接口调用凭证 + * @param openId 用户唯一标识 + * @return WeChatUserInfo + */ + public static WeChatUserInfo getWeChatUserInfo(String accessToken, String openId) { + String url = BnhzConstant.URL.WX_GET_USER_INFO_URL_PREFIX + "?access_token=" + accessToken + "&openid=" + openId; + String s = HttpUtils.sendGet(url); + return JSON.parseObject(s, WeChatUserInfo.class); + } + + /** + * 小程序获取微信用户登录信息 + * @param code 用户凭证 + * @param appId 微信平台appId + * @param secret 微信平台密钥 + * @return 结果 + */ + public static WeChatMiniProgramResult codeToSession(String code, String appId, String secret) { + String url = BnhzConstant.URL.WX_MINI_PROGRAM_GET_USER_SESSION_URL_PREFIX + "?appid=" + appId + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code"; + String s = HttpUtils.sendGet(url); + return JSON.parseObject(s, WeChatMiniProgramResult.class); + } + + /** + * 小程序获取微信用户手机号 + * @param code 凭证 + * @param accessToken 微信用户token + * @return 手机号信息 + */ + public static WeChatPhoneInfo getWechatUserPhoneInfo(String code, String accessToken) { + String url = BnhzConstant.URL.WX_GET_USER_PHONE_URL_PREFIX + accessToken; + HashMap map = new HashMap<>(); + map.put("code", code); + String s = HttpUtils.sendPost(url, JSONObject.toJSONString(map)); + return JSON.parseObject(s, WeChatPhoneInfo.class); + } + + /** + * 小程序获、公众号取微信accessToken + * @param appId 微信平台appId + * @param secret 微信平台密钥 + * @return WeChatAppResult + */ + public static WeChatAppResult getAccessToken(String appId, String secret) { + // 加个缓存 + WeChatAppResult wechatAppResultRedis = SpringUtils.getBean(RedisCache.class).getCacheObject(CacheConstants.WECHAT_GET_ACCESS_TOKEN_APPID + appId); + if (ObjectUtil.isNotNull(wechatAppResultRedis)) { + return wechatAppResultRedis; + } + String url = BnhzConstant.URL.WX_MINI_PROGRAM_GET_ACCESS_TOKEN_URL_PREFIX + "&appid=" + appId + "&secret=" + secret; + String s = HttpUtils.sendGet(url); + WeChatAppResult weChatAppResult = JSON.parseObject(s, WeChatAppResult.class); + if (ObjectUtil.isNotNull(weChatAppResult) && StringUtils.isNotEmpty(weChatAppResult.getAccessToken())) { + SpringUtils.getBean(RedisCache.class).setCacheObject(CacheConstants.WECHAT_GET_ACCESS_TOKEN_APPID + appId, weChatAppResult, 1, TimeUnit.HOURS); + } + return weChatAppResult; + } + + /** + * 微信公众号获取微信用户信息 + * @param accessToken 接口调用凭证 + * @param openId 用户唯一标识 + * @return WeChatUserInfo + */ + public static WeChatUserInfo getWeChatPublicAccountUserInfo(String accessToken, String openId) { + String url = BnhzConstant.URL.WX_PUBLIC_ACCOUNT_GET_USER_INFO_URL_PREFIX + "?access_token=" + accessToken + "&openid=" + openId + "&lang=zh_CN"; + String s = HttpUtils.sendGet(url); + return JSON.parseObject(s, WeChatUserInfo.class); + } + + public static String responseText(WxCallBackXmlBO wxCallBackXmlBO, String content) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(""); + stringBuilder.append(""); + stringBuilder.append(""); + stringBuilder.append("" + (LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli() / 1000) + ""); + stringBuilder.append(""); + stringBuilder.append(""); //替换空格,文本信息内容不能包含有空格 .Replace(" ", string.Empty) + stringBuilder.append(""); + return stringBuilder.toString(); + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/XMLParse.java b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/XMLParse.java new file mode 100644 index 0000000..080f2c1 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/utils/wechat/XMLParse.java @@ -0,0 +1,103 @@ +package com.bnhz.common.utils.wechat; + +/** + * 对企业微信发送给企业后台的消息加解密示例代码. + * + * @copyright Copyright (c) 1998-2014 Tencent Inc. + */ + +// ------------------------------------------------------------------------ + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.StringReader; + +/** + * XMLParse class + * + * 提供提取消息格式中的密文及生成回复消息格式的接口. + */ +class XMLParse { + + /** + * 提取出xml数据包中的加密消息 + * @param xmltext 待提取的xml字符串 + * @return 提取出的加密消息字符串 + * @throws AesException + */ + public static Object[] extract(String xmltext) throws AesException { + Object[] result = new Object[3]; + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + + String FEATURE = null; + // This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented + // Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl + FEATURE = "http://apache.org/xml/features/disallow-doctype-decl"; + dbf.setFeature(FEATURE, true); + + // If you can't completely disable DTDs, then at least do the following: + // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities + // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities + // JDK7+ - http://xml.org/sax/features/external-general-entities + FEATURE = "http://xml.org/sax/features/external-general-entities"; + dbf.setFeature(FEATURE, false); + + // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities + // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities + // JDK7+ - http://xml.org/sax/features/external-parameter-entities + FEATURE = "http://xml.org/sax/features/external-parameter-entities"; + dbf.setFeature(FEATURE, false); + + // Disable external DTDs as well + FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; + dbf.setFeature(FEATURE, false); + + // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks" + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + + // And, per Timothy Morgan: "If for some reason support for inline DOCTYPEs are a requirement, then + // ensure the entity settings are disabled (as shown above) and beware that SSRF attacks + // (http://cwe.mitre.org/data/definitions/918.html) and denial + // of service attacks (such as billion laughs or decompression bombs via "jar:") are a risk." + + // remaining parser logic + DocumentBuilder db = dbf.newDocumentBuilder(); + StringReader sr = new StringReader(xmltext); + InputSource is = new InputSource(sr); + Document document = db.parse(is); + + Element root = document.getDocumentElement(); + NodeList nodelist1 = root.getElementsByTagName("Encrypt"); + result[0] = 0; + result[1] = nodelist1.item(0).getTextContent(); + return result; + } catch (Exception e) { + e.printStackTrace(); + throw new AesException(AesException.ParseXmlError); + } + } + + /** + * 生成xml消息 + * @param encrypt 加密后的消息密文 + * @param signature 安全签名 + * @param timestamp 时间戳 + * @param nonce 随机字符串 + * @return 生成的xml字符串 + */ + public static String generate(String encrypt, String signature, String timestamp, String nonce) { + + String format = "\n" + "\n" + + "\n" + + "%3$s\n" + "\n" + ""; + return String.format(format, encrypt, signature, timestamp, nonce); + + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatAppResult.java b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatAppResult.java new file mode 100644 index 0000000..d7b05de --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatAppResult.java @@ -0,0 +1,72 @@ +package com.bnhz.common.wechat; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +/** + * WeChat 调用api接口获取openid登信息后的返回类 + * @author fastb + * @date 2023-07-31 11:43 + */ +@Data +public class WeChatAppResult { + + /** + * 接口调用凭证 + */ + @JSONField(name = "access_token") + private String accessToken; + + /** + * access_token 接口调用凭证超时时间,单位(秒) + */ + @JSONField(name = "expires_in") + private Long expiresIn; + + /** + * 用户刷新 access_token + */ + @JSONField(name = "refresh_token") + private String refreshToken; + + /** + * 授权用户唯一标识 + */ + @JSONField(name = "openid") + private String openId; + + /** + * 用户授权的作用域(snsapi_userinfo) + */ + @JSONField(name = "scope") + private String scope; + + /** + * 当且仅当该移动应用已获得该用户的 userinfo 授权时,才会出现该字段 + */ + @JSONField(name = "unionid") + private String unionId; + + /** + * 错误码 + */ + @JSONField(name = "errcode") + private Integer errCode; + + /** + * 错误信息 + */ + @JSONField(name = "errmsg") + private String errMsg; + + /** + * 是否绑定手机号 + */ + private Boolean isBind; + + /** + * token 自定义登录状态 + */ + private String token; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatLoginBody.java b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatLoginBody.java new file mode 100644 index 0000000..4d643b4 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatLoginBody.java @@ -0,0 +1,89 @@ +package com.bnhz.common.wechat; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 微信端登录参数 + * @author fastb + * @date 2023-07-31 11:32 + */ +@Data +@Accessors(chain = true) +public class WeChatLoginBody { + + /** + * 传入参数:临时登录凭证 + */ + private String code; + + /** + * 临时获取用户手机号凭证 + */ + private String phoneCode; + + /** + * 传入参数 openid + */ + private String openId; + + /** + * 传入参数 session_key + */ + private String sessionKey; + + /** + * 传入参数 unionid + */ + private String unionId; + + /** + * 传入参数: 用户非敏感信息 + */ + private String rawData; + + /** + * 传入参数: 签名 + */ + private String signature; + + /** + * 传入参数: 用户敏感信息 + */ + private String encryptedData; + + /** + * 传入参数: 解密算法的向量 + */ + private String iv; + + /** + * 用户手机号 + */ + private String userPhone; + + /** + * 用户密码 + */ + private String userPwd; + + /** + * 接口调用凭证 + */ + private String accessToken; + + /** + * access_token 接口调用凭证超时时间,单位(秒) + */ + private Long expiresIn; + + /** + * 用户刷新 access_token + */ + private String refreshToken; + + /** + * 用户授权的作用域(snsapi_userinfo) + */ + private String scope; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatLoginResult.java b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatLoginResult.java new file mode 100644 index 0000000..6893946 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatLoginResult.java @@ -0,0 +1,22 @@ +package com.bnhz.common.wechat; + +import lombok.Data; + +/** + * 微信登录返回结果 + * @author fastb + * @date 2023-08-15 16:43 + */ +@Data +public class WeChatLoginResult { + + /** + * 登录成功返回token + */ + private String token; + + /** + * 绑定账号跳转页面 + */ + private String bindId; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatMiniProgramResult.java b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatMiniProgramResult.java new file mode 100644 index 0000000..5956d35 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatMiniProgramResult.java @@ -0,0 +1,42 @@ +package com.bnhz.common.wechat; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +/** + * @author fastb + * @date 2023-08-14 10:07 + */ +@Data +public class WeChatMiniProgramResult { + + /** + * 会话密钥 + */ + @JSONField(name = "session_key") + private String sessionKey; + + /** + * 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台账号下会返回,详见 UnionID 机制说明 + */ + @JSONField(name = "unionid") + private String unionId; + + /** + * 错误信息 + */ + @JSONField(name = "errmsg") + private String errMsg; + + /** + * 用户唯一标识 + */ + @JSONField(name = "openid") + private String openId; + + /** + * 错误码 + */ + @JSONField(name = "errcode") + private String errCode; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatPhoneInfo.java b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatPhoneInfo.java new file mode 100644 index 0000000..8981384 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatPhoneInfo.java @@ -0,0 +1,41 @@ +package com.bnhz.common.wechat; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +/** + * @author fastb + * @date 2023-08-16 17:48 + */ +@Data +public class WeChatPhoneInfo { + + @JSONField(name = "errcode") + private String errCode; + + @JSONField(name = "errmsg") + private String errmsg; + + @JSONField(name = "phone_info") + private PhoneInfo phoneInfo; + + @Data + public class PhoneInfo { + + private String phoneNumber; + + private String purePhoneNumber; + + private String countryCode; + + private WaterMark watermark; + } + + @Data + class WaterMark { + + private String timestamp; + + private String appid; + } +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatUserInfo.java b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatUserInfo.java new file mode 100644 index 0000000..dbedb39 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/wechat/WeChatUserInfo.java @@ -0,0 +1,80 @@ +package com.bnhz.common.wechat; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +/** + * 微信用户信息 + * @author fastb + * @date 2023-07-31 14:56 + */ +@Data +public class WeChatUserInfo { + + /** + * 普通用户的标识,对当前开发者账号唯一 + */ + @JSONField(name = "openid") + private String openId; + + /** + * 普通用户昵称 + */ + @JSONField(name = "nickname") + private String nickname; + + /** + * 普通用户性别,1 为男性,2 为女性 + */ + @JSONField(name = "sex") + private Integer sex; + + /** + * 普通用户个人资料填写的省份 + */ + @JSONField(name = "province") + private String province; + + /** + * 普通用户个人资料填写的城市 + */ + @JSONField(name = "city") + private String city; + + /** + * 国家,如中国为 CN + */ + @JSONField(name = "country") + private String country; + + /** + * 用户头像,最后一个数值代表正方形头像大小(有 0、46、64、96、132 数值可选,0 代表 640*640 正方形头像),用户没有头像时该项为空 + */ + @JSONField(name = "headimgurl") + private String headImgUrl; + + /** + * 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom) + */ + @JSONField(name = "privilege") + private String privilege; + + /** + * 用户统一标识。针对一个微信开放平台账号下的应用,同一用户的 unionid 是唯一的。 + */ + @JSONField(name = "unionid") + private String unionId; + + /** + * 错误码 + */ + @JSONField(name = "errcode") + private Integer errCode; + + /** + * 错误信息 + */ + @JSONField(name = "errmsg") + private String errMsg; + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/wechat/WxCallBackXmlBO.java b/bnhz-common/src/main/java/com/bnhz/common/wechat/WxCallBackXmlBO.java new file mode 100644 index 0000000..39ff26a --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/wechat/WxCallBackXmlBO.java @@ -0,0 +1,67 @@ +package com.bnhz.common.wechat; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +/** + * 微信回调的时候入参XML解析节点封装BO + * @author fastb + * @date 2024-03-12 15:24 + * @version 1.0 + */ +@Data +public class WxCallBackXmlBO { + + @JSONField(name = "MsgType") + private String msgType; + + @JSONField(name = "FromUserName") + private String fromUserName; + + @JSONField(name = "ToUserName") + private String toUserName; + + @JSONField(name = "CreateTime") + private String createTime; + + @JSONField(name = "Content") + private String content; + + @JSONField(name = "MsgId") + private String msgId; + + @JSONField(name = "Event") + private String event; + + @JSONField(name = "EventKey") + private String eventKey; + + @JSONField(name = "Ticket") + private String ticket; + + @JSONField(name = "UnionId") + private String unionId; + + @JSONField(name = "Recognition") + private String recognition; + + @JSONField(name = "PicUrl") + private String picUrl; + + @JSONField(name = "SuccessOrderId") + private String successOrderId; + + @JSONField(name = "CardId") + private String cardId; + + @JSONField(name = "UserCardCode") + private String userCardCode; + + @JSONField(name = "LocationX") + private String locationX; + + @JSONField(name = "LocationY") + private String locationY; + + +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/xss/Xss.java b/bnhz-common/src/main/java/com/bnhz/common/xss/Xss.java new file mode 100644 index 0000000..bf1ea23 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/xss/Xss.java @@ -0,0 +1,27 @@ +package com.bnhz.common.xss; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 自定义xss校验注解 + * + * @author ruoyi + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) +@Constraint(validatedBy = { XssValidator.class }) +public @interface Xss +{ + String message() + + default "不允许任何脚本运行"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/bnhz-common/src/main/java/com/bnhz/common/xss/XssValidator.java b/bnhz-common/src/main/java/com/bnhz/common/xss/XssValidator.java new file mode 100644 index 0000000..827eaa1 --- /dev/null +++ b/bnhz-common/src/main/java/com/bnhz/common/xss/XssValidator.java @@ -0,0 +1,34 @@ +package com.bnhz.common.xss; + +import com.bnhz.common.utils.StringUtils; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 自定义xss校验注解实现 + * + * @author ruoyi + */ +public class XssValidator implements ConstraintValidator +{ + private static final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) + { + if (StringUtils.isBlank(value)) + { + return true; + } + return !containsHtml(value); + } + + public static boolean containsHtml(String value) + { + Pattern pattern = Pattern.compile(HTML_PATTERN); + Matcher matcher = pattern.matcher(value); + return matcher.matches(); + } +} diff --git a/bnhz-framework/pom.xml b/bnhz-framework/pom.xml new file mode 100644 index 0000000..f204f34 --- /dev/null +++ b/bnhz-framework/pom.xml @@ -0,0 +1,70 @@ + + + + daqi-back + com.bnhz + 3.8.5 + + 4.0.0 + + bnhz-framework + + + framework框架核心 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + com.bnhz + bnhz-system-service + + + + + com.alibaba + druid-spring-boot-starter + + + + + pro.fessional + kaptcha + + + javax.servlet-api + javax.servlet + + + + + + + com.github.oshi + oshi-core + + + + org.redisson + redisson-spring-boot-starter + + + + + + diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/DataScopeAspect.java b/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/DataScopeAspect.java new file mode 100644 index 0000000..55441e8 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/DataScopeAspect.java @@ -0,0 +1,167 @@ +package com.bnhz.framework.aspectj; + +import java.util.ArrayList; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; +import com.bnhz.common.annotation.DataScope; +import com.bnhz.common.core.domain.BaseEntity; +import com.bnhz.common.core.domain.entity.SysRole; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.core.text.Convert; +import com.bnhz.common.utils.SecurityUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.framework.security.context.PermissionContextHolder; + +/** + * 数据过滤处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class DataScopeAspect +{ + /** + * 全部数据权限 + */ + public static final String DATA_SCOPE_ALL = "1"; + + /** + * 自定数据权限 + */ + public static final String DATA_SCOPE_CUSTOM = "2"; + + /** + * 部门数据权限 + */ + public static final String DATA_SCOPE_DEPT = "3"; + + /** + * 部门及以下数据权限 + */ + public static final String DATA_SCOPE_DEPT_AND_CHILD = "4"; + + /** + * 仅本人数据权限 + */ + public static final String DATA_SCOPE_SELF = "5"; + + /** + * 数据权限过滤关键字 + */ + public static final String DATA_SCOPE = "dataScope"; + + @Before("@annotation(controllerDataScope)") + public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable + { + clearDataScope(point); + handleDataScope(point, controllerDataScope); + } + + protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) + { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNotNull(loginUser)) + { + SysUser currentUser = loginUser.getUser(); + // 如果是超级管理员,则不过滤数据 + if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) + { + String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext()); + dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), + controllerDataScope.userAlias(), permission); + } + } + } + + /** + * 数据范围过滤 + * + * @param joinPoint 切点 + * @param user 用户 + * @param deptAlias 部门别名 + * @param userAlias 用户别名 + * @param permission 权限字符 + */ + public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission) + { + StringBuilder sqlString = new StringBuilder(); + List conditions = new ArrayList(); + + for (SysRole role : user.getRoles()) + { + String dataScope = role.getDataScope(); + if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope)) + { + continue; + } + if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions()) + && !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))) + { + continue; + } + if (DATA_SCOPE_ALL.equals(dataScope)) + { + sqlString = new StringBuilder(); + break; + } + else if (DATA_SCOPE_CUSTOM.equals(dataScope)) + { + sqlString.append(StringUtils.format( + " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, + role.getRoleId())); + } + else if (DATA_SCOPE_DEPT.equals(dataScope)) + { + sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId())); + } + else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) + { + sqlString.append(StringUtils.format( + " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", + deptAlias, user.getDeptId(), user.getDeptId())); + } + else if (DATA_SCOPE_SELF.equals(dataScope)) + { + if (StringUtils.isNotBlank(userAlias)) + { + sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId())); + } + else + { + // 数据权限为仅本人且没有userAlias别名不查询任何数据 + sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias)); + } + } + conditions.add(dataScope); + } + + if (StringUtils.isNotBlank(sqlString.toString())) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")"); + } + } + } + + /** + * 拼接权限sql前先清空params.dataScope参数防止注入 + */ + private void clearDataScope(final JoinPoint joinPoint) + { + Object params = joinPoint.getArgs()[0]; + if (StringUtils.isNotNull(params) && params instanceof BaseEntity) + { + BaseEntity baseEntity = (BaseEntity) params; + baseEntity.getParams().put(DATA_SCOPE, ""); + } + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/DataSourceAspect.java b/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/DataSourceAspect.java new file mode 100644 index 0000000..fac9ce4 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/DataSourceAspect.java @@ -0,0 +1,72 @@ +package com.bnhz.framework.aspectj; + +import java.util.Objects; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import com.bnhz.common.annotation.DataSource; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.framework.datasource.DynamicDataSourceContextHolder; + +/** + * 多数据源处理 + * + * @author ruoyi + */ +@Aspect +@Order(1) +@Component +public class DataSourceAspect +{ + protected Logger logger = LoggerFactory.getLogger(getClass()); + + @Pointcut("@annotation(com.bnhz.common.annotation.DataSource)" + + "|| @within(com.bnhz.common.annotation.DataSource)") + public void dsPointCut() + { + + } + + @Around("dsPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable + { + DataSource dataSource = getDataSource(point); + + if (StringUtils.isNotNull(dataSource)) + { + DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name()); + } + + try + { + return point.proceed(); + } + finally + { + // 销毁数据源 在执行方法之后 + DynamicDataSourceContextHolder.clearDataSourceType(); + } + } + + /** + * 获取需要切换的数据源 + */ + public DataSource getDataSource(ProceedingJoinPoint point) + { + MethodSignature signature = (MethodSignature) point.getSignature(); + DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class); + if (Objects.nonNull(dataSource)) + { + return dataSource; + } + + return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/LogAspect.java b/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/LogAspect.java new file mode 100644 index 0000000..4bca3dc --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/LogAspect.java @@ -0,0 +1,227 @@ +package com.bnhz.framework.aspectj; + +import java.util.Collection; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.web.multipart.MultipartFile; +import com.alibaba.fastjson2.JSON; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.enums.BusinessStatus; +import com.bnhz.common.enums.HttpMethod; +import com.bnhz.common.filter.PropertyPreExcludeFilter; +import com.bnhz.common.utils.SecurityUtils; +import com.bnhz.common.utils.ServletUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.ip.IpUtils; +import com.bnhz.framework.manager.AsyncManager; +import com.bnhz.framework.manager.factory.AsyncFactory; +import com.bnhz.system.domain.SysOperLog; + +/** + * 操作日志记录处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class LogAspect +{ + private static final Logger log = LoggerFactory.getLogger(LogAspect.class); + + /** 排除敏感属性字段 */ + public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" }; + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) + { + handleLog(joinPoint, controllerLog, null, jsonResult); + } + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) + { + handleLog(joinPoint, controllerLog, e, null); + } + + protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) + { + try + { + // 获取当前的用户 + LoginUser loginUser = SecurityUtils.getLoginUser(); + + // *========数据库日志=========*// + SysOperLog operLog = new SysOperLog(); + operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); + // 请求的地址 + String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); + operLog.setOperIp(ip); + operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); + if (loginUser != null) + { + operLog.setOperName(loginUser.getUsername()); + } + + if (e != null) + { + operLog.setStatus(BusinessStatus.FAIL.ordinal()); + operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); + } + // 设置方法名称 + String className = joinPoint.getTarget().getClass().getName(); + String methodName = joinPoint.getSignature().getName(); + operLog.setMethod(className + "." + methodName + "()"); + // 设置请求方式 + operLog.setRequestMethod(ServletUtils.getRequest().getMethod()); + // 处理设置注解上的参数 + getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult); + // 保存数据库 + AsyncManager.me().execute(AsyncFactory.recordOper(operLog)); + } + catch (Exception exp) + { + // 记录本地异常日志 + log.error("异常信息:{}", exp.getMessage()); + exp.printStackTrace(); + } + } + + /** + * 获取注解中对方法的描述信息 用于Controller层注解 + * + * @param log 日志 + * @param operLog 操作日志 + * @throws Exception + */ + public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception + { + // 设置action动作 + operLog.setBusinessType(log.businessType().ordinal()); + // 设置标题 + operLog.setTitle(log.title()); + // 设置操作人类别 + operLog.setOperatorType(log.operatorType().ordinal()); + // 是否需要保存request,参数和值 + if (log.isSaveRequestData()) + { + // 获取参数的信息,传入到数据库中。 + setRequestValue(joinPoint, operLog); + } + // 是否需要保存response,参数和值 + if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) + { + operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); + } + } + + /** + * 获取请求的参数,放到log中 + * + * @param operLog 操作日志 + * @throws Exception 异常 + */ + private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception + { + String requestMethod = operLog.getRequestMethod(); + if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) + { + String params = argsArrayToString(joinPoint.getArgs()); + operLog.setOperParam(StringUtils.substring(params, 0, 2000)); + } + else + { + Map paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest()); + operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter()), 0, 2000)); + } + } + + /** + * 参数拼装 + */ + private String argsArrayToString(Object[] paramsArray) + { + String params = ""; + if (paramsArray != null && paramsArray.length > 0) + { + for (Object o : paramsArray) + { + if (StringUtils.isNotNull(o) && !isFilterObject(o)) + { + try + { + String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter()); + params += jsonObj.toString() + " "; + } + catch (Exception e) + { + } + } + } + } + return params.trim(); + } + + /** + * 忽略敏感属性 + */ + public PropertyPreExcludeFilter excludePropertyPreFilter() + { + return new PropertyPreExcludeFilter().addExcludes(EXCLUDE_PROPERTIES); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param o 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + @SuppressWarnings("rawtypes") + public boolean isFilterObject(final Object o) + { + Class clazz = o.getClass(); + if (clazz.isArray()) + { + return clazz.getComponentType().isAssignableFrom(MultipartFile.class); + } + else if (Collection.class.isAssignableFrom(clazz)) + { + Collection collection = (Collection) o; + for (Object value : collection) + { + return value instanceof MultipartFile; + } + } + else if (Map.class.isAssignableFrom(clazz)) + { + Map map = (Map) o; + for (Object value : map.entrySet()) + { + Map.Entry entry = (Map.Entry) value; + return entry.getValue() instanceof MultipartFile; + } + } + return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse + || o instanceof BindingResult; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/RateLimiterAspect.java b/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/RateLimiterAspect.java new file mode 100644 index 0000000..8d8c5a2 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/aspectj/RateLimiterAspect.java @@ -0,0 +1,90 @@ +package com.bnhz.framework.aspectj; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; +import com.bnhz.common.annotation.RateLimiter; +import com.bnhz.common.enums.LimitType; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.utils.ServletUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.ip.IpUtils; + +/** + * 限流处理 + * + * @author ruoyi + */ +@Aspect +@Component +public class RateLimiterAspect +{ + private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); + + private RedisTemplate redisTemplate; + + private RedisScript limitScript; + + @Autowired + public void setRedisTemplate1(RedisTemplate redisTemplate) + { + this.redisTemplate = redisTemplate; + } + + @Autowired + public void setLimitScript(RedisScript limitScript) + { + this.limitScript = limitScript; + } + + @Before("@annotation(rateLimiter)") + public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable + { + int time = rateLimiter.time(); + int count = rateLimiter.count(); + + String combineKey = getCombineKey(rateLimiter, point); + List keys = Collections.singletonList(combineKey); + try + { + Long number = redisTemplate.execute(limitScript, keys, count, time); + if (StringUtils.isNull(number) || number.intValue() > count) + { + throw new ServiceException("访问过于频繁,请稍候再试"); + } + log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey); + } + catch (ServiceException e) + { + throw e; + } + catch (Exception e) + { + throw new RuntimeException("服务器限流异常,请稍候再试"); + } + } + + public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) + { + StringBuffer stringBuffer = new StringBuffer(rateLimiter.key()); + if (rateLimiter.limitType() == LimitType.IP) + { + stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest())).append("-"); + } + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + Class targetClass = method.getDeclaringClass(); + stringBuffer.append(targetClass.getName()).append("-").append(method.getName()); + return stringBuffer.toString(); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/ApplicationConfig.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/ApplicationConfig.java new file mode 100644 index 0000000..df29001 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/ApplicationConfig.java @@ -0,0 +1,30 @@ +package com.bnhz.framework.config; + +import java.util.TimeZone; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * 程序注解配置 + * + * @author ruoyi + */ +@Configuration +// 表示通过aop框架暴露该代理对象,AopContext能够访问 +@EnableAspectJAutoProxy(exposeProxy = true) +// 指定要扫描的Mapper类的包的路径 +@MapperScan("com.bnhz.**.mapper") +public class ApplicationConfig +{ + /** + * 时区配置 + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() + { + return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault()); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/CaptchaConfig.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/CaptchaConfig.java new file mode 100644 index 0000000..2a0c36d --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/CaptchaConfig.java @@ -0,0 +1,83 @@ +package com.bnhz.framework.config; + +import java.util.Properties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import static com.google.code.kaptcha.Constants.*; + +/** + * 验证码配置 + * + * @author ruoyi + */ +@Configuration +public class CaptchaConfig +{ + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } + + @Bean(name = "captchaProducerMath") + public DefaultKaptcha getKaptchaBeanMath() + { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否有边框 默认为true 我们可以自己设置yes,no + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 边框颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90"); + // 验证码文本字符颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); + // 验证码图片宽度 默认为200 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 验证码图片高度 默认为50 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 验证码文本字符大小 默认为40 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35"); + // KAPTCHA_SESSION_KEY + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath"); + // 验证码文本生成器 + properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.bnhz.framework.config.KaptchaTextCreator"); + // 验证码文本字符间距 默认为2 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3"); + // 验证码文本字符长度 默认为5 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6"); + // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize) + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 验证码噪点颜色 默认为Color.BLACK + properties.setProperty(KAPTCHA_NOISE_COLOR, "white"); + // 干扰实现类 + properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); + // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/DruidConfig.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/DruidConfig.java new file mode 100644 index 0000000..e21d477 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/DruidConfig.java @@ -0,0 +1,126 @@ +package com.bnhz.framework.config; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.sql.DataSource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; +import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; +import com.alibaba.druid.util.Utils; +import com.bnhz.common.enums.DataSourceType; +import com.bnhz.common.utils.spring.SpringUtils; +import com.bnhz.framework.config.properties.DruidProperties; +import com.bnhz.framework.datasource.DynamicDataSource; + +/** + * druid 配置多数据源 + * + * @author ruoyi + */ +@Configuration +public class DruidConfig +{ + @Bean + @ConfigurationProperties("spring.datasource.druid.master") + public DataSource masterDataSource(DruidProperties druidProperties) + { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean + @ConfigurationProperties("spring.datasource.druid.slave") + @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") + public DataSource slaveDataSource(DruidProperties druidProperties) + { + DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); + return druidProperties.dataSource(dataSource); + } + + @Bean(name = "dynamicDataSource") + @Primary + public DynamicDataSource dataSource(DataSource masterDataSource) + { + Map targetDataSources = new HashMap<>(); + targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); + setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource"); + return new DynamicDataSource(masterDataSource, targetDataSources); + } + + /** + * 设置数据源 + * + * @param targetDataSources 备选数据源集合 + * @param sourceName 数据源名称 + * @param beanName bean名称 + */ + public void setDataSource(Map targetDataSources, String sourceName, String beanName) + { + try + { + DataSource dataSource = SpringUtils.getBean(beanName); + targetDataSources.put(sourceName, dataSource); + } + catch (Exception e) + { + } + } + + /** + * 去除监控页面底部的广告 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true") + public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) + { + // 获取web监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取common.js的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + final String filePath = "support/http/resources/js/common.js"; + // 创建filter进行过滤 + Filter filter = new Filter() + { + @Override + public void init(javax.servlet.FilterConfig filterConfig) throws ServletException + { + } + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 + response.resetBuffer(); + // 获取common.js + String text = Utils.readFromResource(filePath); + // 正则替换banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + @Override + public void destroy() + { + } + }; + FilterRegistrationBean registrationBean = new FilterRegistrationBean(); + registrationBean.setFilter(filter); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/FastJson2JsonRedisSerializer.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/FastJson2JsonRedisSerializer.java new file mode 100644 index 0000000..e49c26f --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/FastJson2JsonRedisSerializer.java @@ -0,0 +1,48 @@ +package com.bnhz.framework.config; + +import java.nio.charset.Charset; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; + +/** + * Redis使用FastJson序列化 + * + * @author ruoyi + */ +public class FastJson2JsonRedisSerializer implements RedisSerializer +{ + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private Class clazz; + + public FastJson2JsonRedisSerializer(Class clazz) + { + super(); + this.clazz = clazz; + } + + @Override + public byte[] serialize(T t) throws SerializationException + { + if (t == null) + { + return new byte[0]; + } + return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET); + } + + @Override + public T deserialize(byte[] bytes) throws SerializationException + { + if (bytes == null || bytes.length <= 0) + { + return null; + } + String str = new String(bytes, DEFAULT_CHARSET); + + return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/FilterConfig.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/FilterConfig.java new file mode 100644 index 0000000..c81d6fe --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/FilterConfig.java @@ -0,0 +1,71 @@ +package com.bnhz.framework.config; + +import java.util.HashMap; +import java.util.Map; +import javax.servlet.DispatcherType; + +import com.bnhz.framework.interceptor.LogParamFilter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.bnhz.common.filter.RepeatableFilter; +import com.bnhz.common.filter.XssFilter; +import com.bnhz.common.utils.StringUtils; + +/** + * Filter配置 + * + * @author ruoyi + */ +@Configuration +public class FilterConfig +{ + @Value("${xss.excludes}") + private String excludes; + + @Value("${xss.urlPatterns}") + private String urlPatterns; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + @ConditionalOnProperty(value = "xss.enabled", havingValue = "true") + public FilterRegistrationBean xssFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new XssFilter()); + registration.addUrlPatterns(StringUtils.split(urlPatterns, ",")); + registration.setName("xssFilter"); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + Map initParameters = new HashMap(); + initParameters.put("excludes", excludes); + registration.setInitParameters(initParameters); + return registration; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Bean + public FilterRegistrationBean someFilterRegistration() + { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new RepeatableFilter()); + registration.addUrlPatterns("/*"); + registration.setName("repeatableFilter"); + registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); + return registration; + } + + + @Bean + public FilterRegistrationBean registFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new LogParamFilter()); + registration.addUrlPatterns("/*"); + registration.setName("LogFilter"); + registration.setOrder(1); + return registration; + } + +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/KaptchaTextCreator.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/KaptchaTextCreator.java new file mode 100644 index 0000000..ef72947 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/KaptchaTextCreator.java @@ -0,0 +1,68 @@ +package com.bnhz.framework.config; + +import java.util.Random; +import com.google.code.kaptcha.text.impl.DefaultTextCreator; + +/** + * 验证码文本生成器 + * + * @author ruoyi + */ +public class KaptchaTextCreator extends DefaultTextCreator +{ + private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); + + @Override + public String getText() + { + Integer result = 0; + Random random = new Random(); + int x = random.nextInt(10); + int y = random.nextInt(10); + StringBuilder suChinese = new StringBuilder(); + int randomoperands = random.nextInt(3); + if (randomoperands == 0) + { + result = x * y; + suChinese.append(CNUMBERS[x]); + suChinese.append("*"); + suChinese.append(CNUMBERS[y]); + } + else if (randomoperands == 1) + { + if ((x != 0) && y % x == 0) + { + result = y / x; + suChinese.append(CNUMBERS[y]); + suChinese.append("/"); + suChinese.append(CNUMBERS[x]); + } + else + { + result = x + y; + suChinese.append(CNUMBERS[x]); + suChinese.append("+"); + suChinese.append(CNUMBERS[y]); + } + } + else + { + if (x >= y) + { + result = x - y; + suChinese.append(CNUMBERS[x]); + suChinese.append("-"); + suChinese.append(CNUMBERS[y]); + } + else + { + result = y - x; + suChinese.append(CNUMBERS[y]); + suChinese.append("-"); + suChinese.append(CNUMBERS[x]); + } + } + suChinese.append("=?@" + result); + return suChinese.toString(); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/MyBatisConfig.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/MyBatisConfig.java new file mode 100644 index 0000000..1263152 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/MyBatisConfig.java @@ -0,0 +1,177 @@ +package com.bnhz.framework.config; + +import com.baomidou.mybatisplus.autoconfigure.SpringBootVFS; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import com.bnhz.common.utils.StringUtils; +import org.apache.ibatis.io.VFS; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.util.ClassUtils; + +import javax.sql.DataSource; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +/** + * Mybatis支持*匹配扫描包 + * + * @author ruoyi + */ +@Configuration +public class MyBatisConfig +{ + @Autowired + private Environment env; + + static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + + public static String setTypeAliasesPackage(String typeAliasesPackage) + { + ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver(); + MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver); + List allResult = new ArrayList(); + try + { + for (String aliasesPackage : typeAliasesPackage.split(",")) + { + List result = new ArrayList(); + aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN; + Resource[] resources = resolver.getResources(aliasesPackage); + if (resources != null && resources.length > 0) + { + MetadataReader metadataReader = null; + for (Resource resource : resources) + { + if (resource.isReadable()) + { + metadataReader = metadataReaderFactory.getMetadataReader(resource); + try + { + result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName()); + } + catch (ClassNotFoundException e) + { + e.printStackTrace(); + } + } + } + } + if (result.size() > 0) + { + HashSet hashResult = new HashSet(result); + allResult.addAll(hashResult); + } + } + if (allResult.size() > 0) + { + typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0])); + } + else + { + throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包"); + } + } + catch (IOException e) + { + e.printStackTrace(); + } + return typeAliasesPackage; + } + + public Resource[] resolveMapperLocations(String[] mapperLocations) + { + ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); + List resources = new ArrayList(); + if (mapperLocations != null) + { + for (String mapperLocation : mapperLocations) + { + try + { + Resource[] mappers = resourceResolver.getResources(mapperLocation); + resources.addAll(Arrays.asList(mappers)); + } + catch (IOException e) + { + // ignore + } + } + } + return resources.toArray(new Resource[resources.size()]); + } + + /** + * mybatis 配置 + */ +// @Bean(name = "mysqlSessionFactory") +// @Primary +// public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception +// { +// String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage"); +// String mapperLocations = env.getProperty("mybatis.mapperLocations"); +// String configLocation = env.getProperty("mybatis.configLocation"); +// typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage); +// VFS.addImplClass(SpringBootVFS.class); +// +// final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); +// sessionFactory.setDataSource(dataSource); +// sessionFactory.setTypeAliasesPackage(typeAliasesPackage); +// sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ","))); +// sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation)); +// return sessionFactory.getObject(); +// } + + /** + * mybatis-plus 配置:把 SqlSessionFactoryBean 换成 MybatisSqlSessionFactoryBean就行 + * @param dataSource 数据源 + * @return + */ + @Bean(name = "mysqlSessionFactory") + @Primary + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception + { + String typeAliasesPackage = env.getProperty("mybatis-plus.typeAliasesPackage"); + String mapperLocations = env.getProperty("mybatis-plus.mapperLocations"); + String configLocation = env.getProperty("mybatis-plus.configLocation"); + typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage); + VFS.addImplClass(SpringBootVFS.class); + + final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean(); + sessionFactory.setDataSource(dataSource); + sessionFactory.setTypeAliasesPackage(typeAliasesPackage); + sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ","))); + sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation)); + return sessionFactory.getObject(); + } + + @Bean(name = "mysqlTransactionManager") + @Primary + public DataSourceTransactionManager mysqlTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + @Bean(name = "mysqlSqlSessionTemplate") + @Primary + public SqlSessionTemplate mysqlSqlSessionTemplate(@Qualifier("mysqlSessionFactory") SqlSessionFactory sqlSessionFactory) { + return new SqlSessionTemplate(sqlSessionFactory); + } + +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/RedisConfig.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/RedisConfig.java new file mode 100644 index 0000000..906dc45 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/RedisConfig.java @@ -0,0 +1,69 @@ +package com.bnhz.framework.config; + +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +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.core.script.DefaultRedisScript; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * redis配置 + * + * @author ruoyi + */ +@Configuration +@EnableCaching +public class RedisConfig extends CachingConfigurerSupport +{ + @Bean + @SuppressWarnings(value = { "unchecked", "rawtypes" }) + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) + { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); + + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + + // Hash的key也采用StringRedisSerializer的序列化方式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } + + @Bean + public DefaultRedisScript limitScript() + { + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + redisScript.setScriptText(limitScriptText()); + redisScript.setResultType(Long.class); + return redisScript; + } + + /** + * 限流脚本 + */ + private String limitScriptText() + { + return "local key = KEYS[1]\n" + + "local count = tonumber(ARGV[1])\n" + + "local time = tonumber(ARGV[2])\n" + + "local current = redis.call('get', key);\n" + + "if current and tonumber(current) > count then\n" + + " return tonumber(current);\n" + + "end\n" + + "current = redis.call('incr', key)\n" + + "if tonumber(current) == 1 then\n" + + " redis.call('expire', key, time)\n" + + "end\n" + + "return tonumber(current);"; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/RedissonConfig.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/RedissonConfig.java new file mode 100644 index 0000000..8515910 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/RedissonConfig.java @@ -0,0 +1,44 @@ +package com.bnhz.framework.config; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.client.codec.StringCodec; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Leo + * @date 2024/8/19 14:48 + */ +@Configuration +public class RedissonConfig { + + @Value("${spring.redis.database}") + private int database; + + @Value("${spring.redis.host}") + private String host; + + @Value("${spring.redis.port}") + private String port; + + @Value("${spring.redis.password}") + private String password; + + @Bean(value = "redissonClient", destroyMethod = "shutdown") + public RedissonClient redissonClient() throws Exception { + + Config config = new Config(); + config.useSingleServer().setAddress(String.format("redis://%s:%s", this.host, this.port)); + if (!this.password.isEmpty()) { + config.useSingleServer().setPassword(this.password); + } + config.useSingleServer().setDatabase(this.database); + + StringCodec codec = new StringCodec(); + config.setCodec(codec); + return Redisson.create(config); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/ResourcesConfig.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/ResourcesConfig.java new file mode 100644 index 0000000..24f1e06 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/ResourcesConfig.java @@ -0,0 +1,73 @@ +package com.bnhz.framework.config; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.CacheControl; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import com.bnhz.common.config.DaQiConfig; +import com.bnhz.common.constant.Constants; +import com.bnhz.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 通用配置 + * + * @author ruoyi + */ +@Configuration +public class ResourcesConfig implements WebMvcConfigurer +{ + @Autowired + private RepeatSubmitInterceptor repeatSubmitInterceptor; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) + { + /** 本地文件上传路径 */ + registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**") + .addResourceLocations("file:" + DaQiConfig.getProfile() + "/"); + + /** swagger配置 */ + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/") + .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic());; + } + + /** + * 自定义拦截规则 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); + } + + /** + * 跨域配置 + */ + @Bean + public CorsFilter corsFilter() + { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + // 设置访问源地址 + config.addAllowedOriginPattern("*"); + // 设置访问源请求头 + config.addAllowedHeader("*"); + // 设置访问源请求方法 + config.addAllowedMethod("*"); + // 有效期 1800秒 + config.setMaxAge(1800L); + // 添加映射路径,拦截一切请求 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + // 返回新的CorsFilter + return new CorsFilter(source); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/SecurityConfig.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/SecurityConfig.java new file mode 100644 index 0000000..874f68c --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/SecurityConfig.java @@ -0,0 +1,174 @@ +package com.bnhz.framework.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.web.filter.CorsFilter; +import com.bnhz.framework.config.properties.PermitAllUrlProperties; +import com.bnhz.framework.security.filter.JwtAuthenticationTokenFilter; +import com.bnhz.framework.security.handle.AuthenticationEntryPointImpl; +import com.bnhz.framework.security.handle.LogoutSuccessHandlerImpl; + +/** + * spring security配置 + * + * @author ruoyi + */ +//@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter +{ + /** + * 自定义用户认证逻辑 + */ + @Autowired + private UserDetailsService userDetailsService; + + /** + * 认证失败处理类 + */ + @Autowired + private AuthenticationEntryPointImpl unauthorizedHandler; + + /** + * 退出处理类 + */ + @Autowired + private LogoutSuccessHandlerImpl logoutSuccessHandler; + + /** + * token认证过滤器 + */ + @Autowired + private JwtAuthenticationTokenFilter authenticationTokenFilter; + + /** + * 跨域过滤器 + */ + @Autowired + private CorsFilter corsFilter; + + /** + * 允许匿名访问的地址 + */ + @Autowired + private PermitAllUrlProperties permitAllUrl; + + /** + * 解决 无法直接注入 AuthenticationManager + * + * @return + * @throws Exception + */ + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception + { + return super.authenticationManagerBean(); + } + + /** + * anyRequest | 匹配所有请求路径 + * access | SpringEl表达式结果为true时可以访问 + * anonymous | 匿名可以访问 + * denyAll | 用户不能访问 + * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) + * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 + * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 + * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 + * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 + * hasRole | 如果有参数,参数表示角色,则其角色可以访问 + * permitAll | 用户可以任意访问 + * rememberMe | 允许通过remember-me登录的用户访问 + * authenticated | 用户登录后可访问 + */ + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception + { + // 注解标记允许匿名访问的url + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests(); + permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll()); + + httpSecurity + // CSRF禁用,因为不使用session + .csrf().disable() + // 禁用HTTP响应标头 + .headers().cacheControl().disable().and() + // 认证失败处理类 + .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() + // 基于token,所以不需要session + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + // 过滤请求 + .authorizeRequests() + // 对于登录login 注册register 验证码captchaImage 允许匿名访问 + .antMatchers("/login","/KC/login", "/register", "/captchaImage","/iot/tool/register","/iot/tool/ntp","/iot/tool/download", + "/iot/tool/mqtt/auth","/iot/tool/mqtt/authv5","/iot/tool/mqtt/webhook","/iot/tool/mqtt/webhookv5","/auth/**/**", + "/wechat/mobileLogin", "/wechat/miniLogin", "/wechat/wxBind/callback").permitAll() + .antMatchers("/zlmhook/**").permitAll() + .antMatchers("/ruleengine/rulemanager/**").permitAll() + .antMatchers("/yunchache/gps_r").permitAll() + .antMatchers("/sip/player/getBigScreenUrl/**").permitAll() + .antMatchers("/goview/sys/login","/goview/project/getData").permitAll() + .antMatchers("/notify/smsLoginCaptcha","/auth/sms/login", "/notify/weComVerifyUrl","/wechat/publicAccount/callback").permitAll() + // 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() + .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() + + .antMatchers("/oauth2/**").permitAll() +// // oauth +// .antMatchers("/oauth/css/**","/oauth/fonts/**","/oauth/js/**").permitAll() + // dueros + .antMatchers("/dueros").permitAll() + + // 除上面外的所有请求全部需要鉴权认证 + .anyRequest().authenticated() + +// // oauth +// .and() +// .formLogin() +// .loginPage("/oauth/login") +// .permitAll() +// .and() +// .logout().logoutUrl("/oauth/logout") +// .permitAll() + + .and() + .headers().frameOptions().disable(); + // 添加Logout filter + httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); + // 添加JWT filter + httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + // 添加CORS filter + httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); + httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); + } + + /** + * 强散列哈希加密实现 + */ + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() + { + return new BCryptPasswordEncoder(); + } + + /** + * 身份认证接口 + */ + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception + { + auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/ServerConfig.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/ServerConfig.java new file mode 100644 index 0000000..1160d7c --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/ServerConfig.java @@ -0,0 +1,32 @@ +package com.bnhz.framework.config; + +import javax.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; +import com.bnhz.common.utils.ServletUtils; + +/** + * 服务相关配置 + * + * @author ruoyi + */ +@Component +public class ServerConfig +{ + /** + * 获取完整的请求路径,包括:域名,端口,上下文访问路径 + * + * @return 服务地址 + */ + public String getUrl() + { + HttpServletRequest request = ServletUtils.getRequest(); + return getDomain(request); + } + + public static String getDomain(HttpServletRequest request) + { + StringBuffer url = request.getRequestURL(); + String contextPath = request.getServletContext().getContextPath(); + return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString(); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/ThreadPoolConfig.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/ThreadPoolConfig.java new file mode 100644 index 0000000..36879f5 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/ThreadPoolConfig.java @@ -0,0 +1,63 @@ +package com.bnhz.framework.config; + +import com.bnhz.common.utils.Threads; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 线程池配置 + * + * @author ruoyi + **/ +@Configuration +public class ThreadPoolConfig +{ + // 核心线程池大小 + private int corePoolSize = 50; + + // 最大可创建的线程数 + private int maxPoolSize = 200; + + // 队列最大长度 + private int queueCapacity = 1000; + + // 线程池维护线程所允许的空闲时间 + private int keepAliveSeconds = 300; + + @Bean(name = "threadPoolTaskExecutor") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() + { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setMaxPoolSize(maxPoolSize); + executor.setCorePoolSize(corePoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setKeepAliveSeconds(keepAliveSeconds); + // 线程池对拒绝任务(无线程可用)的处理策略 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + return executor; + } + + /** + * 执行周期性或定时任务 + */ + @Bean(name = "scheduledExecutorService") + protected ScheduledExecutorService scheduledExecutorService() + { + return new ScheduledThreadPoolExecutor(corePoolSize, + new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(), + new ThreadPoolExecutor.CallerRunsPolicy()) + { + @Override + protected void afterExecute(Runnable r, Throwable t) + { + super.afterExecute(r, t); + Threads.printException(r, t); + } + }; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/properties/DruidProperties.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/properties/DruidProperties.java new file mode 100644 index 0000000..b052ed5 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/properties/DruidProperties.java @@ -0,0 +1,77 @@ +package com.bnhz.framework.config.properties; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import com.alibaba.druid.pool.DruidDataSource; + +/** + * druid 配置属性 + * + * @author ruoyi + */ +@Configuration +public class DruidProperties +{ + @Value("${spring.datasource.druid.initialSize}") + private int initialSize; + + @Value("${spring.datasource.druid.minIdle}") + private int minIdle; + + @Value("${spring.datasource.druid.maxActive}") + private int maxActive; + + @Value("${spring.datasource.druid.maxWait}") + private int maxWait; + + @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") + private int timeBetweenEvictionRunsMillis; + + @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") + private int minEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") + private int maxEvictableIdleTimeMillis; + + @Value("${spring.datasource.druid.validationQuery}") + private String validationQuery; + + @Value("${spring.datasource.druid.testWhileIdle}") + private boolean testWhileIdle; + + @Value("${spring.datasource.druid.testOnBorrow}") + private boolean testOnBorrow; + + @Value("${spring.datasource.druid.testOnReturn}") + private boolean testOnReturn; + + public DruidDataSource dataSource(DruidDataSource datasource) + { + /** 配置初始化大小、最小、最大 */ + datasource.setInitialSize(initialSize); + datasource.setMaxActive(maxActive); + datasource.setMinIdle(minIdle); + + /** 配置获取连接等待超时的时间 */ + datasource.setMaxWait(maxWait); + + /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ + datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + + /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ + datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); + + /** + * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 + */ + datasource.setValidationQuery(validationQuery); + /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ + datasource.setTestWhileIdle(testWhileIdle); + /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnBorrow(testOnBorrow); + /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ + datasource.setTestOnReturn(testOnReturn); + return datasource; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/config/properties/PermitAllUrlProperties.java b/bnhz-framework/src/main/java/com/bnhz/framework/config/properties/PermitAllUrlProperties.java new file mode 100644 index 0000000..a937b68 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/config/properties/PermitAllUrlProperties.java @@ -0,0 +1,72 @@ +package com.bnhz.framework.config.properties; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; +import org.apache.commons.lang3.RegExUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import com.bnhz.common.annotation.Anonymous; + +/** + * 设置Anonymous注解允许匿名访问的url + * + * @author ruoyi + */ +@Configuration +public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware +{ + private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}"); + + private ApplicationContext applicationContext; + + private List urls = new ArrayList<>(); + + public String ASTERISK = "*"; + + @Override + public void afterPropertiesSet() + { + RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class); + Map map = mapping.getHandlerMethods(); + + map.keySet().forEach(info -> { + HandlerMethod handlerMethod = map.get(info); + + // 获取方法上边的注解 替代path variable 为 * + Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class); + Optional.ofNullable(method).ifPresent(anonymous -> info.getPatternsCondition().getPatterns() + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + + // 获取类上边的注解, 替代path variable 为 * + Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class); + Optional.ofNullable(controller).ifPresent(anonymous -> info.getPatternsCondition().getPatterns() + .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK)))); + }); + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException + { + this.applicationContext = context; + } + + public List getUrls() + { + return urls; + } + + public void setUrls(List urls) + { + this.urls = urls; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/datasource/DynamicDataSource.java b/bnhz-framework/src/main/java/com/bnhz/framework/datasource/DynamicDataSource.java new file mode 100644 index 0000000..24d844d --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/datasource/DynamicDataSource.java @@ -0,0 +1,26 @@ +package com.bnhz.framework.datasource; + +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +/** + * 动态数据源 + * + * @author ruoyi + */ +public class DynamicDataSource extends AbstractRoutingDataSource +{ + public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) + { + super.setDefaultTargetDataSource(defaultTargetDataSource); + super.setTargetDataSources(targetDataSources); + super.afterPropertiesSet(); + } + + @Override + protected Object determineCurrentLookupKey() + { + return DynamicDataSourceContextHolder.getDataSourceType(); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/datasource/DynamicDataSourceContextHolder.java b/bnhz-framework/src/main/java/com/bnhz/framework/datasource/DynamicDataSourceContextHolder.java new file mode 100644 index 0000000..54db261 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/datasource/DynamicDataSourceContextHolder.java @@ -0,0 +1,45 @@ +package com.bnhz.framework.datasource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 数据源切换处理 + * + * @author ruoyi + */ +public class DynamicDataSourceContextHolder +{ + public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); + + /** + * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本, + * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 + */ + private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>(); + + /** + * 设置数据源的变量 + */ + public static void setDataSourceType(String dsType) + { + log.info("切换到{}数据源", dsType); + CONTEXT_HOLDER.set(dsType); + } + + /** + * 获得数据源的变量 + */ + public static String getDataSourceType() + { + return CONTEXT_HOLDER.get(); + } + + /** + * 清空数据源变量 + */ + public static void clearDataSourceType() + { + CONTEXT_HOLDER.remove(); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/interceptor/LogParamFilter.java b/bnhz-framework/src/main/java/com/bnhz/framework/interceptor/LogParamFilter.java new file mode 100644 index 0000000..cd386ba --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/interceptor/LogParamFilter.java @@ -0,0 +1,122 @@ +package com.bnhz.framework.interceptor; + +import com.bnhz.common.utils.uuid.UUID; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; +import org.springframework.web.util.WebUtils; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; + +/** + * @author Leo + * @date 2024/7/29 16:37 + */ +@Slf4j +public class LogParamFilter extends OncePerRequestFilter implements Ordered { + // put filter at the end of all other filters to make sure we are processing after all others + private int order = Ordered.LOWEST_PRECEDENCE - 8; + public static final String SPLIT_STRING_M = "="; + public static final String SPLIT_STRING_DOT = ", "; + + @Override + public int getOrder() { + return order; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + ContentCachingRequestWrapper wrapperRequest = new ContentCachingRequestWrapper(request); + ContentCachingResponseWrapper wrapperResponse = new ContentCachingResponseWrapper(response); + String urlParams = getRequestParams(request); + String requestBodyStr = getRequestBody(wrapperRequest); + String trackId = UUID.randomUUID().toString(); + log.info("trackId:===>{}| url: {}:[{}] | params[{}] | request body:{}", trackId, request.getMethod(), request.getRequestURI(), urlParams, requestBodyStr); + filterChain.doFilter(wrapperRequest, wrapperResponse); + + + String responseBodyStr = getResponseBody(wrapperResponse); + log.info("trackId:===>{}| response body:{}", trackId, responseBodyStr); + wrapperResponse.copyBodyToResponse(); + } + + /** + * 打印请求参数 + * + * @param request + */ + @SneakyThrows + private String getRequestBody(ContentCachingRequestWrapper request) { + ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); + if (wrapper != null) { + wrapper.setCharacterEncoding(StandardCharsets.UTF_8.displayName()); + byte[] buf = wrapper.getContentAsByteArray(); + if (buf.length > 0) { + String payload; + try { + payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding()); + } catch (UnsupportedEncodingException e) { + payload = "[unknown]"; + } + return payload.replaceAll("\\n", ""); + } + } + return ""; + } + + /** + * 打印返回参数 + * + * @param response + */ + private String getResponseBody(ContentCachingResponseWrapper response) { + ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, + ContentCachingResponseWrapper.class); + + if (wrapper != null) { + wrapper.setCharacterEncoding(StandardCharsets.UTF_8.displayName()); + byte[] buf = wrapper.getContentAsByteArray(); + if (buf.length > 0) { + String payload; + try { + payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding()); + } catch (UnsupportedEncodingException e) { + payload = "[unknown]"; + } + return payload; + } + } + return ""; + } + + /** + * 获取请求地址上的参数 + * + * @param request + * @return + */ + public static String getRequestParams(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); + Enumeration enu = request.getParameterNames(); + //获取请求参数 + while (enu.hasMoreElements()) { + String name = enu.nextElement(); + sb.append(name + SPLIT_STRING_M).append(request.getParameter(name)); + if (enu.hasMoreElements()) { + sb.append(SPLIT_STRING_DOT); + } + } + return sb.toString(); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/interceptor/RepeatSubmitInterceptor.java b/bnhz-framework/src/main/java/com/bnhz/framework/interceptor/RepeatSubmitInterceptor.java new file mode 100644 index 0000000..9630bb3 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/interceptor/RepeatSubmitInterceptor.java @@ -0,0 +1,55 @@ +package com.bnhz.framework.interceptor; + +import java.lang.reflect.Method; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import com.alibaba.fastjson2.JSON; +import com.bnhz.common.annotation.RepeatSubmit; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.utils.ServletUtils; + +/** + * 防止重复提交拦截器 + * + * @author ruoyi + */ +@Component +public abstract class RepeatSubmitInterceptor implements HandlerInterceptor +{ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception + { + if (handler instanceof HandlerMethod) + { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); + if (annotation != null) + { + if (this.isRepeatSubmit(request, annotation)) + { + AjaxResult ajaxResult = AjaxResult.error(annotation.message()); + ServletUtils.renderString(response, JSON.toJSONString(ajaxResult)); + return false; + } + } + return true; + } + else + { + return true; + } + } + + /** + * 验证是否重复提交由子类实现具体的防重复提交的规则 + * + * @param request + * @return + * @throws Exception + */ + public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation); +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/interceptor/impl/SameUrlDataInterceptor.java b/bnhz-framework/src/main/java/com/bnhz/framework/interceptor/impl/SameUrlDataInterceptor.java new file mode 100644 index 0000000..b1ad559 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/interceptor/impl/SameUrlDataInterceptor.java @@ -0,0 +1,110 @@ +package com.bnhz.framework.interceptor.impl; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.bnhz.common.annotation.RepeatSubmit; +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.filter.RepeatedlyRequestWrapper; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.http.HttpHelper; +import com.bnhz.framework.interceptor.RepeatSubmitInterceptor; + +/** + * 判断请求url和数据是否和上一次相同, + * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 + * + * @author ruoyi + */ +@Component +public class SameUrlDataInterceptor extends RepeatSubmitInterceptor +{ + public final String REPEAT_PARAMS = "repeatParams"; + + public final String REPEAT_TIME = "repeatTime"; + + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + @Autowired + private RedisCache redisCache; + + @SuppressWarnings("unchecked") + @Override + public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) + { + String nowParams = ""; + if (request instanceof RepeatedlyRequestWrapper) + { + RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; + nowParams = HttpHelper.getBodyString(repeatedlyRequest); + } + + // body参数为空,获取Parameter的数据 + if (StringUtils.isEmpty(nowParams)) + { + nowParams = JSON.toJSONString(request.getParameterMap()); + } + Map nowDataMap = new HashMap(); + nowDataMap.put(REPEAT_PARAMS, nowParams); + nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); + + // 请求地址(作为存放cache的key值) + String url = request.getRequestURI(); + + // 唯一值(没有消息头则使用请求地址) + String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); + + // 唯一标识(指定key + url + 消息头) + String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey; + + Object sessionObj = redisCache.getCacheObject(cacheRepeatKey); + if (sessionObj != null) + { + Map sessionMap = (Map) sessionObj; + if (sessionMap.containsKey(url)) + { + Map preDataMap = (Map) sessionMap.get(url); + if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) + { + return true; + } + } + } + Map cacheMap = new HashMap(); + cacheMap.put(url, nowDataMap); + redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); + return false; + } + + /** + * 判断参数是否相同 + */ + private boolean compareParams(Map nowMap, Map preMap) + { + String nowParams = (String) nowMap.get(REPEAT_PARAMS); + String preParams = (String) preMap.get(REPEAT_PARAMS); + return nowParams.equals(preParams); + } + + /** + * 判断两次间隔时间 + */ + private boolean compareTime(Map nowMap, Map preMap, int interval) + { + long time1 = (Long) nowMap.get(REPEAT_TIME); + long time2 = (Long) preMap.get(REPEAT_TIME); + if ((time1 - time2) < interval) + { + return true; + } + return false; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/manager/AsyncManager.java b/bnhz-framework/src/main/java/com/bnhz/framework/manager/AsyncManager.java new file mode 100644 index 0000000..4d85a19 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/manager/AsyncManager.java @@ -0,0 +1,55 @@ +package com.bnhz.framework.manager; + +import java.util.TimerTask; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import com.bnhz.common.utils.Threads; +import com.bnhz.common.utils.spring.SpringUtils; + +/** + * 异步任务管理器 + * + * @author ruoyi + */ +public class AsyncManager +{ + /** + * 操作延迟10毫秒 + */ + private final int OPERATE_DELAY_TIME = 10; + + /** + * 异步操作任务调度线程池 + */ + private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService"); + + /** + * 单例模式 + */ + private AsyncManager(){} + + private static AsyncManager me = new AsyncManager(); + + public static AsyncManager me() + { + return me; + } + + /** + * 执行任务 + * + * @param task 任务 + */ + public void execute(TimerTask task) + { + executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS); + } + + /** + * 停止任务线程池 + */ + public void shutdown() + { + Threads.shutdownAndAwaitTermination(executor); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/manager/ShutdownManager.java b/bnhz-framework/src/main/java/com/bnhz/framework/manager/ShutdownManager.java new file mode 100644 index 0000000..7eab2a4 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/manager/ShutdownManager.java @@ -0,0 +1,39 @@ +package com.bnhz.framework.manager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import javax.annotation.PreDestroy; + +/** + * 确保应用退出时能关闭后台线程 + * + * @author ruoyi + */ +@Component +public class ShutdownManager +{ + private static final Logger logger = LoggerFactory.getLogger("sys-user"); + + @PreDestroy + public void destroy() + { + shutdownAsyncManager(); + } + + /** + * 停止异步执行任务 + */ + private void shutdownAsyncManager() + { + try + { + logger.info("====关闭后台任务任务线程池===="); + AsyncManager.me().shutdown(); + } + catch (Exception e) + { + logger.error(e.getMessage(), e); + } + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/manager/factory/AsyncFactory.java b/bnhz-framework/src/main/java/com/bnhz/framework/manager/factory/AsyncFactory.java new file mode 100644 index 0000000..9ae7be0 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/manager/factory/AsyncFactory.java @@ -0,0 +1,102 @@ +package com.bnhz.framework.manager.factory; + +import java.util.TimerTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.utils.LogUtils; +import com.bnhz.common.utils.ServletUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.ip.AddressUtils; +import com.bnhz.common.utils.ip.IpUtils; +import com.bnhz.common.utils.spring.SpringUtils; +import com.bnhz.system.domain.SysLogininfor; +import com.bnhz.system.domain.SysOperLog; +import com.bnhz.system.service.ISysLogininforService; +import com.bnhz.system.service.ISysOperLogService; +import eu.bitwalker.useragentutils.UserAgent; + +/** + * 异步工厂(产生任务用) + * + * @author ruoyi + */ +public class AsyncFactory +{ + private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user"); + + /** + * 记录登录信息 + * + * @param username 用户名 + * @param status 状态 + * @param message 消息 + * @param args 列表 + * @return 任务task + */ + public static TimerTask recordLogininfor(final String username, final String status, final String message, + final Object... args) + { + final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + final String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); + return new TimerTask() + { + @Override + public void run() + { + String address = AddressUtils.getRealAddressByIP(ip); + StringBuilder s = new StringBuilder(); + s.append(LogUtils.getBlock(ip)); + s.append(address); + s.append(LogUtils.getBlock(username)); + s.append(LogUtils.getBlock(status)); + s.append(LogUtils.getBlock(message)); + // 打印信息到日志 + sys_user_logger.info(s.toString(), args); + // 获取客户端操作系统 + String os = userAgent.getOperatingSystem().getName(); + // 获取客户端浏览器 + String browser = userAgent.getBrowser().getName(); + // 封装对象 + SysLogininfor logininfor = new SysLogininfor(); + logininfor.setUserName(username); + logininfor.setIpaddr(ip); + logininfor.setLoginLocation(address); + logininfor.setBrowser(browser); + logininfor.setOs(os); + logininfor.setMsg(message); + // 日志状态 + if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) + { + logininfor.setStatus(Constants.SUCCESS); + } + else if (Constants.LOGIN_FAIL.equals(status)) + { + logininfor.setStatus(Constants.FAIL); + } + // 插入数据 + SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor); + } + }; + } + + /** + * 操作日志记录 + * + * @param operLog 操作日志信息 + * @return 任务task + */ + public static TimerTask recordOper(final SysOperLog operLog) + { + return new TimerTask() + { + @Override + public void run() + { + // 远程查询操作地点 + operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp())); + SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog); + } + }; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/LambdaQueryWrapperX.java b/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/LambdaQueryWrapperX.java new file mode 100644 index 0000000..a067c10 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/LambdaQueryWrapperX.java @@ -0,0 +1,136 @@ +package com.bnhz.framework.mybatis; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +/** + * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: + *

+ * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 + * + * @param 数据类型 + */ +public class LambdaQueryWrapperX extends LambdaQueryWrapper { + + public LambdaQueryWrapperX likeIfPresent(SFunction column, String val) { + if (StringUtils.hasText(val)) { + return (LambdaQueryWrapperX) super.like(column, val); + } + return this; + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Collection values) { + if (!CollectionUtils.isEmpty(values)) { + return (LambdaQueryWrapperX) super.in(column, values); + } + return this; + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Object... values) { + if (!ArrayUtil.isEmpty(values)) { + return (LambdaQueryWrapperX) super.in(column, values); + } + return this; + } + + public LambdaQueryWrapperX eqIfPresent(SFunction column, Object val) { + if (ObjectUtil.isNotEmpty(val)) { + return (LambdaQueryWrapperX) super.eq(column, val); + } + return this; + } + + public LambdaQueryWrapperX neIfPresent(SFunction column, Object val) { + if (ObjectUtil.isNotEmpty(val)) { + return (LambdaQueryWrapperX) super.ne(column, val); + } + return this; + } + + public LambdaQueryWrapperX gtIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.gt(column, val); + } + return this; + } + + public LambdaQueryWrapperX geIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.ge(column, val); + } + return this; + } + + public LambdaQueryWrapperX ltIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.lt(column, val); + } + return this; + } + + public LambdaQueryWrapperX leIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.le(column, val); + } + return this; + } + + public LambdaQueryWrapperX betweenIfPresent(SFunction column, Object val1, Object val2) { + if (val1 != null && val2 != null) { + return (LambdaQueryWrapperX) super.between(column, val1, val2); + } + if (val1 != null) { + return (LambdaQueryWrapperX) ge(column, val1); + } + if (val2 != null) { + return (LambdaQueryWrapperX) le(column, val2); + } + return this; + } + + public LambdaQueryWrapperX betweenIfPresent(SFunction column, Object[] values) { + Object val1 = ArrayUtils.get(values, 0); + Object val2 = ArrayUtils.get(values, 1); + return betweenIfPresent(column, val1, val2); + } + + // ========== 重写父类方法,方便链式调用 ========== + + @Override + public LambdaQueryWrapperX eq(boolean condition, SFunction column, Object val) { + super.eq(condition, column, val); + return this; + } + + @Override + public LambdaQueryWrapperX eq(SFunction column, Object val) { + super.eq(column, val); + return this; + } + + @Override + public LambdaQueryWrapperX orderByDesc(SFunction column) { + super.orderByDesc(true, column); + return this; + } + + @Override + public LambdaQueryWrapperX last(String lastSql) { + super.last(lastSql); + return this; + } + + @Override + public LambdaQueryWrapperX in(SFunction column, Collection coll) { + super.in(column, coll); + return this; + } + +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/QueryWrapperX.java b/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/QueryWrapperX.java new file mode 100644 index 0000000..b08720b --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/QueryWrapperX.java @@ -0,0 +1,140 @@ +package com.bnhz.framework.mybatis; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +/** + * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: + * + * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 + * + * @param 数据类型 + */ +public class QueryWrapperX extends QueryWrapper { + + public QueryWrapperX likeIfPresent(String column, String val) { + if (StringUtils.hasText(val)) { + return (QueryWrapperX) super.like(column, val); + } + return this; + } + + public QueryWrapperX inIfPresent(String column, Collection values) { + if (!CollectionUtils.isEmpty(values)) { + return (QueryWrapperX) super.in(column, values); + } + return this; + } + + public QueryWrapperX inIfPresent(String column, Object... values) { + if (!ArrayUtils.isEmpty(values)) { + return (QueryWrapperX) super.in(column, values); + } + return this; + } + + public QueryWrapperX eqIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.eq(column, val); + } + return this; + } + + public QueryWrapperX neIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.ne(column, val); + } + return this; + } + + public QueryWrapperX gtIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.gt(column, val); + } + return this; + } + + public QueryWrapperX geIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.ge(column, val); + } + return this; + } + + public QueryWrapperX ltIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.lt(column, val); + } + return this; + } + + public QueryWrapperX leIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.le(column, val); + } + return this; + } + + public QueryWrapperX betweenIfPresent(String column, Object val1, Object val2) { + if (val1 != null && val2 != null) { + return (QueryWrapperX) super.between(column, val1, val2); + } + if (val1 != null) { + return (QueryWrapperX) ge(column, val1); + } + if (val2 != null) { + return (QueryWrapperX) le(column, val2); + } + return this; + } + + public QueryWrapperX betweenIfPresent(String column, Object[] values) { + if (values!= null && values.length != 0 && values[0] != null && values[1] != null) { + return (QueryWrapperX) super.between(column, values[0], values[1]); + } + if (values!= null && values.length != 0 && values[0] != null) { + return (QueryWrapperX) ge(column, values[0]); + } + if (values!= null && values.length != 0 && values[1] != null) { + return (QueryWrapperX) le(column, values[1]); + } + return this; + } + + // ========== 重写父类方法,方便链式调用 ========== + + @Override + public QueryWrapperX eq(boolean condition, String column, Object val) { + super.eq(condition, column, val); + return this; + } + + @Override + public QueryWrapperX eq(String column, Object val) { + super.eq(column, val); + return this; + } + + @Override + public QueryWrapperX orderByDesc(String column) { + super.orderByDesc(true, column); + return this; + } + + @Override + public QueryWrapperX last(String lastSql) { + super.last(lastSql); + return this; + } + + @Override + public QueryWrapperX in(String column, Collection coll) { + super.in(column, coll); + return this; + } + +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/mapper/BaseMapperX.java b/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/mapper/BaseMapperX.java new file mode 100644 index 0000000..976e1ff --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/mapper/BaseMapperX.java @@ -0,0 +1,112 @@ +package com.bnhz.framework.mybatis.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import com.bnhz.common.core.domain.PageParam; +import com.bnhz.common.core.domain.PageResult; +import com.bnhz.framework.mybatis.utils.MyBatisUtils; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 + *

+ * 为什么继承 MPJBaseMapper 接口?支持 MyBatis Plus 多表 Join 的能力。 + */ +public interface BaseMapperX extends BaseMapper { + + default PageResult selectPage(PageParam pageParam, @Param("ew") Wrapper queryWrapper) { + // MyBatis Plus 查询 + IPage mpPage = MyBatisUtils.buildPage(pageParam); + selectPage(mpPage, queryWrapper); + // 转换返回 + return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); + } + + default T selectOne(String field, Object value) { + return selectOne(new QueryWrapper().eq(field, value)); + } + + default T selectOne(SFunction field, Object value) { + return selectOne(new LambdaQueryWrapper().eq(field, value)); + } + + default T selectOne(String field1, Object value1, String field2, Object value2) { + return selectOne(new QueryWrapper().eq(field1, value1).eq(field2, value2)); + } + + default T selectOne(SFunction field1, Object value1, SFunction field2, Object value2) { + return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2)); + } + + default Long selectCount() { + return selectCount(new QueryWrapper()); + } + + default Long selectCount(String field, Object value) { + return selectCount(new QueryWrapper().eq(field, value)); + } + + default Long selectCount(SFunction field, Object value) { + return selectCount(new LambdaQueryWrapper().eq(field, value)); + } + + default List selectList() { + return selectList(new QueryWrapper<>()); + } + + default List selectList(String field, Object value) { + return selectList(new QueryWrapper().eq(field, value)); + } + + default List selectList(SFunction field, Object value) { + return selectList(new LambdaQueryWrapper().eq(field, value)); + } + + default List selectList(String field, Collection values) { + return selectList(new QueryWrapper().in(field, values)); + } + + default List selectList(SFunction field, Collection values) { + return selectList(new LambdaQueryWrapper().in(field, values)); + } + + default List selectList(SFunction leField, SFunction geField, Object value) { + return selectList(new LambdaQueryWrapper().le(leField, value).ge(geField, value)); + } + + /** + * 批量插入,适合大量数据插入 + * + * @param entities 实体们 + */ + default void insertBatch(Collection entities) { + Db.saveBatch(entities); + } + + /** + * 批量插入,适合大量数据插入 + * + * @param entities 实体们 + * @param size 插入数量 Db.saveBatch 默认为 1000 + */ + default void insertBatch(Collection entities, int size) { + Db.saveBatch(entities, size); + } + + default void updateBatch(T update) { + update(update, new QueryWrapper<>()); + } + + default void updateBatch(Collection entities, int size) { + Db.updateBatchById(entities, size); + } + +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/utils/MyBatisUtils.java b/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/utils/MyBatisUtils.java new file mode 100644 index 0000000..2e40671 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/mybatis/utils/MyBatisUtils.java @@ -0,0 +1,88 @@ +package com.bnhz.framework.mybatis.utils; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bnhz.common.core.domain.PageParam; +import com.bnhz.common.core.domain.SortingField; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * MyBatis 工具类 + */ +public class MyBatisUtils { + + private static final String MYSQL_ESCAPE_CHARACTER = "`"; + + public static Page buildPage(PageParam pageParam) { + return buildPage(pageParam, null); + } + + public static Page buildPage(PageParam pageParam, Collection sortingFields) { + // 页码 + 数量 + Page page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize()); + // 排序字段 + if (!CollectionUtil.isEmpty(sortingFields)) { + page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? + OrderItem.asc(sortingField.getField()) : OrderItem.desc(sortingField.getField())) + .collect(Collectors.toList())); + } + return page; + } + + /** + * 将拦截器添加到链中 + * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 + * + * @param interceptor 链 + * @param inner 拦截器 + * @param index 位置 + */ + public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) { + List inners = new ArrayList<>(interceptor.getInterceptors()); + inners.add(index, inner); + interceptor.setInterceptors(inners); + } + + /** + * 获得 Table 对应的表名 + * + * 兼容 MySQL 转义表名 `t_xxx` + * + * @param table 表 + * @return 去除转移字符后的表名 + */ + public static String getTableName(Table table) { + String tableName = table.getName(); + if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) { + tableName = tableName.substring(1, tableName.length() - 1); + } + return tableName; + } + + /** + * 构建 Column 对象 + * + * @param tableName 表名 + * @param tableAlias 别名 + * @param column 字段名 + * @return Column 对象 + */ + public static Column buildColumn(String tableName, Alias tableAlias, String column) { + if (tableAlias != null) { + tableName = tableAlias.getName(); + } + return new Column(tableName + StringPool.DOT + column); + } + +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/security/context/AuthenticationContextHolder.java b/bnhz-framework/src/main/java/com/bnhz/framework/security/context/AuthenticationContextHolder.java new file mode 100644 index 0000000..5f1a726 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/security/context/AuthenticationContextHolder.java @@ -0,0 +1,28 @@ +package com.bnhz.framework.security.context; + +import org.springframework.security.core.Authentication; + +/** + * 身份验证信息 + * + * @author ruoyi + */ +public class AuthenticationContextHolder +{ + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public static Authentication getContext() + { + return contextHolder.get(); + } + + public static void setContext(Authentication context) + { + contextHolder.set(context); + } + + public static void clearContext() + { + contextHolder.remove(); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/security/context/PermissionContextHolder.java b/bnhz-framework/src/main/java/com/bnhz/framework/security/context/PermissionContextHolder.java new file mode 100644 index 0000000..5fd6c1e --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/security/context/PermissionContextHolder.java @@ -0,0 +1,27 @@ +package com.bnhz.framework.security.context; + +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import com.bnhz.common.core.text.Convert; + +/** + * 权限信息 + * + * @author ruoyi + */ +public class PermissionContextHolder +{ + private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT"; + + public static void setContext(String permission) + { + RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission, + RequestAttributes.SCOPE_REQUEST); + } + + public static String getContext() + { + return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES, + RequestAttributes.SCOPE_REQUEST)); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/security/filter/JwtAuthenticationTokenFilter.java b/bnhz-framework/src/main/java/com/bnhz/framework/security/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..cb4f339 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/security/filter/JwtAuthenticationTokenFilter.java @@ -0,0 +1,44 @@ +package com.bnhz.framework.security.filter; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.utils.SecurityUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.framework.web.service.TokenService; + +/** + * token过滤器 验证token有效性 + * + * @author ruoyi + */ +@Component +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter +{ + @Autowired + private TokenService tokenService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) + { + tokenService.verifyToken(loginUser); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + chain.doFilter(request, response); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/security/handle/AuthenticationEntryPointImpl.java b/bnhz-framework/src/main/java/com/bnhz/framework/security/handle/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..2da5d86 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/security/handle/AuthenticationEntryPointImpl.java @@ -0,0 +1,44 @@ +package com.bnhz.framework.security.handle; + +import java.io.IOException; +import java.io.Serializable; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; +import com.alibaba.fastjson2.JSON; +import com.bnhz.common.constant.HttpStatus; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.utils.ServletUtils; +import com.bnhz.common.utils.StringUtils; + +/** + * 认证失败处理类 返回未授权 + * + * @author ruoyi + */ +@Component +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable +{ + private static final long serialVersionUID = -8970718410437077606L; + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) + throws IOException + { +// if (isAjaxRequest(request)){ + int code = HttpStatus.UNAUTHORIZED; + String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI()); + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); +// }else { +// response.sendRedirect("/oauth/login"); +// } + } + + + public static boolean isAjaxRequest(HttpServletRequest request) { + String ajaxFlag = request.getHeader("X-Requested-With"); + return ajaxFlag != null && "XMLHttpRequest".equals(ajaxFlag); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/security/handle/LogoutSuccessHandlerImpl.java b/bnhz-framework/src/main/java/com/bnhz/framework/security/handle/LogoutSuccessHandlerImpl.java new file mode 100644 index 0000000..66a9a7b --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/security/handle/LogoutSuccessHandlerImpl.java @@ -0,0 +1,52 @@ +package com.bnhz.framework.security.handle; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import com.alibaba.fastjson2.JSON; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.utils.ServletUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.framework.manager.AsyncManager; +import com.bnhz.framework.manager.factory.AsyncFactory; +import com.bnhz.framework.web.service.TokenService; + +/** + * 自定义退出处理类 返回成功 + * + * @author ruoyi + */ +@Configuration +public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler +{ + @Autowired + private TokenService tokenService; + + /** + * 退出处理 + * + * @return + */ + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException + { + LoginUser loginUser = tokenService.getLoginUser(request); + if (StringUtils.isNotNull(loginUser)) + { + String userName = loginUser.getUsername(); + // 删除用户缓存记录 + tokenService.delLoginUser(loginUser.getToken()); + // 记录用户退出日志 + AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功")); + } + ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功"))); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/util/RedissonLockUtil.java b/bnhz-framework/src/main/java/com/bnhz/framework/util/RedissonLockUtil.java new file mode 100644 index 0000000..439f31d --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/util/RedissonLockUtil.java @@ -0,0 +1,36 @@ +package com.bnhz.framework.util; + +import lombok.AllArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * @author Leo + * @date 2024/8/19 15:07 + */ +@Component +@AllArgsConstructor +public class RedissonLockUtil { + + private final RedissonClient redissonClient; + + public boolean tryLock(String key, Long leaseTime) { + RLock lock = redissonClient.getLock(key); + if (lock.isLocked()) { + return false; + } + try { + if(lock.tryLock(0, leaseTime, TimeUnit.SECONDS)) { + return true; + }else { + return false; + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/Server.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/Server.java new file mode 100644 index 0000000..e9f2643 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/Server.java @@ -0,0 +1,240 @@ +package com.bnhz.framework.web.domain; + +import java.net.UnknownHostException; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import com.bnhz.common.utils.Arith; +import com.bnhz.common.utils.ip.IpUtils; +import com.bnhz.framework.web.domain.server.Cpu; +import com.bnhz.framework.web.domain.server.Jvm; +import com.bnhz.framework.web.domain.server.Mem; +import com.bnhz.framework.web.domain.server.Sys; +import com.bnhz.framework.web.domain.server.SysFile; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.CentralProcessor.TickType; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.FileSystem; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; +import oshi.util.Util; + +/** + * 服务器相关信息 + * + * @author ruoyi + */ +public class Server +{ + private static final int OSHI_WAIT_SECOND = 1000; + + /** + * CPU相关信息 + */ + private Cpu cpu = new Cpu(); + + /** + * 內存相关信息 + */ + private Mem mem = new Mem(); + + /** + * JVM相关信息 + */ + private Jvm jvm = new Jvm(); + + /** + * 服务器相关信息 + */ + private Sys sys = new Sys(); + + /** + * 磁盘相关信息 + */ + private List sysFiles = new LinkedList(); + + public Cpu getCpu() + { + return cpu; + } + + public void setCpu(Cpu cpu) + { + this.cpu = cpu; + } + + public Mem getMem() + { + return mem; + } + + public void setMem(Mem mem) + { + this.mem = mem; + } + + public Jvm getJvm() + { + return jvm; + } + + public void setJvm(Jvm jvm) + { + this.jvm = jvm; + } + + public Sys getSys() + { + return sys; + } + + public void setSys(Sys sys) + { + this.sys = sys; + } + + public List getSysFiles() + { + return sysFiles; + } + + public void setSysFiles(List sysFiles) + { + this.sysFiles = sysFiles; + } + + public void copyTo() throws Exception + { + SystemInfo si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + + setCpuInfo(hal.getProcessor()); + + setMemInfo(hal.getMemory()); + + setSysInfo(); + + setJvmInfo(); + + setSysFiles(si.getOperatingSystem()); + } + + /** + * 设置CPU信息 + */ + private void setCpuInfo(CentralProcessor processor) + { + // CPU信息 + long[] prevTicks = processor.getSystemCpuLoadTicks(); + Util.sleep(OSHI_WAIT_SECOND); + long[] ticks = processor.getSystemCpuLoadTicks(); + long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()]; + long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()]; + long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()]; + long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()]; + long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()]; + long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()]; + long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()]; + long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()]; + long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; + cpu.setCpuNum(processor.getLogicalProcessorCount()); + cpu.setTotal(totalCpu); + cpu.setSys(cSys); + cpu.setUsed(user); + cpu.setWait(iowait); + cpu.setFree(idle); + } + + /** + * 设置内存信息 + */ + private void setMemInfo(GlobalMemory memory) + { + mem.setTotal(memory.getTotal()); + mem.setUsed(memory.getTotal() - memory.getAvailable()); + mem.setFree(memory.getAvailable()); + } + + /** + * 设置服务器信息 + */ + private void setSysInfo() + { + Properties props = System.getProperties(); + sys.setComputerName(IpUtils.getHostName()); + sys.setComputerIp(IpUtils.getHostIp()); + sys.setOsName(props.getProperty("os.name")); + sys.setOsArch(props.getProperty("os.arch")); + sys.setUserDir(props.getProperty("user.dir")); + } + + /** + * 设置Java虚拟机 + */ + private void setJvmInfo() throws UnknownHostException + { + Properties props = System.getProperties(); + jvm.setTotal(Runtime.getRuntime().totalMemory()); + jvm.setMax(Runtime.getRuntime().maxMemory()); + jvm.setFree(Runtime.getRuntime().freeMemory()); + jvm.setVersion(props.getProperty("java.version")); + jvm.setHome(props.getProperty("java.home")); + } + + /** + * 设置磁盘信息 + */ + private void setSysFiles(OperatingSystem os) + { + FileSystem fileSystem = os.getFileSystem(); + List fsArray = fileSystem.getFileStores(); + for (OSFileStore fs : fsArray) + { + long free = fs.getUsableSpace(); + long total = fs.getTotalSpace(); + long used = total - free; + SysFile sysFile = new SysFile(); + sysFile.setDirName(fs.getMount()); + sysFile.setSysTypeName(fs.getType()); + sysFile.setTypeName(fs.getName()); + sysFile.setTotal(convertFileSize(total)); + sysFile.setFree(convertFileSize(free)); + sysFile.setUsed(convertFileSize(used)); + sysFile.setUsage(Arith.mul(Arith.div(used, total, 4), 100)); + sysFiles.add(sysFile); + } + } + + /** + * 字节转换 + * + * @param size 字节大小 + * @return 转换后值 + */ + public String convertFileSize(long size) + { + long kb = 1024; + long mb = kb * 1024; + long gb = mb * 1024; + if (size >= gb) + { + return String.format("%.1f GB", (float) size / gb); + } + else if (size >= mb) + { + float f = (float) size / mb; + return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f); + } + else if (size >= kb) + { + float f = (float) size / kb; + return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f); + } + else + { + return String.format("%d B", size); + } + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Cpu.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Cpu.java new file mode 100644 index 0000000..73b7690 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Cpu.java @@ -0,0 +1,101 @@ +package com.bnhz.framework.web.domain.server; + +import com.bnhz.common.utils.Arith; + +/** + * CPU相关信息 + * + * @author ruoyi + */ +public class Cpu +{ + /** + * 核心数 + */ + private int cpuNum; + + /** + * CPU总的使用率 + */ + private double total; + + /** + * CPU系统使用率 + */ + private double sys; + + /** + * CPU用户使用率 + */ + private double used; + + /** + * CPU当前等待率 + */ + private double wait; + + /** + * CPU当前空闲率 + */ + private double free; + + public int getCpuNum() + { + return cpuNum; + } + + public void setCpuNum(int cpuNum) + { + this.cpuNum = cpuNum; + } + + public double getTotal() + { + return Arith.round(Arith.mul(total, 100), 2); + } + + public void setTotal(double total) + { + this.total = total; + } + + public double getSys() + { + return Arith.round(Arith.mul(sys / total, 100), 2); + } + + public void setSys(double sys) + { + this.sys = sys; + } + + public double getUsed() + { + return Arith.round(Arith.mul(used / total, 100), 2); + } + + public void setUsed(double used) + { + this.used = used; + } + + public double getWait() + { + return Arith.round(Arith.mul(wait / total, 100), 2); + } + + public void setWait(double wait) + { + this.wait = wait; + } + + public double getFree() + { + return Arith.round(Arith.mul(free / total, 100), 2); + } + + public void setFree(double free) + { + this.free = free; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Jvm.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Jvm.java new file mode 100644 index 0000000..a3ef1b1 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Jvm.java @@ -0,0 +1,130 @@ +package com.bnhz.framework.web.domain.server; + +import java.lang.management.ManagementFactory; +import com.bnhz.common.utils.Arith; +import com.bnhz.common.utils.DateUtils; + +/** + * JVM相关信息 + * + * @author ruoyi + */ +public class Jvm +{ + /** + * 当前JVM占用的内存总数(M) + */ + private double total; + + /** + * JVM最大可用内存总数(M) + */ + private double max; + + /** + * JVM空闲内存(M) + */ + private double free; + + /** + * JDK版本 + */ + private String version; + + /** + * JDK路径 + */ + private String home; + + public double getTotal() + { + return Arith.div(total, (1024 * 1024), 2); + } + + public void setTotal(double total) + { + this.total = total; + } + + public double getMax() + { + return Arith.div(max, (1024 * 1024), 2); + } + + public void setMax(double max) + { + this.max = max; + } + + public double getFree() + { + return Arith.div(free, (1024 * 1024), 2); + } + + public void setFree(double free) + { + this.free = free; + } + + public double getUsed() + { + return Arith.div(total - free, (1024 * 1024), 2); + } + + public double getUsage() + { + return Arith.mul(Arith.div(total - free, total, 4), 100); + } + + /** + * 获取JDK名称 + */ + public String getName() + { + return ManagementFactory.getRuntimeMXBean().getVmName(); + } + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + + public String getHome() + { + return home; + } + + public void setHome(String home) + { + this.home = home; + } + + /** + * JDK启动时间 + */ + public String getStartTime() + { + return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.getServerStartDate()); + } + + /** + * JDK运行时间 + */ + public String getRunTime() + { + return DateUtils.getDatePoor(DateUtils.getNowDate(), DateUtils.getServerStartDate()); + } + + /** + * 运行参数 + */ + public String getInputArgs() + { + return ManagementFactory.getRuntimeMXBean().getInputArguments().toString(); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Mem.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Mem.java new file mode 100644 index 0000000..82ab4fb --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Mem.java @@ -0,0 +1,61 @@ +package com.bnhz.framework.web.domain.server; + +import com.bnhz.common.utils.Arith; + +/** + * 內存相关信息 + * + * @author ruoyi + */ +public class Mem +{ + /** + * 内存总量 + */ + private double total; + + /** + * 已用内存 + */ + private double used; + + /** + * 剩余内存 + */ + private double free; + + public double getTotal() + { + return Arith.div(total, (1024 * 1024 * 1024), 2); + } + + public void setTotal(long total) + { + this.total = total; + } + + public double getUsed() + { + return Arith.div(used, (1024 * 1024 * 1024), 2); + } + + public void setUsed(long used) + { + this.used = used; + } + + public double getFree() + { + return Arith.div(free, (1024 * 1024 * 1024), 2); + } + + public void setFree(long free) + { + this.free = free; + } + + public double getUsage() + { + return Arith.mul(Arith.div(used, total, 4), 100); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Sys.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Sys.java new file mode 100644 index 0000000..6b15303 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/Sys.java @@ -0,0 +1,84 @@ +package com.bnhz.framework.web.domain.server; + +/** + * 系统相关信息 + * + * @author ruoyi + */ +public class Sys +{ + /** + * 服务器名称 + */ + private String computerName; + + /** + * 服务器Ip + */ + private String computerIp; + + /** + * 项目路径 + */ + private String userDir; + + /** + * 操作系统 + */ + private String osName; + + /** + * 系统架构 + */ + private String osArch; + + public String getComputerName() + { + return computerName; + } + + public void setComputerName(String computerName) + { + this.computerName = computerName; + } + + public String getComputerIp() + { + return computerIp; + } + + public void setComputerIp(String computerIp) + { + this.computerIp = computerIp; + } + + public String getUserDir() + { + return userDir; + } + + public void setUserDir(String userDir) + { + this.userDir = userDir; + } + + public String getOsName() + { + return osName; + } + + public void setOsName(String osName) + { + this.osName = osName; + } + + public String getOsArch() + { + return osArch; + } + + public void setOsArch(String osArch) + { + this.osArch = osArch; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/SysFile.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/SysFile.java new file mode 100644 index 0000000..d2b9a5d --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/domain/server/SysFile.java @@ -0,0 +1,114 @@ +package com.bnhz.framework.web.domain.server; + +/** + * 系统文件相关信息 + * + * @author ruoyi + */ +public class SysFile +{ + /** + * 盘符路径 + */ + private String dirName; + + /** + * 盘符类型 + */ + private String sysTypeName; + + /** + * 文件类型 + */ + private String typeName; + + /** + * 总大小 + */ + private String total; + + /** + * 剩余大小 + */ + private String free; + + /** + * 已经使用量 + */ + private String used; + + /** + * 资源的使用率 + */ + private double usage; + + public String getDirName() + { + return dirName; + } + + public void setDirName(String dirName) + { + this.dirName = dirName; + } + + public String getSysTypeName() + { + return sysTypeName; + } + + public void setSysTypeName(String sysTypeName) + { + this.sysTypeName = sysTypeName; + } + + public String getTypeName() + { + return typeName; + } + + public void setTypeName(String typeName) + { + this.typeName = typeName; + } + + public String getTotal() + { + return total; + } + + public void setTotal(String total) + { + this.total = total; + } + + public String getFree() + { + return free; + } + + public void setFree(String free) + { + this.free = free; + } + + public String getUsed() + { + return used; + } + + public void setUsed(String used) + { + this.used = used; + } + + public double getUsage() + { + return usage; + } + + public void setUsage(double usage) + { + this.usage = usage; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/exception/GlobalExceptionHandler.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..6e885f4 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,120 @@ +package com.bnhz.framework.web.exception; + +import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import com.bnhz.common.constant.HttpStatus; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.exception.DemoModeException; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.utils.StringUtils; + +/** + * 全局异常处理器 + * + * @author ruoyi + */ +@RestControllerAdvice +public class GlobalExceptionHandler +{ + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 权限校验异常 + */ + @ExceptionHandler(AccessDeniedException.class) + public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage()); + return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权"); + } + + /** + * 请求方式不支持 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, + HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod()); + return AjaxResult.error(e.getMessage()); + } + + /** + * 业务异常 + */ + @ExceptionHandler(ServiceException.class) + public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) + { + log.error(e.getMessage(), e); + Integer code = e.getCode(); + return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage()); + } + + /** + * 拦截未知的运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生未知异常.", requestURI, e); + if (StringUtils.isEmpty(e.getMessage()) || e.getMessage().length() <= 20) { + return AjaxResult.error(e.getMessage()); + } + return AjaxResult.error("系统异常!"); + } + + /** + * 系统异常 + */ + @ExceptionHandler(Exception.class) + public AjaxResult handleException(Exception e, HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + log.error("请求地址'{}',发生系统异常.", requestURI, e); + if (StringUtils.isEmpty(e.getMessage()) || e.getMessage().length() <= 20) { + return AjaxResult.error(e.getMessage()); + } + return AjaxResult.error("系统异常!"); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(BindException.class) + public AjaxResult handleBindException(BindException e) + { + log.error(e.getMessage(), e); + String message = e.getAllErrors().get(0).getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) + { + log.error(e.getMessage(), e); + String message = e.getBindingResult().getFieldError().getDefaultMessage(); + return AjaxResult.error(message); + } + + /** + * 演示模式异常 + */ + @ExceptionHandler(DemoModeException.class) + public AjaxResult handleDemoModeException(DemoModeException e) + { + return AjaxResult.error("演示模式,不允许操作"); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/service/PermissionService.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/PermissionService.java new file mode 100644 index 0000000..089ad73 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/PermissionService.java @@ -0,0 +1,168 @@ +package com.bnhz.framework.web.service; + +import java.util.Set; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import com.bnhz.common.core.domain.entity.SysRole; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.utils.SecurityUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.framework.security.context.PermissionContextHolder; + +/** + * RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母 + * + * @author ruoyi + */ +@Service("ss") +public class PermissionService +{ + /** 所有权限标识 */ + private static final String ALL_PERMISSION = "*:*:*"; + + /** 管理员角色权限标识 */ + private static final String SUPER_ADMIN = "admin"; + + private static final String ROLE_DELIMETER = ","; + + private static final String PERMISSION_DELIMETER = ","; + + /** + * 验证用户是否具备某权限 + * + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + public boolean hasPermi(String permission) + { + if (StringUtils.isEmpty(permission)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + PermissionContextHolder.setContext(permission); + return hasPermissions(loginUser.getPermissions(), permission); + } + + /** + * 验证用户是否不具备某权限,与 hasPermi逻辑相反 + * + * @param permission 权限字符串 + * @return 用户是否不具备某权限 + */ + public boolean lacksPermi(String permission) + { + return hasPermi(permission) != true; + } + + /** + * 验证用户是否具有以下任意一个权限 + * + * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表 + * @return 用户是否具有以下任意一个权限 + */ + public boolean hasAnyPermi(String permissions) + { + if (StringUtils.isEmpty(permissions)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) + { + return false; + } + PermissionContextHolder.setContext(permissions); + Set authorities = loginUser.getPermissions(); + for (String permission : permissions.split(PERMISSION_DELIMETER)) + { + if (permission != null && hasPermissions(authorities, permission)) + { + return true; + } + } + return false; + } + + /** + * 判断用户是否拥有某个角色 + * + * @param role 角色字符串 + * @return 用户是否具备某角色 + */ + public boolean hasRole(String role) + { + if (StringUtils.isEmpty(role)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (SysRole sysRole : loginUser.getUser().getRoles()) + { + String roleKey = sysRole.getRoleKey(); + if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))) + { + return true; + } + } + return false; + } + + /** + * 验证用户是否不具备某角色,与 isRole逻辑相反。 + * + * @param role 角色名称 + * @return 用户是否不具备某角色 + */ + public boolean lacksRole(String role) + { + return hasRole(role) != true; + } + + /** + * 验证用户是否具有以下任意一个角色 + * + * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表 + * @return 用户是否具有以下任意一个角色 + */ + public boolean hasAnyRoles(String roles) + { + if (StringUtils.isEmpty(roles)) + { + return false; + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) + { + return false; + } + for (String role : roles.split(ROLE_DELIMETER)) + { + if (hasRole(role)) + { + return true; + } + } + return false; + } + + /** + * 判断是否包含权限 + * + * @param permissions 权限列表 + * @param permission 权限字符串 + * @return 用户是否具备某权限 + */ + private boolean hasPermissions(Set permissions, String permission) + { + return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysLoginService.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysLoginService.java new file mode 100644 index 0000000..415c6b2 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysLoginService.java @@ -0,0 +1,255 @@ +package com.bnhz.framework.web.service; + +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.enums.UserStatus; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.exception.user.CaptchaException; +import com.bnhz.common.exception.user.CaptchaExpireException; +import com.bnhz.common.exception.user.UserPasswordNotMatchException; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.MessageUtils; +import com.bnhz.common.utils.ServletUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.ip.IpUtils; +import com.bnhz.framework.manager.AsyncManager; +import com.bnhz.framework.manager.factory.AsyncFactory; +import com.bnhz.framework.security.context.AuthenticationContextHolder; +import com.bnhz.system.service.ISysConfigService; +import com.bnhz.system.service.ISysUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 登录校验方法 + * + * @author ruoyi + */ +@Component +public class SysLoginService +{ + @Autowired + private TokenService tokenService; + + @Resource + private AuthenticationManager authenticationManager; + + @Autowired + private RedisCache redisCache; + + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + + @Autowired + private UserDetailsServiceImpl userDetailsServiceImpl; + + @Resource + private SysPasswordService passwordService; + + /** + * 登录验证 + * + * @param username 用户名 + * @param password 密码 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public String login(String username, String password, String code, String uuid, Integer sourceType) + { + boolean captchaEnabled = configService.selectCaptchaEnabled(); + // 验证码开关 黑卡用户不用验证码 + if (captchaEnabled && !"blackcar".equals(username)) + { + validateCaptcha(username, code, uuid); + } + // 用户验证 + Authentication authentication = null; + try + { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + AuthenticationContextHolder.setContext(authenticationToken); + // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername + authentication = authenticationManager.authenticate(authenticationToken); + } + catch (Exception e) + { + if (e instanceof BadCredentialsException) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); + throw new ServiceException(e.getMessage()); + } + } + finally + { + AuthenticationContextHolder.clearContext(); + } + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + // 移动端、小程序限制终端用户登录 + Long deptId = loginUser.getDeptId(); +// if (null != sourceType && 1 == sourceType && null == deptId) { +// throw new ServiceException("web端只允许租户登录!"); +// } +// if (!"admin".equals(loginUser.getUsername()) && null != sourceType) { +// Long deptId = loginUser.getDeptId(); +// if (1 == sourceType && null == deptId) { +// throw new ServiceException("web端只允许租户登录!"); +// } +// if (1 != sourceType && null != deptId) { +// throw new ServiceException("只允许终端用户登录!"); +// } +// } + recordLoginInfo(loginUser.getUserId()); + // 生成token + return tokenService.createToken(loginUser); + } + + /** + * 第三方验证后,调用登录方法 + * @param username 用户名 + * @param password 密码 + * @return token + */ + public String socialLogin(String username, String password){ + // 用户验证 + Authentication authentication = null; + try + { + // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername + authentication = authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken(username, password)); + } + catch (Exception e) + { + if (e instanceof BadCredentialsException) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); + throw new UserPasswordNotMatchException(); + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); + throw new ServiceException(e.getMessage()); + } + } + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + recordLoginInfo(loginUser.getUserId()); + // 生成token + return tokenService.createToken(loginUser); + } + + /** + * 三方跳转登录认证方法 + * @param username 系统用户名 + * @param encodePwd 系统用户密码 + * @return + */ + public String redirectLogin(String username,String encodePwd){ +// UserDetails userDetails=userDetailsServiceImpl.loadUserByUsername(username); + SysUser user = userService.selectUserByUserName(username); + if (StringUtils.isNull(user)) + { + throw new ServiceException("登录用户:" + username + " 不存在"); + } + else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) + { + throw new ServiceException("对不起,您的账号:" + username + " 已被删除"); + } + else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) + { + throw new ServiceException("对不起,您的账号:" + username + " 已停用"); + } + // 重写验证方法 + passwordService.socialValidate(user, encodePwd); + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + UserDetails userDetails = userDetailsServiceImpl.createLoginUser(user); + LoginUser loginUser = (LoginUser) userDetails; + recordLoginInfo(loginUser.getUserId()); + // 生成token + return tokenService.createToken(loginUser); + + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + redisCache.deleteObject(verifyKey); + if (captcha == null) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); + throw new CaptchaExpireException(); + } + if (!code.equalsIgnoreCase(captcha)) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); + throw new CaptchaException(); + } + } + + /** + * 记录登录信息 + * + * @param userId 用户ID + */ + public void recordLoginInfo(Long userId) + { + SysUser sysUser = new SysUser(); + sysUser.setUserId(userId); + sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest())); + sysUser.setLoginDate(DateUtils.getNowDate()); + userService.updateUserProfile(sysUser); + } + + public String ssoLogin(String username, String password) { + SysUser user = userService.selectUserByUserName(username); + if (StringUtils.isNull(user)) + { + throw new ServiceException("登录用户:" + username + " 不存在"); + } + else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) + { + throw new ServiceException("对不起,您的账号:" + username + " 已被删除"); + } + else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) + { + throw new ServiceException("对不起,您的账号:" + username + " 已停用"); + } + // 重写验证方法 + passwordService.ssoValidate(user, password); + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); + UserDetails userDetails = userDetailsServiceImpl.createLoginUser(user); + LoginUser loginUser = (LoginUser) userDetails; + recordLoginInfo(loginUser.getUserId()); + // 生成token + return tokenService.createToken(loginUser); + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysPasswordService.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysPasswordService.java new file mode 100644 index 0000000..a8ee891 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysPasswordService.java @@ -0,0 +1,158 @@ +package com.bnhz.framework.web.service; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.exception.user.UserPasswordNotMatchException; +import com.bnhz.common.exception.user.UserPasswordRetryLimitExceedException; +import com.bnhz.common.utils.MessageUtils; +import com.bnhz.common.utils.SecurityUtils; +import com.bnhz.framework.manager.AsyncManager; +import com.bnhz.framework.manager.factory.AsyncFactory; +import com.bnhz.framework.security.context.AuthenticationContextHolder; + +/** + * 登录密码方法 + * + * @author ruoyi + */ +@Component +public class SysPasswordService +{ + @Autowired + private RedisCache redisCache; + + @Value(value = "${user.password.maxRetryCount}") + private int maxRetryCount; + + @Value(value = "${user.password.lockTime}") + private int lockTime; + + /** + * 登录账户密码错误次数缓存键名 + * + * @param username 用户名 + * @return 缓存键key + */ + private String getCacheKey(String username) + { + return CacheConstants.PWD_ERR_CNT_KEY + username; + } + + public void validate(SysUser user) + { + Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); + String username = usernamePasswordAuthenticationToken.getName(); + String password = usernamePasswordAuthenticationToken.getCredentials().toString(); + + Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); + + if (retryCount == null) + { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime))); + throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); + } + + if (!matches(user, password)) + { + retryCount = retryCount + 1; + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.count", retryCount))); + redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new UserPasswordNotMatchException(); + } + else + { + clearLoginRecordCache(username); + } + } + + public void socialValidate(SysUser user, String encodePwd) + { + String username = user.getUserName(); + String password = user.getPassword(); + + Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); + + if (retryCount == null) + { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime))); + throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); + } + + if(!password.equals(encodePwd)){ + retryCount = retryCount + 1; + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.count", retryCount))); + redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new UserPasswordNotMatchException(); + } + else + { + clearLoginRecordCache(username); + } + } + + public boolean matches(SysUser user, String rawPassword) + { + return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); + } + + public void clearLoginRecordCache(String loginName) + { + if (redisCache.hasKey(getCacheKey(loginName))) + { + redisCache.deleteObject(getCacheKey(loginName)); + } + } + + public void ssoValidate(SysUser user, String encodePwd) { + String username = user.getUserName(); + String password = user.getPassword(); + + Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); + + if (retryCount == null) + { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime))); + throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); + } + + if (!matches(user, encodePwd)) + { + retryCount = retryCount + 1; + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.count", retryCount))); + redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new UserPasswordNotMatchException(); + } + else + { + clearLoginRecordCache(username); + } + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysPermissionService.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysPermissionService.java new file mode 100644 index 0000000..13873fb --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysPermissionService.java @@ -0,0 +1,82 @@ +package com.bnhz.framework.web.service; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.bnhz.common.core.domain.entity.SysRole; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.system.service.ISysMenuService; +import com.bnhz.system.service.ISysRoleService; + +/** + * 用户权限处理 + * + * @author ruoyi + */ +@Component +public class SysPermissionService +{ + @Autowired + private ISysRoleService roleService; + + @Autowired + private ISysMenuService menuService; + + /** + * 获取角色数据权限 + * + * @param user 用户信息 + * @return 角色权限信息 + */ + public Set getRolePermission(SysUser user) + { + Set roles = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + roles.add("admin"); + } + else + { + roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId())); + } + return roles; + } + + /** + * 获取菜单数据权限 + * + * @param user 用户信息 + * @return 菜单权限信息 + */ + public Set getMenuPermission(SysUser user) + { + Set perms = new HashSet(); + // 管理员拥有所有权限 + if (user.isAdmin()) + { + perms.add("*:*:*"); + } + else + { + List roles = user.getRoles(); + if (!roles.isEmpty() && roles.size() > 1) + { + // 多角色设置permissions属性,以便数据权限匹配权限 + for (SysRole role : roles) + { + Set rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId()); + role.setPermissions(rolePerms); + perms.addAll(rolePerms); + } + } + else + { + perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId())); + } + } + return perms; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysRegisterService.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysRegisterService.java new file mode 100644 index 0000000..89dfe64 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/SysRegisterService.java @@ -0,0 +1,115 @@ +package com.bnhz.framework.web.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.constant.UserConstants; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.domain.model.RegisterBody; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.exception.user.CaptchaException; +import com.bnhz.common.exception.user.CaptchaExpireException; +import com.bnhz.common.utils.MessageUtils; +import com.bnhz.common.utils.SecurityUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.framework.manager.AsyncManager; +import com.bnhz.framework.manager.factory.AsyncFactory; +import com.bnhz.system.service.ISysConfigService; +import com.bnhz.system.service.ISysUserService; + +/** + * 注册校验方法 + * + * @author ruoyi + */ +@Component +public class SysRegisterService +{ + @Autowired + private ISysUserService userService; + + @Autowired + private ISysConfigService configService; + + @Autowired + private RedisCache redisCache; + + /** + * 注册 + */ + public String register(RegisterBody registerBody) + { + String msg = "", username = registerBody.getUsername(), password = registerBody.getPassword(); + SysUser sysUser = new SysUser(); + sysUser.setUserName(username); + + // 验证码开关 + boolean captchaEnabled = configService.selectCaptchaEnabled(); + if (captchaEnabled) + { + validateCaptcha(username, registerBody.getCode(), registerBody.getUuid()); + } + + if (StringUtils.isEmpty(username)) + { + msg = "用户名不能为空"; + } + else if (StringUtils.isEmpty(password)) + { + msg = "用户密码不能为空"; + } + else if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) + { + msg = "账户长度必须在2到20个字符之间"; + } + else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) + { + msg = "密码长度必须在5到20个字符之间"; + } + else if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(sysUser))) + { + msg = "保存用户'" + username + "'失败,注册账号已存在"; + } + else + { + sysUser.setNickName(username); + sysUser.setPassword(SecurityUtils.encryptPassword(password)); + boolean regFlag = userService.registerUser(sysUser); + if (!regFlag) + { + msg = "注册失败,请联系系统管理人员"; + } + else + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success"))); + } + } + return msg; + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) + { + String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); + String captcha = redisCache.getCacheObject(verifyKey); + redisCache.deleteObject(verifyKey); + if (captcha == null) + { + throw new CaptchaExpireException(); + } + if (!code.equalsIgnoreCase(captcha)) + { + throw new CaptchaException(); + } + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/service/TokenService.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/TokenService.java new file mode 100644 index 0000000..e6cccf1 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/TokenService.java @@ -0,0 +1,270 @@ +package com.bnhz.framework.web.service; + +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.constant.Constants; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.utils.ServletUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.ip.AddressUtils; +import com.bnhz.common.utils.ip.IpUtils; +import com.bnhz.common.utils.uuid.IdUtils; +import eu.bitwalker.useragentutils.UserAgent; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * token验证处理 + * + * @author ruoyi + */ +@Component +public class TokenService +{ + // 令牌自定义标识 + @Value("${token.header}") + private String header; + + // 令牌秘钥 + @Value("${token.secret}") + private String secret; + + // 令牌有效期(默认30分钟) + @Value("${token.expireTime}") + private int expireTime; + + protected static final long MILLIS_SECOND = 1000; + + protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; + + private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L; + + @Autowired + private RedisCache redisCache; + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUser(HttpServletRequest request) + { + // 获取请求携带的令牌 + String token = getToken(request); + if (StringUtils.isNotEmpty(token)) + { + try + { + Claims claims = parseToken(token); + // 解析对应的权限以及用户信息 + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String userKey = getTokenKey(uuid); + LoginUser user = redisCache.getCacheObject(userKey); + return user; + } + catch (Exception e) + { + } + } + return null; + } + + /** + * 获取用户身份信息 + * + * @return 用户信息 + */ + public LoginUser getLoginUserByToken(String token) { + if (StringUtils.isNotEmpty(token)) { + try { + Claims claims = parseToken(token); + // 解析对应的权限以及用户信息 + String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); + String userKey = getTokenKey(uuid); + LoginUser user = redisCache.getCacheObject(userKey); + return user; + } catch (Exception e) { + } + } + return null; + } + + /** + * 根据用户id获取用户身份信息 + * 由于多端登录,根据token获取的用户信息不一样,所以增加一个根据用户id获取用户信息的缓存key,以后多端需要获取用户最新信息就用这个方法吧 + * @return 用户信息 + */ + public LoginUser getLoginUserByUserId(Long userId) { + if (userId != null) { + try { + String userKey = getUserIdKey(userId); + return redisCache.getCacheObject(userKey); + } catch (Exception e) { + } + } + return null; + } + + /** + * 设置用户身份信息 + */ + public void setLoginUser(LoginUser loginUser) + { + if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) + { + refreshToken(loginUser); + } + } + + /** + * 删除用户身份信息 + */ + public void delLoginUser(String token) + { + if (StringUtils.isNotEmpty(token)) + { + String userKey = getTokenKey(token); + redisCache.deleteObject(userKey); + } + } + + /** + * 创建令牌 + * + * @param loginUser 用户信息 + * @return 令牌 + */ + public String createToken(LoginUser loginUser) + { + String token = IdUtils.fastUUID(); + loginUser.setToken(token); + setUserAgent(loginUser); + refreshToken(loginUser); + + Map claims = new HashMap<>(); + claims.put(Constants.LOGIN_USER_KEY, token); + return createToken(claims); + } + + /** + * 验证令牌有效期,相差不足20分钟,自动刷新缓存 + * + * @param loginUser + * @return 令牌 + */ + public void verifyToken(LoginUser loginUser) + { + long expireTime = loginUser.getExpireTime(); + long currentTime = System.currentTimeMillis(); + if (expireTime - currentTime <= MILLIS_MINUTE_TEN) + { + refreshToken(loginUser); + } + } + + /** + * 刷新令牌有效期 + * + * @param loginUser 登录信息 + */ + public void refreshToken(LoginUser loginUser) + { + loginUser.setLoginTime(System.currentTimeMillis()); + loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); + // 根据uuid将loginUser缓存 + String userKey = getTokenKey(loginUser.getToken()); + redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); + // 使用token作为用户信息缓存key,多端不能同步最新信息,需要重新登录,因此添加一个使用用户id作为缓存key + String userIdKey = getUserIdKey(loginUser.getUserId()); + redisCache.setCacheObject(userIdKey, loginUser, expireTime, TimeUnit.MINUTES); + } + + /** + * 设置用户代理信息 + * + * @param loginUser 登录信息 + */ + public void setUserAgent(LoginUser loginUser) + { + UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); + String ip = IpUtils.getIpAddr(ServletUtils.getRequest()); + loginUser.setIpaddr(ip); + loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip)); + loginUser.setBrowser(userAgent.getBrowser().getName()); + loginUser.setOs(userAgent.getOperatingSystem().getName()); + } + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + private String createToken(Map claims) + { + String token = Jwts.builder() + .setClaims(claims) + .signWith(SignatureAlgorithm.HS512, secret).compact(); + return token; + } + + /** + * 从令牌中获取数据声明 + * + * @param token 令牌 + * @return 数据声明 + */ + private Claims parseToken(String token) + { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从令牌中获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) + { + Claims claims = parseToken(token); + return claims.getSubject(); + } + + /** + * 获取请求token + * + * @param request + * @return token + */ + private String getToken(HttpServletRequest request) + { + String token = request.getHeader(header); + if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) + { + token = token.replace(Constants.TOKEN_PREFIX, ""); + } + return token; + } + + private String getTokenKey(String uuid) + { + return CacheConstants.LOGIN_TOKEN_KEY + uuid; + } + + private String getUserIdKey(Long userId) { + return CacheConstants.LOGIN_USERID_KEY + userId; + } +} diff --git a/bnhz-framework/src/main/java/com/bnhz/framework/web/service/UserDetailsServiceImpl.java b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..935e567 --- /dev/null +++ b/bnhz-framework/src/main/java/com/bnhz/framework/web/service/UserDetailsServiceImpl.java @@ -0,0 +1,65 @@ +package com.bnhz.framework.web.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.enums.UserStatus; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.system.service.ISysUserService; + +/** + * 用户验证处理 + * + * @author ruoyi + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService +{ + private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class); + + @Autowired + private ISysUserService userService; + + @Autowired + private SysPasswordService passwordService; + + @Autowired + private SysPermissionService permissionService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException + { + SysUser user = userService.selectUserByUserName(username); + if (StringUtils.isNull(user)) + { + log.info("登录用户:{} 不存在.", username); + throw new ServiceException("登录用户:" + username + " 不存在"); + } + else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) + { + log.info("登录用户:{} 已被删除.", username); + throw new ServiceException("对不起,您的账号:" + username + " 已被删除"); + } + else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) + { + log.info("登录用户:{} 已被停用.", username); + throw new ServiceException("对不起,您的账号:" + username + " 已停用"); + } + + passwordService.validate(user); + + return createLoginUser(user); + } + + public UserDetails createLoginUser(SysUser user) + { + return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user)); + } +} diff --git a/bnhz-gateway/bnhz-mq/pom.xml b/bnhz-gateway/bnhz-mq/pom.xml new file mode 100644 index 0000000..3ce1e57 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + bnhz-gateway + com.bnhz + 3.8.5 + + bnhz-mq + + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + 2.2.0 + + + + com.bnhz + bnhz-protocol-base + + + + com.bnhz + bnhz-protocol-collect + + + + com.bnhz + sip-server + + + + cn.hutool + hutool-all + + + + com.yomahub + liteflow-core + 2.11.3 + + + + com.yomahub + liteflow-spring + 2.11.3 + + + + com.bnhz + bnhz-notify-core + + + + + + diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/config/KafkaConfig.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/config/KafkaConfig.java new file mode 100644 index 0000000..9f056b3 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/config/KafkaConfig.java @@ -0,0 +1,20 @@ +package com.bnhz.mq.config; + +import org.apache.kafka.clients.admin.NewTopic; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static com.bnhz.common.constant.BnhzConstant.TOPIC.DEVICE_REPORT_TOPIC; + +/** + * @author Leo + * @date 2024/8/23 17:43 + */ +@Configuration +public class KafkaConfig { + + @Bean + public NewTopic initialTopic2() { + return new NewTopic(DEVICE_REPORT_TOPIC, 3, (short) 1); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/config/MqConfig.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/config/MqConfig.java new file mode 100644 index 0000000..b2da840 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/config/MqConfig.java @@ -0,0 +1,35 @@ +package com.bnhz.mq.config; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.mq.redischannel.service.RedisPublishServiceImpl; +import com.bnhz.mq.rocketmq.service.RocketMqPublishServiceImpl; +import com.bnhz.mq.service.IMessagePublishService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * mq集群配置 + * @author gsb + * @date 2022/10/10 8:27 + */ +@Configuration +//是否开启集群,默认不开启 +@ConditionalOnExpression("${cluster.enable:false}") +public class MqConfig { + + @Bean + @ConditionalOnProperty(prefix ="cluster", name = "type" ,havingValue = BnhzConstant.MQTT.REDIS_CHANNEL,matchIfMissing = true) + public IMessagePublishService redisChannelPublish(){ + return new RedisPublishServiceImpl(); + } + + //@Bean + @ConditionalOnProperty(prefix ="cluster", name = "type",havingValue = BnhzConstant.MQTT.ROCKET_MQ) + public IMessagePublishService rocketMqPublish() { + return new RocketMqPublishServiceImpl(); + } + + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/model/ReportDataBo.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/model/ReportDataBo.java new file mode 100644 index 0000000..c0e5337 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/model/ReportDataBo.java @@ -0,0 +1,44 @@ +package com.bnhz.mq.model; + +import com.bnhz.common.core.thingsModel.ThingsModelSimpleItem; +import com.bnhz.common.core.thingsModel.ThingsModelValuesInput; +import lombok.Data; + +import java.util.List; + +/** + * 上报数据模型bo + * @author bill + */ +@Data +public class ReportDataBo { + + /**产品id*/ + private Long productId; + /**设备编号*/ + private String serialNumber; + /**上报消息*/ + private String message; + /**上报的数据*/ + private List dataList; + /**设备影子*/ + private boolean isShadow; + /** + * 物模型类型 + * 1=属性,2=功能,3=事件,4=设备升级,5=设备上线,6=设备下线 + */ + private int type; + /**是否执行规则引擎*/ + private boolean isRuleEngine; + /**从机编号*/ + private Integer slaveId; + + + private Long userId; + private String userName; + private String deviceName; + + /*解析后组装好的数据*/ + private ThingsModelValuesInput valuesInput; + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/config/RedisConsumeConfig.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/config/RedisConsumeConfig.java new file mode 100644 index 0000000..c561d6e --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/config/RedisConsumeConfig.java @@ -0,0 +1,62 @@ +package com.bnhz.mq.redischannel.config; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.mq.redischannel.consumer.RedisChannelConsume; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.annotation.EnableCaching; +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.StringRedisTemplate; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; + +/** + * redisChannel配置 + * @author gsb + * @date 2022/10/10 8:57 + */ +@Configuration +@EnableCaching +@Slf4j +public class RedisConsumeConfig { + + @Bean + @ConditionalOnProperty(prefix ="cluster", name = "type" ,havingValue = BnhzConstant.MQTT.REDIS_CHANNEL,matchIfMissing = true) + RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, + MessageListenerAdapter listenerAdapter) { + + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.addMessageListener(listenerAdapter, new PatternTopic(BnhzConstant.CHANNEL.PROP_READ)); + container.addMessageListener(listenerAdapter, new PatternTopic(BnhzConstant.CHANNEL.FUNCTION_INVOKE)); + /*推送消息不需要关联ClientId,只需要处理推送数据, 即使本地服务不存在该客户端*/ + //container.addMessageListener(listenerAdapter, new PatternTopic(bnhzConstant.CHANNEL.PUBLISH)); + container.addMessageListener(listenerAdapter,new PatternTopic(BnhzConstant.CHANNEL.UPGRADE)); + //container.addMessageListener(listenerAdapter,new PatternTopic(bnhzConstant.CHANNEL.OTHER)); + //container.addMessageListener(listenerAdapter, new PatternTopic(bnhzConstant.CHANNEL.PUBLISH_ACK)); + //container.addMessageListener(listenerAdapter, new PatternTopic(bnhzConstant.CHANNEL.PUB_REC)); + //container.addMessageListener(listenerAdapter, new PatternTopic(bnhzConstant.CHANNEL.PUB_REL)); + //container.addMessageListener(listenerAdapter, new PatternTopic(bnhzConstant.CHANNEL.PUB_COMP)); + return container; + } + + /**配置消息监听类 默认监听方法onMessage*/ + @Bean + @ConditionalOnProperty(prefix ="cluster", name = "type" ,havingValue = BnhzConstant.MQTT.REDIS_CHANNEL,matchIfMissing = true) + MessageListenerAdapter listenerAdapter(RedisChannelConsume consume){ + return new MessageListenerAdapter(consume,"onMessage"); + } + + @Bean + @ConditionalOnProperty(prefix ="cluster", name = "type" ,havingValue = BnhzConstant.MQTT.REDIS_CHANNEL,matchIfMissing = true) + StringRedisTemplate template(RedisConnectionFactory connectionFactory){ + return new StringRedisTemplate(connectionFactory); + } + + + + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceOtherMsgConsumer.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceOtherMsgConsumer.java new file mode 100644 index 0000000..d729d17 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceOtherMsgConsumer.java @@ -0,0 +1,34 @@ +package com.bnhz.mq.redischannel.consumer; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.DeviceReportBo; +import com.bnhz.mq.service.impl.DeviceOtherMsgHandler; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * @author gsb + * @date 2023/2/27 14:33 + */ +@Slf4j +@Component +public class DeviceOtherMsgConsumer { + + @Resource + private DeviceOtherMsgHandler otherMsgHandler; + + @Async(BnhzConstant.TASK.DEVICE_OTHER_TASK) + public void consume(DeviceReportBo bo){ + try { + //处理emq订阅的非 property/post 属性上报的消息 ,因为其他消息量小,放在一起处理 + otherMsgHandler.messageHandler(bo); + }catch (Exception e){ + log.error("=>设备其他消息处理出错",e); + } + } + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DevicePropFetchConsumer.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DevicePropFetchConsumer.java new file mode 100644 index 0000000..0bb288a --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DevicePropFetchConsumer.java @@ -0,0 +1,71 @@ +package com.bnhz.mq.redischannel.consumer; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.message.DeviceDownMessage; +import com.bnhz.common.core.mq.message.PropRead; +import com.bnhz.common.core.protocol.Message; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.core.redis.RedisKeyBuilder; +import com.bnhz.common.utils.gateway.protocol.ByteUtils; +import com.bnhz.mq.service.impl.MessageManager; +import com.bnhz.mqttclient.PubMqttClient; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +/** + * 平台定时批量获取设备属性(或单个获取) + * @author bill + */ +@Slf4j +@Component +public class DevicePropFetchConsumer { + + + @Autowired + private PubMqttClient pubMqttClient; + @Autowired + private RedisCache redisCache; + @Resource + private MessageManager messageManager; + + + @Async(BnhzConstant.TASK.DEVICE_FETCH_PROP_TASK) + public void consume(DeviceDownMessage downMessage){ + execute(downMessage); + } + + private void execute(DeviceDownMessage message){ + try { + for (PropRead read : message.getValues()) { + //缓存值 + String cacheKey = RedisKeyBuilder.buildPropReadCacheKey(message.getSubCode()); + redisCache.setCacheObject(cacheKey, read, 1500, TimeUnit.MILLISECONDS); + switch (message.getServerType()){ + //通过mqtt内部客户端 下发指令 + case MQTT: + pubMqttClient.publish(message.getTopic(), ByteUtils.hexToByte(read.getData()), null); + log.info("=>MQTT-线程=[{}],轮询指令:[{}],主题:[{}]", Thread.currentThread().getName(), read.getData(), message.getTopic()); + break; + // 下发TCP客户端 + case TCP: + Message msg = new Message(); + msg.setClientId(message.getSerialNumber()); + msg.setPayload(Unpooled.wrappedBuffer(ByteBufUtil.decodeHexDump(read.getData()))); + messageManager.requestR(message.getSerialNumber(), msg,Message.class); + log.info("=>TCP-线程=[{}],轮询指令:[{}]", Thread.currentThread().getName(), read.getData()); + break; + } + Thread.sleep(1500); + } + }catch (Exception e){ + log.error("线程错误e",e); + } + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceReplyMsgConsumer.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceReplyMsgConsumer.java new file mode 100644 index 0000000..90d4eda --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceReplyMsgConsumer.java @@ -0,0 +1,44 @@ +package com.bnhz.mq.redischannel.consumer; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.DeviceReportBo; +import com.bnhz.mq.service.IDeviceReportMessageService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 设备消息回复 + * + * @author gsb + * @date 2022/10/10 11:12 + */ +@Component +@Slf4j +public class DeviceReplyMsgConsumer { + + + @Resource + private IDeviceReportMessageService deviceReportMessageService; + + + /*设备回调消息,统一用上报model*/ + @Async(BnhzConstant.TASK.DEVICE_REPLY_MESSAGE_TASK) + public void consume(DeviceReportBo bo) { + try { + String topicName = bo.getTopicName(); + + if (topicName.endsWith(BnhzConstant.TOPIC.MSG_REPLY)) { + //普通设备回复消息 + deviceReportMessageService.parseReplyMsg(bo); + } else if (topicName.endsWith(BnhzConstant.TOPIC.UPGRADE_REPLY)) { + //OTA升级的回复消息 + deviceReportMessageService.parseOTAUpdateReply(bo); + } + } catch (Exception e) { + log.error("=>设备回复消息消费异常", e); + } + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceReportMsgConsumer.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceReportMsgConsumer.java new file mode 100644 index 0000000..f1851ef --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceReportMsgConsumer.java @@ -0,0 +1,35 @@ +package com.bnhz.mq.redischannel.consumer; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.DeviceReportBo; +import com.bnhz.mq.service.IDeviceReportMessageService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 设备上报消息处理 + * + * @author bill + */ +@Slf4j +@Component +public class DeviceReportMsgConsumer { + + + @Autowired + private IDeviceReportMessageService reportMessageService; + + @Async(BnhzConstant.TASK.DEVICE_UP_MESSAGE_TASK) + public void consume(DeviceReportBo bo) { + try { + //处理数据解析 + reportMessageService.parseReportMsg(bo); + } catch (Exception e) { + log.error("设备主动上报队列监听异常", e); + } + } + + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceStatusConsumer.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceStatusConsumer.java new file mode 100644 index 0000000..b3ab98a --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/DeviceStatusConsumer.java @@ -0,0 +1,134 @@ +package com.bnhz.mq.redischannel.consumer; + +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.base.service.ISessionStore; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.DeviceStatusBo; +import com.bnhz.common.core.mq.MQSendMessageBo; +import com.bnhz.common.core.thingsModel.ThingsModelSimpleItem; +import com.bnhz.common.enums.DeviceStatus; +import com.bnhz.common.enums.ThingsModelType; +import com.bnhz.iot.domain.Device; +import com.bnhz.iot.domain.Product; +import com.bnhz.iot.model.ThingsModels.ThingsModelShadow; +import com.bnhz.iot.service.IDeviceService; +import com.bnhz.iot.service.IProductService; +import com.bnhz.iot.service.cache.IDeviceCache; +import com.bnhz.iot.util.SnowflakeIdWorker; +import com.bnhz.mq.model.ReportDataBo; +import com.bnhz.mq.redischannel.producer.MessageProducer; +import com.bnhz.mq.service.IMqttMessagePublish; +import com.bnhz.mq.service.IRuleEngine; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 设备状态消息处理类 + * + * @author gsb + * @date 2022/10/10 11:02 + */ +@Slf4j +@Component +public class DeviceStatusConsumer { + + @Autowired + private IDeviceCache deviceCache; + @Resource + private IRuleEngine ruleEngine; + @Resource + private IDeviceService deviceService; + @Resource + private IProductService productService; + @Resource + private ISessionStore sessionStore; + @Resource + private IMqttMessagePublish mqttMessagePublish; + @Value("${server.broker.enabled}") + private Boolean enabled; + private SnowflakeIdWorker snowflakeIdWorker = new SnowflakeIdWorker(2); + + public synchronized void consume(DeviceStatusBo bo) { + try { + Device device = deviceService.selectDeviceBySerialNumber(bo.getSerialNumber()); + if (ObjectUtils.isEmpty(device)) { + log.error("未找到对应设备,设备编号:{}", bo.getSerialNumber()); + return; + } + if (enabled) { //如果使用Netty版本 + boolean containsKey = sessionStore.containsKey(bo.getSerialNumber()); + boolean isOnline = device.getStatus() == 3; + log.info("=>session:{},数据库:{},更新状态:{}", containsKey, isOnline, bo.getStatus().getCode()); + if (containsKey && !isOnline) { + //如果session存在,但数据库状态不在线,则以session为准 + bo.setStatus(DeviceStatus.ONLINE); + } + if (!containsKey && isOnline) { + bo.setStatus(DeviceStatus.OFFLINE); + } + } + /*更新设备状态*/ + deviceCache.updateDeviceStatusCache(bo, device); + //处理影子模式值 + this.handlerShadow(device, bo.getStatus()); + //设备上下线执行规则引擎 + ReportDataBo dataBo = new ReportDataBo(); + dataBo.setRuleEngine(true); + dataBo.setProductId(device.getProductId()); + dataBo.setType(bo.getStatus().equals(DeviceStatus.ONLINE) ? 5 : 6); + dataBo.setSerialNumber(bo.getSerialNumber()); + ruleEngine.ruleMatch(dataBo); + } catch (Exception e) { + log.error("=>设备状态处理异常", e); + } + } + + private void handlerShadow(Device device, DeviceStatus status) { + //获取设备协议编码 + Product product = productService.selectProductByProductId(device.getProductId()); + /* 设备上线 处理影子值*/ + if (!ObjectUtils.isEmpty(status) && status.equals(DeviceStatus.ONLINE) && !ObjectUtils.isEmpty(device.getIsShadow()) && device.getIsShadow() == 1) { + ThingsModelShadow shadow = deviceService.getDeviceShadowThingsModel(device); + List properties = shadow.getProperties(); + List functions = shadow.getFunctions(); + //JsonArray组合发送 + if (BnhzConstant.PROTOCOL.JsonArray.equals(product.getProtocolCode())) { + if (!CollectionUtils.isEmpty(properties)) { + mqttMessagePublish.publishProperty(device.getProductId(), device.getSerialNumber(), properties, 3); + } + if (!CollectionUtils.isEmpty(functions)) { + mqttMessagePublish.publishFunction(device.getProductId(), device.getSerialNumber(), functions, 3); + } + } else { //其他协议单个发送 + functions.addAll(properties); + if (!CollectionUtils.isEmpty(functions)) { + for (ThingsModelSimpleItem function : functions) { + MQSendMessageBo bo = new MQSendMessageBo(); + bo.setTransport(product.getTransport()); + bo.setProtocolCode(product.getProtocolCode()); + bo.setIsShadow(false); + bo.setProductId(product.getProductId()); + bo.setIdentifier(function.getId()); + bo.setSerialNumber(device.getSerialNumber()); + bo.setType(ThingsModelType.SERVICE); + JSONObject jsonObject = new JSONObject(); + jsonObject.put(function.getId(), function.getValue()); + bo.setValue(jsonObject); + long id = snowflakeIdWorker.nextId(); + bo.setMessageId(id + ""); + bo.setSlaveId(function.getSlaveId()); + //发送到MQ处理 + MessageProducer.sendFunctionInvoke(bo); + } + } + } + } + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/FunctionInvokeConsumer.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/FunctionInvokeConsumer.java new file mode 100644 index 0000000..80046eb --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/FunctionInvokeConsumer.java @@ -0,0 +1,43 @@ +package com.bnhz.mq.redischannel.consumer; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.MQSendMessageBo; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.iot.domain.Device; +import com.bnhz.iot.service.IDeviceService; +import com.bnhz.mq.service.IMqttMessagePublish; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * 指令(服务)下发处理类 + * + * @author gsb + * @date 2022/10/11 8:17 + */ +@Slf4j +@Component +public class FunctionInvokeConsumer { + + + @Autowired + private IMqttMessagePublish functionSendService; + @Autowired + private IDeviceService deviceService; + + @Async(BnhzConstant.TASK.FUNCTION_INVOKE_TASK) + public void handler(MQSendMessageBo bo) { + try { + Device device = deviceService.selectDeviceBySerialNumber(bo.getSerialNumber()); + Optional.ofNullable(device).orElseThrow(()->new ServiceException("服务下发的设备:["+bo.getSerialNumber()+"]不存在")); + //处理数据下发 + functionSendService.funcSend(bo); + } catch (Exception e) { + log.error("=>服务下发异常", e); + } + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/RedisChannelConsume.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/RedisChannelConsume.java new file mode 100644 index 0000000..e6b19a8 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/consumer/RedisChannelConsume.java @@ -0,0 +1,58 @@ +package com.bnhz.mq.redischannel.consumer; + +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.MQSendMessageBo; +import com.bnhz.common.core.mq.message.DeviceDownMessage; +import com.bnhz.common.core.mq.ota.OtaUpgradeBo; +import com.bnhz.mq.redischannel.queue.DevicePropFetchQueue; +import com.bnhz.mq.redischannel.queue.FunctionInvokeQueue; +import com.bnhz.mq.redischannel.queue.OtaUpgradeQueue; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.stereotype.Component; + +/** + * redisChannel消息监听 + * + * @author gsb + * @date 2022/10/10 9:17 + */ +@Component +@Slf4j +public class RedisChannelConsume implements MessageListener { + + /** + * 监听推送消息 + */ + @Override + public void onMessage(Message message, byte[] pattern) { + try { + /*获取channel*/ + String channel = new String(message.getChannel()); + /*获取消息*/ + String body = new String(message.getBody()); + switch (channel) { + case BnhzConstant.CHANNEL.PROP_READ: + DeviceDownMessage downMessage = JSONObject.parseObject(body, DeviceDownMessage.class); + DevicePropFetchQueue.offer(downMessage); + break; + case BnhzConstant.CHANNEL.FUNCTION_INVOKE: + MQSendMessageBo sendBo = JSONObject.parseObject(body, MQSendMessageBo.class); + FunctionInvokeQueue.offer(sendBo); + break; + case BnhzConstant.CHANNEL.UPGRADE: + OtaUpgradeBo upgradeBo = JSONObject.parseObject(body, OtaUpgradeBo.class); + OtaUpgradeQueue.offer(upgradeBo); + break; + + default: + log.error("=>未知消息类型,channel:[{}]", channel); + break; + } + } catch (Exception e) { + log.error("=>redisChannel处理消息异常,e", e); + } + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceOtherListen.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceOtherListen.java new file mode 100644 index 0000000..bccfc32 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceOtherListen.java @@ -0,0 +1,35 @@ +package com.bnhz.mq.redischannel.listen; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.DeviceReportBo; +import com.bnhz.mq.redischannel.consumer.DeviceOtherMsgConsumer; +import com.bnhz.mq.redischannel.queue.DeviceOtherQueue; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * @author gsb + * @date 2023/2/28 10:02 + */ +@Slf4j +@Component +public class DeviceOtherListen { + + @Resource + private DeviceOtherMsgConsumer otherMsgConsumer; + + @Async(BnhzConstant.TASK.DEVICE_OTHER_TASK) + public void listen(){ + while (true){ + try { + DeviceReportBo reportBo = DeviceOtherQueue.take(); + otherMsgConsumer.consume(reportBo); + }catch (Exception e){ + log.error("=>emq数据转发异常"); + } + } + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DevicePropFetchListen.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DevicePropFetchListen.java new file mode 100644 index 0000000..c1cc399 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DevicePropFetchListen.java @@ -0,0 +1,37 @@ +package com.bnhz.mq.redischannel.listen; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.message.DeviceDownMessage; +import com.bnhz.mq.redischannel.consumer.DevicePropFetchConsumer; +import com.bnhz.mq.redischannel.queue.DevicePropFetchQueue; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 设备属性获取(定时获取)监听 + * + * @author gsb + * @date 2022/10/11 8:26 + */ +@Slf4j +@Component +public class DevicePropFetchListen { + + @Autowired + private DevicePropFetchConsumer devicePropFetchConsumer; + + @Async(BnhzConstant.TASK.MESSAGE_CONSUME_TASK_FETCH) + public void listen() { + while (true) { + try { + DeviceDownMessage downMessage = DevicePropFetchQueue.take(); + devicePropFetchConsumer.consume(downMessage); + Thread.sleep(200); + } catch (Exception e) { + log.error("=>设备属性获取异常", e); + } + } + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceReplyListen.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceReplyListen.java new file mode 100644 index 0000000..98ceb24 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceReplyListen.java @@ -0,0 +1,37 @@ +package com.bnhz.mq.redischannel.listen; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.DeviceReportBo; +import com.bnhz.mq.redischannel.consumer.DeviceReplyMsgConsumer; +import com.bnhz.mq.redischannel.queue.DeviceReplyQueue; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 设备回调消息监听 + * + * @author bill + */ +@Slf4j +@Component +public class DeviceReplyListen { + + @Autowired + private DeviceReplyMsgConsumer deviceReplyMsgHandler; + + @Async(BnhzConstant.TASK.MESSAGE_CONSUME_TASK_PUB) + public void listen() { + while (true) { + try { + /*读队列消息*/ + DeviceReportBo reportBo = DeviceReplyQueue.take(); + /*处理消息*/ + deviceReplyMsgHandler.consume(reportBo); + } catch (Exception e) { + log.error("=>设备回调消息监听异常", e); + } + } + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceReportListen.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceReportListen.java new file mode 100644 index 0000000..8d7ef55 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceReportListen.java @@ -0,0 +1,51 @@ +package com.bnhz.mq.redischannel.listen; + +import com.alibaba.fastjson.JSONObject; +import com.bnhz.common.core.mq.DeviceReportBo; +import com.bnhz.framework.util.RedissonLockUtil; +import com.bnhz.mq.redischannel.consumer.DeviceReportMsgConsumer; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import static com.bnhz.common.constant.BnhzConstant.TOPIC.DEVICE_REPORT_TOPIC; + +/** + * 设备主动上报消息监听 + * + * @author bill + */ +@Slf4j +@Component +@AllArgsConstructor +public class DeviceReportListen { + + private final DeviceReportMsgConsumer reportMsgConsumer; + + private final RedissonLockUtil redissonLockUtil; + + + + + @KafkaListener(topics = DEVICE_REPORT_TOPIC) + public void listen(String msg) { + try { + /*取出数据*/ + DeviceReportBo reportBo = JSONObject.parseObject(msg, DeviceReportBo.class); + //一分钟内放重复消费 + boolean lock = redissonLockUtil.tryLock(reportBo.getMessageId(), 60L); + if (lock) { + /*处理数据*/ + reportMsgConsumer.consume(reportBo); + } else { + log.warn("重复数据不能进行消费=====>:{}", reportBo.getMessageId()); + } + + } catch (Exception e) { + log.error("=>设备上报数据监听异常", e); + } + } + + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceStatusListen.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceStatusListen.java new file mode 100644 index 0000000..94dd5ab --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/DeviceStatusListen.java @@ -0,0 +1,35 @@ +package com.bnhz.mq.redischannel.listen; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.DeviceStatusBo; +import com.bnhz.mq.redischannel.consumer.DeviceStatusConsumer; +import com.bnhz.mq.redischannel.queue.DeviceStatusQueue; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 设备状态监听 + * @author bill + */ +@Slf4j +@Component +public class DeviceStatusListen { + + @Autowired + private DeviceStatusConsumer deviceStatusConsumer; + + @Async(BnhzConstant.TASK.MESSAGE_CONSUME_TASK) + public void listen() { + try { + while (true) { + DeviceStatusBo status = DeviceStatusQueue.take(); + deviceStatusConsumer.consume(status); + } + } catch (Exception e) { + log.error("设备状态监听错误", e); + } + } + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/FunctionInvokeListen.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/FunctionInvokeListen.java new file mode 100644 index 0000000..f51a0b9 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/FunctionInvokeListen.java @@ -0,0 +1,35 @@ +package com.bnhz.mq.redischannel.listen; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.MQSendMessageBo; +import com.bnhz.mq.redischannel.consumer.FunctionInvokeConsumer; +import com.bnhz.mq.redischannel.queue.FunctionInvokeQueue; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 设备服务下发监听 + * + * @author bill + */ +@Slf4j +@Component +public class FunctionInvokeListen { + + @Autowired + private FunctionInvokeConsumer functionInvokeConsumer; + + @Async(BnhzConstant.TASK.MESSAGE_CONSUME_TASK) + public void listen() { + while (true) { + try { + MQSendMessageBo sendBo = FunctionInvokeQueue.take(); + functionInvokeConsumer.handler(sendBo); + } catch (Exception e) { + log.error("=>下发服务消费异常", e); + } + } + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/UpgradeListen.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/UpgradeListen.java new file mode 100644 index 0000000..38404bc --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/listen/UpgradeListen.java @@ -0,0 +1,38 @@ +package com.bnhz.mq.redischannel.listen; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.ota.OtaUpgradeBo; +import com.bnhz.mq.redischannel.queue.OtaUpgradeQueue; +import com.bnhz.mq.service.IMqttMessagePublish; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +/** + * OTA升级消息监听 + * + * @author gsb + * @date 2022/10/11 8:36 + */ +@Slf4j +@Service +public class UpgradeListen { + + @Autowired + private IMqttMessagePublish functionSendService; + + @Async(BnhzConstant.TASK.MESSAGE_CONSUME_TASK) + public void listen() { + while (true) { + try { + /*获取队列中的OTA升级消息*/ + OtaUpgradeBo upgradeBo = OtaUpgradeQueue.take(); + // OTA升级处理 + functionSendService.upGradeOTA(upgradeBo); + } catch (Exception e) { + log.error("->OTA消息监听异常", e); + } + } + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/producer/EmqxMessageProducer.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/producer/EmqxMessageProducer.java new file mode 100644 index 0000000..e6fb359 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/producer/EmqxMessageProducer.java @@ -0,0 +1,22 @@ +package com.bnhz.mq.redischannel.producer; + +import com.bnhz.common.core.mq.DeviceReportBo; +import com.bnhz.mqttclient.IEmqxMessageProducer; +import org.springframework.stereotype.Component; + +/** + * @author bill + */ +@Component +public class EmqxMessageProducer implements IEmqxMessageProducer { + + + @Override + public void sendEmqxMessage(String topicName, DeviceReportBo deviceReportBo) { + if (topicName.contains("property/post")){ + MessageProducer.sendPublishMsg(deviceReportBo); + }else { + MessageProducer.sendOtherMsg(deviceReportBo); + } + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/producer/MessageProducer.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/producer/MessageProducer.java new file mode 100644 index 0000000..6584103 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/producer/MessageProducer.java @@ -0,0 +1,36 @@ +package com.bnhz.mq.redischannel.producer; + +import com.bnhz.common.core.mq.DeviceReportBo; +import com.bnhz.common.core.mq.DeviceStatusBo; +import com.bnhz.common.core.mq.MQSendMessageBo; +import com.bnhz.common.core.mq.message.DeviceDownMessage; +import com.bnhz.mq.redischannel.queue.*; + +/** + *设备消息生产者 ,设备的消息发送通道 + * @author bill + */ +public class MessageProducer { + + /*发送设备获取属性消息到队列*/ + public static void sendPropFetch(DeviceDownMessage bo){ + DevicePropFetchQueue.offer(bo); + } + /*发送设备服务下发消息到队列*/ + public static void sendFunctionInvoke(MQSendMessageBo bo){ + FunctionInvokeQueue.offer(bo); + } + /*发送设备上报消息到队列*/ + public static void sendPublishMsg(DeviceReportBo bo){ + DeviceReportQueue.offer(bo); + } + public static void sendOtherMsg(DeviceReportBo bo){ + DeviceOtherQueue.offer(bo); + } + + public static void sendStatusMsg(DeviceStatusBo bo){ + DeviceStatusQueue.offer(bo); + } + + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceOtherQueue.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceOtherQueue.java new file mode 100644 index 0000000..a75eab9 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceOtherQueue.java @@ -0,0 +1,25 @@ +package com.bnhz.mq.redischannel.queue; + +import com.bnhz.common.core.mq.DeviceReportBo; +import lombok.SneakyThrows; + +import java.util.concurrent.LinkedBlockingQueue; + +/** + * @author gsb + * @date 2022/10/10 10:13 + */ +public class DeviceOtherQueue { + + private static final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + + /*元素加入队列,最后*/ + public static void offer(DeviceReportBo dto){ + queue.offer(dto); + } + /*取出队列元素 先进先出*/ + @SneakyThrows + public static DeviceReportBo take(){ + return queue.take(); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DevicePropFetchQueue.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DevicePropFetchQueue.java new file mode 100644 index 0000000..9b068ad --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DevicePropFetchQueue.java @@ -0,0 +1,25 @@ +package com.bnhz.mq.redischannel.queue; + +import com.bnhz.common.core.mq.message.DeviceDownMessage; +import lombok.SneakyThrows; + +import java.util.concurrent.LinkedBlockingQueue; + +/** + * 设备属性获取存储列队 + * @author gsb + * @date 2022/10/11 8:29 + */ +public class DevicePropFetchQueue { + private static final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + + /*元素加入队列,最后*/ + public static void offer(DeviceDownMessage dto){ + queue.offer(dto); + } + /*取出队列元素 先进先出*/ + @SneakyThrows + public static DeviceDownMessage take(){ + return queue.take(); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceReplyQueue.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceReplyQueue.java new file mode 100644 index 0000000..8fa43ca --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceReplyQueue.java @@ -0,0 +1,25 @@ +package com.bnhz.mq.redischannel.queue; + +import com.bnhz.common.core.mq.DeviceReportBo; +import lombok.SneakyThrows; + +import java.util.concurrent.LinkedBlockingQueue; + +/** + * 设备消息回调队列 {@link DeviceReportBo} + * @author gsb + * @date 2022/10/10 10:15 + */ +public class DeviceReplyQueue { + private static final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + + /*元素加入队列,最后*/ + public static void offer(DeviceReportBo dto){ + queue.offer(dto); + } + /*取出队列元素 先进先出*/ + @SneakyThrows + public static DeviceReportBo take(){ + return queue.take(); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceReportQueue.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceReportQueue.java new file mode 100644 index 0000000..51b6f87 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceReportQueue.java @@ -0,0 +1,25 @@ +package com.bnhz.mq.redischannel.queue; + +import com.bnhz.common.core.mq.DeviceReportBo; +import lombok.SneakyThrows; + +import java.util.concurrent.LinkedBlockingQueue; + +/** + * @author gsb + * @date 2022/10/10 10:13 + */ +public class DeviceReportQueue { + + private static final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + + /*元素加入队列,最后*/ + public static void offer(DeviceReportBo dto){ + queue.offer(dto); + } + /*取出队列元素 先进先出*/ + @SneakyThrows + public static DeviceReportBo take(){ + return queue.take(); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceStatusQueue.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceStatusQueue.java new file mode 100644 index 0000000..c475ada --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/DeviceStatusQueue.java @@ -0,0 +1,25 @@ +package com.bnhz.mq.redischannel.queue; + +import com.bnhz.common.core.mq.DeviceStatusBo; +import lombok.SneakyThrows; + +import java.util.concurrent.LinkedBlockingQueue; + +/** + * 设备消息缓存队列 添加{@link DeviceStatusBo} 消息 + * @author gsb + * @date 2022/10/10 9:59 + */ +public class DeviceStatusQueue { + private static final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + + /*元素加入队列,最后*/ + public static void offer(DeviceStatusBo dto){ + queue.offer(dto); + } + /*取出队列元素 先进先出*/ + @SneakyThrows + public static DeviceStatusBo take(){ + return queue.take(); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/FunctionInvokeQueue.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/FunctionInvokeQueue.java new file mode 100644 index 0000000..8e8fe6f --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/FunctionInvokeQueue.java @@ -0,0 +1,25 @@ +package com.bnhz.mq.redischannel.queue; + +import com.bnhz.common.core.mq.MQSendMessageBo; +import lombok.SneakyThrows; + +import java.util.concurrent.LinkedBlockingQueue; + +/** + * 服务下发队列 处理{@link MQSendMessageBo} + * @author gsb + * @date 2022/10/10 10:11 + */ +public class FunctionInvokeQueue { + private static final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + + /*元素加入队列,最后*/ + public static void offer(MQSendMessageBo dto){ + queue.offer(dto); + } + /*取出队列元素 先进先出*/ + @SneakyThrows + public static MQSendMessageBo take(){ + return queue.take(); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/OtaUpgradeQueue.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/OtaUpgradeQueue.java new file mode 100644 index 0000000..5b9774f --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/queue/OtaUpgradeQueue.java @@ -0,0 +1,25 @@ +package com.bnhz.mq.redischannel.queue; + +import com.bnhz.common.core.mq.ota.OtaUpgradeBo; +import lombok.SneakyThrows; + +import java.util.concurrent.LinkedBlockingQueue; + +/** + * OTA升级列队 {@link OtaUpgradeBo} + * @author gsb + * @date 2022/10/10 10:30 + */ +public class OtaUpgradeQueue { + private static final LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + + /*元素加入队列,最后*/ + public static void offer(OtaUpgradeBo dto){ + queue.offer(dto); + } + /*取出队列元素 先进先出*/ + @SneakyThrows + public static OtaUpgradeBo take(){ + return queue.take(); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/service/RedisPublishServiceImpl.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/service/RedisPublishServiceImpl.java new file mode 100644 index 0000000..ae18564 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/redischannel/service/RedisPublishServiceImpl.java @@ -0,0 +1,27 @@ +package com.bnhz.mq.redischannel.service; + +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.mq.service.IMessagePublishService; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * 设备消息推送至RedisChannel + * @author bill + */ +@NoArgsConstructor +public class RedisPublishServiceImpl implements IMessagePublishService { + + @Autowired + private RedisCache redisCache; + + /** + * 消息推送到redisChannel + * @param message 设备消息 + * @param channel 推送channel + */ + @Override + public void publish(Object message,String channel) { + redisCache.publish(message,channel); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/consumer/ConsumerTopicConstant.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/consumer/ConsumerTopicConstant.java new file mode 100644 index 0000000..24f3d4f --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/consumer/ConsumerTopicConstant.java @@ -0,0 +1,27 @@ +package com.bnhz.mq.rocketmq.consumer; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author bill + */ +@Component +@ConfigurationProperties(prefix = "rocketmq.producer") +@Data +public class ConsumerTopicConstant { + + /**网关默认主题*/ + private String mqTopic; + /*设备状态topic*/ + private String deviceStatusTopic; + /*设备主动上报topic*/ + private String deviceUpTopic; + /*设备服务下发topic*/ + private String functionInvokeTopic; + /*设备消息回调topic*/ + private String deviceReplyTopic; + /*平台获取属性topic*/ + private String fetchPropTopic; +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketDeviceStatusListener.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketDeviceStatusListener.java new file mode 100644 index 0000000..8ce50c1 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketDeviceStatusListener.java @@ -0,0 +1,26 @@ +package com.bnhz.mq.rocketmq.listener; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.DeviceStatusBo; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +/** + * RocketMQ监听设备状态消息 + * @author gsb + * @date 2022/10/11 9:37 + */ +@Slf4j +@Component +@RocketMQMessageListener(consumerGroup = BnhzConstant.CHANNEL.DEVICE_STATUS_GROUP , topic = BnhzConstant.CHANNEL.DEVICE_STATUS) +@ConditionalOnProperty(prefix ="cluster", name = "type",havingValue = BnhzConstant.MQTT.ROCKET_MQ) +public class RocketDeviceStatusListener implements RocketMQListener { + + @Override + public void onMessage(DeviceStatusBo deviceStatusBo) { + log.debug("=>收到设备状态消息,message=[{}]",deviceStatusBo); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketFunctionInvokeListener.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketFunctionInvokeListener.java new file mode 100644 index 0000000..d264526 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketFunctionInvokeListener.java @@ -0,0 +1,27 @@ +package com.bnhz.mq.rocketmq.listener; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.MQSendMessageBo; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +/** + * MQ监听服务下发消息 + * @author gsb + * @date 2022/10/11 9:53 + */ +@Slf4j +@Component +@RocketMQMessageListener(consumerGroup = BnhzConstant.CHANNEL.FUNCTION_INVOKE_GROUP , topic = BnhzConstant.CHANNEL.FUNCTION_INVOKE) +@ConditionalOnProperty(prefix ="cluster", name = "type",havingValue = BnhzConstant.MQTT.ROCKET_MQ) +public class RocketFunctionInvokeListener implements RocketMQListener { + + + @Override + public void onMessage(MQSendMessageBo bo) { + + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketPropReadListener.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketPropReadListener.java new file mode 100644 index 0000000..5447295 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketPropReadListener.java @@ -0,0 +1,25 @@ +package com.bnhz.mq.rocketmq.listener; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.message.DeviceDownMessage; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +/** + * @author gsb + * @date 2022/10/11 16:49 + */ +@Slf4j +@Component +@RocketMQMessageListener(consumerGroup = BnhzConstant.CHANNEL.PROP_READ_GROUP , topic = BnhzConstant.CHANNEL.PROP_READ) +@ConditionalOnProperty(prefix ="cluster", name = "type",havingValue = BnhzConstant.MQTT.ROCKET_MQ) +public class RocketPropReadListener implements RocketMQListener { + + @Override + public void onMessage(DeviceDownMessage deviceDownMessage) { + + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketPublishMsgListener.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketPublishMsgListener.java new file mode 100644 index 0000000..79ba373 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/listener/RocketPublishMsgListener.java @@ -0,0 +1,26 @@ +package com.bnhz.mq.rocketmq.listener; + +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.mq.DeviceReportBo; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +/** + * MQ监听设备推送消息(上报消息和回调消息) + * @author gsb + * @date 2022/10/11 9:51 + */ +@Slf4j +@Component +@RocketMQMessageListener(consumerGroup = BnhzConstant.CHANNEL.PUBLISH_GROUP , topic = BnhzConstant.CHANNEL.PUBLISH) +@ConditionalOnProperty(prefix ="cluster", name = "type",havingValue = BnhzConstant.MQTT.ROCKET_MQ) +public class RocketPublishMsgListener implements RocketMQListener { + + @Override + public void onMessage(DeviceReportBo bo) { + log.debug("=>收到设备推送消息,message=[{}]",bo); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/model/MQSendMessage.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/model/MQSendMessage.java new file mode 100644 index 0000000..7dfc9fa --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/model/MQSendMessage.java @@ -0,0 +1,21 @@ +package com.bnhz.mq.rocketmq.model; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 网关通用模型 + * @author bill + */ +@Data +public class MQSendMessage implements Serializable { + + private static final long serialVersionUID = 1L; + + /**topic*/ + private String topicName; + + /**消息-json格式*/ + private String message; +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/producer/RocketMqProducer.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/producer/RocketMqProducer.java new file mode 100644 index 0000000..09b9bd3 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/producer/RocketMqProducer.java @@ -0,0 +1,260 @@ +package com.bnhz.mq.rocketmq.producer; + +import com.alibaba.fastjson2.JSON; +import com.bnhz.common.utils.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.*; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.apache.rocketmq.spring.support.RocketMQHeaders; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +//@Component +@Slf4j +public class RocketMqProducer { + + /** + * rocketmq模板注入 + */ + @Autowired + private RocketMQTemplate rocketMQTemplate; + + + /** + * 普通发送 + * @param topic 消息主题 + * @param msg 消息体 + * @param 消息泛型 + */ + public void send(String topic, T msg) { + rocketMQTemplate.convertAndSend(topic, msg); + //rocketMQTemplate.send(topic + ":tag1", MessageBuilder.withPayload(msg).build()); // 等价于上面一行 + } + + + /** + * 发送带tag的消息,直接在topic后面加上":tag" + * + * @param topic 消息主题 + * @param tag 消息tag + * @param msg 消息体 + * @param 消息泛型 + * @return + */ + public SendResult sendTagMsg(String topic, String tag, T msg) { + topic = topic + ":" + tag; + return rocketMQTemplate.syncSend(topic, MessageBuilder.withPayload(msg).build()); + } + + + /** + * 发送同步消息(阻塞当前线程,等待broker响应发送结果,这样不太容易丢失消息) + * sendResult为返回的发送结果 + */ + public SendResult sendMsg(String topic, T msg) { + Message message = MessageBuilder.withPayload(msg).build(); + SendResult sendResult = rocketMQTemplate.syncSend(topic, message); + log.info("【sendMsg】sendResult={}", JSON.toJSONString(sendResult)); + return sendResult; + } + + + /** + * 发送异步消息 + * 发送异步消息(通过线程池执行发送到broker的消息任务,执行完后回调:在SendCallback中可处理相关成功失败时的逻辑) + * (适合对响应时间敏感的业务场景) + * @param topic 消息Topic + * @param msg 消息实体 + * + */ + public void asyncSend(String topic, T msg) { + Message message = MessageBuilder.withPayload(msg).build(); + asyncSend(topic, message, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + log.info("topic:{}消息---发送MQ成功---", topic); + } + + @Override + public void onException(Throwable throwable) { + log.error("topic:{}消息---发送MQ失败 ex:{}---", topic, throwable.getMessage()); + } + }); + } + + + /** + * 发送异步消息 + * 发送异步消息(通过线程池执行发送到broker的消息任务,执行完后回调:在SendCallback中可处理相关成功失败时的逻辑) + * (适合对响应时间敏感的业务场景) + * @param topic 消息Topic + * @param message 消息实体 + * @param sendCallback 回调函数 + */ + public void asyncSend(String topic, Message message, SendCallback sendCallback) { + rocketMQTemplate.asyncSend(topic, message, sendCallback); + } + + + /** + * 发送异步消息 + * + * @param topic 消息Topic + * @param message 消息实体 + * @param sendCallback 回调函数 + * @param timeout 超时时间 + */ + public void asyncSend(String topic, Message message, SendCallback sendCallback, long timeout) { + rocketMQTemplate.asyncSend(topic, message, sendCallback, timeout); + } + + + /** + * 同步延迟消息 + * rocketMQ的延迟消息发送其实是已发送就已经到broker端了,然后消费端会延迟收到消息。 + * RocketMQ 目前只支持固定精度的定时消息。 + * 固定等级:1到18分别对应1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h + * 延迟的底层方法是用定时任务实现的。 + * 发送延时消息(delayLevel的值就为0,因为不延时) + * + * @param topic 消息主题 + * @param msg 消息体 + * @param timeout 发送超时时间 + * @param delayLevel 延迟级别 1到18 + * @param 消息泛型 + */ + public void sendDelay(String topic, T msg, long timeout, int delayLevel) { + Message message = MessageBuilder.withPayload(msg).build(); + rocketMQTemplate.syncSend(topic, message, timeout, delayLevel); + } + + + /** + * 发送异步延迟消息 + * + * @param topic 消息Topic + * @param message 消息实体 + * @param sendCallback 回调函数 + * @param timeout 超时时间 + * @param delayLevel 延迟消息的级别 + */ + public void asyncSendDelay(String topic, Message message, SendCallback sendCallback, long timeout, int delayLevel) { + rocketMQTemplate.asyncSend(topic, message, sendCallback, timeout, delayLevel); + } + + + /** + * 发送异步延迟消息 + * + * @param topic 消息Topic + * @param message 消息实体 + * @param timeout 超时时间 + * @param delayLevel 延迟消息的级别 + */ + public void asyncSendDelay(String topic, Message message, long timeout, int delayLevel) { + rocketMQTemplate.asyncSend(topic, message, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + log.info("topic:{}消息---发送MQ成功---", topic); + } + + @Override + public void onException(Throwable throwable) { + log.error("topic:{}消息---发送MQ失败 ex:{}---", topic, throwable.getMessage()); + } + }, timeout, delayLevel); + } + + + /** + * 单向消息 + * 特点为只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答 + * 此方式发送消息的过程耗时非常短,一般在微秒级别 + * 应用场景:适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集 + * @param topic 消息主题 + * @param msg 消息体 + * @param 消息泛型 + */ + public void sendOneWayMsg(String topic, T msg) { + Message message = MessageBuilder.withPayload(msg).build(); + rocketMQTemplate.sendOneWay(topic, message); + } + + + /** + * 发送顺序消息 + * + * @param topic 消息主题 + * @param msg 消息体 + * @param hashKey 确定消息发送到哪个队列中 + * @param 消息泛型 + */ + public void syncSendOrderly(String topic, T msg, String hashKey) { + Message message = MessageBuilder.withPayload(msg).build(); + log.info("发送顺序消息,topic:{}, hashKey:{}", topic, hashKey); + rocketMQTemplate.syncSendOrderly(topic, message, hashKey); + } + + + /** + * 发送顺序消息 + * + * @param topic 消息主题 + * @param msg 消息体 + * @param hashKey 确定消息发送到哪个队列中 + * @param timeout 超时时间 + */ + public void syncSendOrderly(String topic, T msg, String hashKey, long timeout) { + Message message = MessageBuilder.withPayload(msg).build(); + log.info("发送顺序消息,topic:{}, hashKey:{}, timeout:{}", topic, hashKey, timeout); + rocketMQTemplate.syncSendOrderly(topic, message, hashKey, timeout); + } + + + /** + * 发送批量消息 + * + * @param topic 消息主题 + * @param msgList 消息体集合 + * @param 消息泛型 + * @return + */ + public SendResult asyncSendBatch(String topic, List msgList) { + List> messageList = msgList.stream() + .map(msg -> MessageBuilder.withPayload(msg).build()).collect(Collectors.toList()); + return rocketMQTemplate.syncSend(topic, messageList); + } + + /** + * 发送事务消息 + * + * @param txProducerGroup 事务消息的生产者组名称 + * @param topic 事务消息主题 + * @param tag 事务消息tag + * @param msg 事务消息体 + * @param arg 事务消息监听器回查参数 + * @param 事务消息泛型 + */ + public void sendTransaction(String txProducerGroup, String topic, String tag, T msg, T arg){ + if(StringUtils.isNotEmpty(tag)){ + topic = topic + ":" + tag; + } + String transactionId = UUID.randomUUID().toString(); + Message message = MessageBuilder.withPayload(msg) + //header也有大用处 + .setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId) + .setHeader("share_id", "TEST") + .build(); + TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(topic, message, arg); + if(result.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE) + && result.getSendStatus().equals(SendStatus.SEND_OK)){ + log.info("事物消息发送成功"); + } + log.info("事物消息发送结果:{}", result); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/service/RocketMqPublishServiceImpl.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/service/RocketMqPublishServiceImpl.java new file mode 100644 index 0000000..148298e --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/rocketmq/service/RocketMqPublishServiceImpl.java @@ -0,0 +1,26 @@ +package com.bnhz.mq.rocketmq.service; + +import com.bnhz.mq.rocketmq.producer.RocketMqProducer; +import com.bnhz.mq.service.IMessagePublishService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * 设备消息推送至RocketMq + * @author bill + */ +public class RocketMqPublishServiceImpl implements IMessagePublishService { + + @Autowired + private RocketMqProducer rocketMqProducer; + + /** + * rocket通用生产消息方法 + * @param message 设备消息 + * @param channel 推送topic + */ + @Override + public void publish(Object message,String channel) + { + rocketMqProducer.send(channel,message); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/ruleEngine/MainExecutorBuilder.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/ruleEngine/MainExecutorBuilder.java new file mode 100644 index 0000000..c868f06 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/ruleEngine/MainExecutorBuilder.java @@ -0,0 +1,20 @@ +package com.bnhz.mq.ruleEngine; + +import com.yomahub.liteflow.thread.ExecutorBuilder; +import org.springframework.scheduling.concurrent.CustomizableThreadFactory; + +import java.util.concurrent.*; + +public class MainExecutorBuilder implements ExecutorBuilder { + private ThreadFactory springThreadFactory = new CustomizableThreadFactory("liteflow-main-"); + @Override + public ExecutorService buildExecutor() { + return new ThreadPoolExecutor( + 10, + 30, + 5, + TimeUnit.MINUTES, + new ArrayBlockingQueue(1000), + springThreadFactory); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/ruleEngine/SceneContext.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/ruleEngine/SceneContext.java new file mode 100644 index 0000000..a86b176 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/ruleEngine/SceneContext.java @@ -0,0 +1,597 @@ +package com.bnhz.mq.ruleEngine; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.core.mq.InvokeReqDto; +import com.bnhz.common.core.notify.AlertPushParams; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.core.redis.RedisKeyBuilder; +import com.bnhz.common.core.thingsModel.SceneThingsModelItem; +import com.bnhz.common.core.thingsModel.ThingsModelSimpleItem; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.spring.SpringUtils; +import com.bnhz.iot.domain.*; +import com.bnhz.iot.mapper.AlertLogMapper; +import com.bnhz.iot.model.AlertSceneSendVO; +import com.bnhz.iot.model.SceneTerminalUserVO; +import com.bnhz.iot.model.ScriptTemplate; +import com.bnhz.iot.model.ThingsModels.ValueItem; +import com.bnhz.iot.service.*; +import com.bnhz.mq.service.IFunctionInvoke; +import com.bnhz.notify.core.service.NotifySendService; +import lombok.Data; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.*; +import java.util.stream.Collectors; + +import static java.util.regex.Pattern.compile; + +@Data +public class SceneContext { + + /** + * 上报信息的设备编号 + */ + private String deviceNum; + + /** + * 上报信息的设备所属产品ID + */ + private Long productId; + + /** + * 上报信息的设备信息类型 1=属性, 2=功能,3=事件,4=设备升级,5=设备上线,6=设备下线 + */ + private int type; + + /** + * 上报的物模型集合 + */ + private List thingsModelSimpleItems; + + /** + * 触发成功的物模型集合,保留给告警记录 + */ + private List sceneThingsModelItems; + + + private static IFunctionInvoke functionInvoke = SpringUtils.getBean(IFunctionInvoke.class); + + private static IDeviceService deviceService = SpringUtils.getBean(IDeviceService.class); + + private static IAlertService alertService = SpringUtils.getBean(IAlertService.class); + + private static NotifySendService notifySendService = SpringUtils.getBean(NotifySendService.class); + + private static RedisCache redisCache = SpringUtils.getBean(RedisCache.class); + + private static AlertLogMapper alertLogMapper = SpringUtils.getBean(AlertLogMapper.class); + + private static IDeviceUserService deviceUserService = SpringUtils.getBean(IDeviceUserService.class); + + private static ISceneService sceneService = SpringUtils.getBean(ISceneService.class); + + private static IDeviceAlertUserService deviceAlertUserService = SpringUtils.getBean(IDeviceAlertUserService.class); + + public SceneContext(String deviceNum, Long productId, int type, List thingsModelSimpleItems) { + this.deviceNum = deviceNum; + this.productId = productId; + this.type = type; + this.thingsModelSimpleItems = thingsModelSimpleItems; + } + + /** + * 处理规则脚本 + * + * @return + */ + private boolean process(String json) throws InterruptedException { + System.out.println("------------------[规则引擎执行...]---------------------"); + ScriptTemplate scriptTemplate = JSON.parseObject(json, ScriptTemplate.class); + if (scriptTemplate.getPurpose() == 2) { + // 触发器,检查静默时间 + if (!checkSilent(scriptTemplate.getSilent(), scriptTemplate.getSceneId())) { + // 触发条件为不满足时,返回true + if (scriptTemplate.getCond() == 3) { + return true; + } + return false; + } + + // 触发器 + if (scriptTemplate.getSource() == 1) { + // 设备触发 + return deviceTrigger(scriptTemplate); + } else if (scriptTemplate.getSource() == 3) { + // 产品触发 + return productTrigger(scriptTemplate); + } + + } else if (scriptTemplate.getPurpose() == 3) { + // 执行动作,延迟执行,线程休眠 delay x 1000毫秒 + Thread.sleep(scriptTemplate.getDelay() * 1000); + + if (scriptTemplate.getSource() == 4) { + // 告警 + this.alert(scriptTemplate.getDelay(), scriptTemplate.getSceneId()); + } else if (scriptTemplate.getSource() == 1 || scriptTemplate.getSource() == 3) { + // 下发指令 + this.send(scriptTemplate); + } + + // 更新静默时间 + this.updateSilent(scriptTemplate.getSilent(), scriptTemplate.getSceneId()); + } + return false; + } + + /*** + * 设备触发脚本处理 + * @param scriptTemplate 解析后的Json脚本数据 + * @return + */ + private boolean deviceTrigger(ScriptTemplate scriptTemplate) { + // 判断定制触发(执行一次)或设备上报 + boolean isDeviceReport = StringUtils.isEmpty(deviceNum) ? false : true; + if (isDeviceReport) { + // 1. 匹配设备编号 + boolean matchDeviceNum = Arrays.asList(scriptTemplate.getDeviceNums().split(",")).contains(deviceNum); + if (scriptTemplate.getType() < 4) { + // 2.匹配物模型标识符 + ThingsModelSimpleItem matchItem = null; + if (thingsModelSimpleItems != null) { + for (ThingsModelSimpleItem item : thingsModelSimpleItems) { + if (item.getId().equals(scriptTemplate.getId())) { + matchItem = item; + break; + } + } + } + if (matchDeviceNum && matchItem != null) { + // 记录结果 + if (sceneThingsModelItems == null) { + sceneThingsModelItems = new ArrayList<>(); + } + SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), matchItem.getValue(), type, + scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum); + sceneThingsModelItems.add(sceneItem); + // 3.设备上报值匹配 + boolean isMatch = matchValue(scriptTemplate.getOperator(), scriptTemplate.getValue(), matchItem.getValue()); + if (isMatch) { + return true; + } + } + + } else { + // 上线,下线 + if (matchDeviceNum && scriptTemplate.getType() == type) { + // 记录结果 + if (sceneThingsModelItems == null) { + sceneThingsModelItems = new ArrayList<>(); + } + SceneThingsModelItem sceneItem = new SceneThingsModelItem(type == 5 ? "online" : "offline", type == 5 ? "1" : "0", type, + scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum); + sceneThingsModelItems.add(sceneItem); + // 记录结果 + return true; + } + } + } else { + // 定时触发/执行一次 + int resultCount = 0; + // 3.查询设备最新上报值去匹配 + for (String num : Arrays.asList(scriptTemplate.getDeviceNums().split(","))) { + // 数组类型,key去除前缀,值从逗号分隔的字符串获取 + String id = ""; + String value = ""; + int index = 0; + if (scriptTemplate.getId().startsWith("array_")) { + id = scriptTemplate.getId().substring(9); + index = Integer.parseInt(scriptTemplate.getId().substring(6, 8)); + } else { + id = scriptTemplate.getId(); + } + String key = RedisKeyBuilder.buildTSLVCacheKey(scriptTemplate.getProductId(), num); + String cacheValue = redisCache.getCacheMapValue(key, id); + if (StringUtils.isEmpty(cacheValue)) { + continue; + } + ValueItem valueItem = JSON.parseObject(cacheValue, ValueItem.class); + if (scriptTemplate.getId().startsWith("array_")) { + String[] values = valueItem.getValue().split(","); + value = values[index]; + } else { + value = valueItem.getValue(); + } + boolean isMatch = matchValue(scriptTemplate.getOperator(), scriptTemplate.getValue(), value); + if (isMatch) { + // 记录结果 + if (sceneThingsModelItems == null) { + sceneThingsModelItems = new ArrayList<>(); + } + SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), value, type, + scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), num); + sceneThingsModelItems.add(sceneItem); + resultCount++; + } + } + // 任意设备匹配成功返回true + return resultCount > 0 ? true : false; + } + return false; + } + + /*** + * 产品触发脚本处理 + * @param scriptTemplate + * @return + */ + private boolean productTrigger(ScriptTemplate scriptTemplate) { + // 判断定制触发(执行一次)或设备上报 + boolean isDeviceReport = StringUtils.isEmpty(deviceNum) ? false : true; + if (isDeviceReport) { + // 匹配产品编号 + boolean matchProductId = scriptTemplate.getProductId().equals(productId); + if (scriptTemplate.getType() < 4) { + // 匹配物模型标识符 + ThingsModelSimpleItem matchItem = null; + if (thingsModelSimpleItems != null) { + for (ThingsModelSimpleItem item : thingsModelSimpleItems) { + if (item.getId().equals(scriptTemplate.getId())) { + matchItem = item; + break; + } + } + } + if (matchProductId && matchItem != null) { + // 记录结果 + if (sceneThingsModelItems == null) { + sceneThingsModelItems = new ArrayList<>(); + } + SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), matchItem.getValue(), type, + scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum); + sceneThingsModelItems.add(sceneItem); + // 设备上报值匹配 + boolean isMatch = matchValue(scriptTemplate.getOperator(), scriptTemplate.getValue(), matchItem.getValue()); + if (isMatch) { + return true; + } + } + + } else { + // 上线,下线 + if (matchProductId && scriptTemplate.getType() == type) { + // 记录结果 + if (sceneThingsModelItems == null) { + sceneThingsModelItems = new ArrayList<>(); + } + SceneThingsModelItem sceneItem = new SceneThingsModelItem(type == 5 ? "online" : "offline", type == 5 ? "1" : "0", type, + scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum); + sceneThingsModelItems.add(sceneItem); + // 记录结果 + return true; + } + } + } else { + // 定时触发/执行一次 + int resultCount = 0; + // 查询设备最新上报值去匹配 + String[] deviceNums = deviceService.getDeviceNumsByProductId(scriptTemplate.getProductId()); + for (String num : deviceNums) { + // 数组类型,key去除前缀,值从逗号分隔的字符串获取 + String id = ""; + String value = ""; + int index = 0; + if (scriptTemplate.getId().startsWith("array_")) { + id = scriptTemplate.getId().substring(9); + index = Integer.parseInt(scriptTemplate.getId().substring(6, 8)); + } else { + id = scriptTemplate.getId(); + } + String key = RedisKeyBuilder.buildTSLVCacheKey(scriptTemplate.getProductId(), num); + String cacheValue = redisCache.getCacheMapValue(key, id); + if (StringUtils.isEmpty(cacheValue)) { + continue; + } + ValueItem valueItem = JSON.parseObject(cacheValue, ValueItem.class); + if (scriptTemplate.getId().startsWith("array_")) { + String[] values = valueItem.getValue().split(","); + value = values[index]; + } else { + value = valueItem.getValue(); + } + boolean isMatch = matchValue(scriptTemplate.getOperator(), scriptTemplate.getValue(), value); + if (isMatch) { + // 记录结果 + if (sceneThingsModelItems == null) { + sceneThingsModelItems = new ArrayList<>(); + } + SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), value, type, + scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), num); + sceneThingsModelItems.add(sceneItem); + resultCount++; + } + } + // 任意设备匹配成功返回true + return resultCount > 0 ? true : false; + } + return false; + } + + + /** + * 执行动作,下发指令 + * + * @param scriptTemplate + */ + private void send(ScriptTemplate scriptTemplate) { + String[] deviceNumbers = null; + if (scriptTemplate.getSource() == 1) { + // 下发给指定设备 + deviceNumbers = scriptTemplate.getDeviceNums().split(","); + + } else if (scriptTemplate.getSource() == 3) { + // 下发给产品下所有设备 + deviceNumbers = deviceService.getDeviceNumsByProductId(scriptTemplate.getProductId()); + } + for (String deviceNum : deviceNumbers) { + InvokeReqDto reqDto = new InvokeReqDto(); + reqDto.setProductId(scriptTemplate.getProductId()); + reqDto.setSerialNumber(deviceNum); + reqDto.setModelName(""); + reqDto.setType(1); + reqDto.setIdentifier(scriptTemplate.getId()); + Map params = new HashMap<>(); + params.put(scriptTemplate.getId(), scriptTemplate.getValue()); + reqDto.setRemoteCommand(params); + reqDto.setValue(new JSONObject(reqDto.getRemoteCommand())); + functionInvoke.invokeNoReply(reqDto); + } + } + + /** + * 执行动作,告警处理 + * + * @param sceneId 场景ID + * @param delay 延时(单位秒,90秒内) + */ + private void alert(int delay, Long sceneId) { + Set sceneIdSet = sceneThingsModelItems.stream().map(SceneThingsModelItem::getSceneId).collect(Collectors.toSet()); + List sceneTerminalUserVOList = sceneService.selectTerminalUserBySceneIds(sceneIdSet); + Map sceneTerminalUserMap = sceneTerminalUserVOList.stream().collect(Collectors.toMap(SceneTerminalUserVO::getSceneId, Function.identity())); + List alertLogList = new ArrayList<>(); + for (SceneThingsModelItem sceneThingsModelItem : sceneThingsModelItems) { + // 查询设备信息 + Device device = deviceService.selectDeviceBySerialNumber(sceneThingsModelItem.getDeviceNumber()); + Optional.ofNullable(device).orElseThrow(() -> new ServiceException("告警推送,设备不存在" + "[{" + sceneThingsModelItem.getDeviceNumber() + "}]")); + // 判断是否是终端用户的场景 + SceneTerminalUserVO sceneTerminalUserVO = sceneTerminalUserMap.get(sceneId); + if (1 == sceneTerminalUserVO.getTerminalUser()) { + AlertLog alertLog = this.getTerminalUserAlertLog(sceneTerminalUserVO, device, sceneThingsModelItem); + alertLogList.add(alertLog); + continue; + } + + // 获取场景相关的告警参数,告警必须要是启动状态 + List sceneSendVOList = alertService.listByAlertIds(sceneId); + if (CollectionUtils.isEmpty(sceneSendVOList)) { + continue; + } + // 获取告警推送参数 + AlertPushParams alertPushParams = new AlertPushParams(); + alertPushParams.setDeviceName(device.getDeviceName()); + alertPushParams.setSerialNumber(sceneThingsModelItem.getDeviceNumber()); +// List deviceUserList = deviceUserService.getDeviceUserAndShare(device.getDeviceId()); + // 多租户改版查询自己配置的告警用户 + DeviceAlertUser deviceAlertUser = new DeviceAlertUser(); + deviceAlertUser.setDeviceId(device.getDeviceId()); + List deviceUserList = deviceAlertUserService.selectDeviceAlertUserList(deviceAlertUser); + if (CollectionUtils.isNotEmpty(deviceUserList)) { + alertPushParams.setUserPhoneSet(deviceUserList.stream().map(DeviceAlertUser::getPhoneNumber).filter(StringUtils::isNotEmpty).collect(Collectors.toSet())); + alertPushParams.setUserIdSet(deviceUserList.stream().map(DeviceAlertUser::getUserId).collect(Collectors.toSet())); + } + String address; + if (StringUtils.isNotEmpty(device.getNetworkAddress())) { + address = device.getNetworkAddress(); + } else if (StringUtils.isNotEmpty(device.getNetworkIp())) { + address = device.getNetworkIp(); + } else if (Objects.nonNull(device.getLongitude()) && Objects.nonNull(device.getLatitude())) { + address = device.getLongitude() + "," + device.getLatitude(); + } else { + address = "未知地点"; + } + alertPushParams.setAddress(address); + alertPushParams.setAlertTime(DateUtils.parseDateToStr(DateUtils.YY_MM_DD_HH_MM_SS, new Date())); + // 获取告警关联模版id + for (AlertSceneSendVO alertSceneSendVO : sceneSendVOList) { + List alertNotifyTemplateList = alertService.listAlertNotifyTemplate(alertSceneSendVO.getAlertId()); + alertPushParams.setAlertName(alertSceneSendVO.getAlertName()); + for (AlertNotifyTemplate alertNotifyTemplate : alertNotifyTemplateList) { + alertPushParams.setNotifyTemplateId(alertNotifyTemplate.getNotifyTemplateId()); + notifySendService.alertSend(alertPushParams); + } + AlertLog alertLog = getAlertLog(alertSceneSendVO, device, sceneThingsModelItem); + alertLogList.add(alertLog); + } + } + // 保存告警日志 + if (CollectionUtils.isNotEmpty(alertLogList)) { + alertLogMapper.insertAlertLogBatch(alertLogList); + } + } + + private AlertLog getTerminalUserAlertLog(SceneTerminalUserVO sceneTerminalUserVO, Device device, SceneThingsModelItem sceneThingsModelItem) { + AlertLog alertLog = new AlertLog(); + alertLog.setAlertName("设备告警"); + alertLog.setAlertLevel(1L); + alertLog.setSerialNumber(sceneThingsModelItem.getDeviceNumber()); + alertLog.setProductId(sceneThingsModelItem.getProductId()); + alertLog.setDeviceName(device.getDeviceName()); + alertLog.setUserId(sceneTerminalUserVO.getUserId()); + // 统一未处理 + alertLog.setStatus(2); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", sceneThingsModelItem.getId()); + jsonObject.put("value", sceneThingsModelItem.getValue()); + jsonObject.put("remark", ""); + alertLog.setDetail(jsonObject.toJSONString()); + alertLog.setCreateTime(new Date()); + return alertLog; + } + + /** + * 组装告警日志 + * + * @param alertSceneSendVO + * @return com.bnhz.iot.domain.AlertLog + * @param: device + */ + private AlertLog getAlertLog(AlertSceneSendVO alertSceneSendVO, Device device, SceneThingsModelItem sceneThingsModelItem) { + AlertLog alertLog = new AlertLog(); + alertLog.setAlertName(alertSceneSendVO.getAlertName()); + alertLog.setAlertLevel(alertSceneSendVO.getAlertLevel()); + alertLog.setSerialNumber(sceneThingsModelItem.getDeviceNumber()); + alertLog.setProductId(sceneThingsModelItem.getProductId()); + alertLog.setDeviceName(device.getDeviceName()); + alertLog.setUserId(device.getTenantId()); + // 统一未处理 + alertLog.setStatus(2); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", sceneThingsModelItem.getId()); + jsonObject.put("value", sceneThingsModelItem.getValue()); + jsonObject.put("remark", ""); + alertLog.setDetail(jsonObject.toJSONString()); + alertLog.setCreateTime(new Date()); + return alertLog; + } + + /** + * 检查静默周期物模型值是否匹配 + * + * @param operator 操作符 + * @param triggerValue 触发值 + * @param value 上报的值 + * @return + */ + private boolean matchValue(String operator, String triggerValue, String value) { + boolean result = false; + // 操作符比较 + switch (operator) { + case "=": + result = value.equals(triggerValue); + break; + case "!=": + result = !value.equals(triggerValue); + break; + case ">": + if (isNumeric(value) && isNumeric(triggerValue)) { + result = Double.parseDouble(value) > Double.parseDouble(triggerValue); + } + break; + case "<": + if (isNumeric(value) && isNumeric(triggerValue)) { + result = Double.parseDouble(value) < Double.parseDouble(triggerValue); + } + break; + case ">=": + if (isNumeric(value) && isNumeric(triggerValue)) { + result = Double.parseDouble(value) >= Double.parseDouble(triggerValue); + } + break; + case "<=": + if (isNumeric(value) && isNumeric(triggerValue)) { + result = Double.parseDouble(value) <= Double.parseDouble(triggerValue); + } + break; + case "between": + // 比较值用英文中划线分割 - + String[] triggerValues = triggerValue.split("-"); + if (isNumeric(value) && isNumeric(triggerValues[0]) && isNumeric(triggerValues[1])) { + result = Double.parseDouble(value) >= Double.parseDouble(triggerValues[0]) && Double.parseDouble(value) <= Double.parseDouble(triggerValues[1]); + } + break; + case "notBetween": + // 比较值用英文中划线分割 - + String[] trigValues = triggerValue.split("-"); + if (isNumeric(value) && isNumeric(trigValues[0]) && isNumeric(trigValues[1])) { + result = Double.parseDouble(value) <= Double.parseDouble(trigValues[0]) || Double.parseDouble(value) >= Double.parseDouble(trigValues[1]); + } + break; + case "contain": + result = value.contains(triggerValue); + break; + case "notContain": + result = !value.contains(triggerValue); + break; + default: + break; + } + return result; + } + + /** + * 检查静默时间 + * + * @param silent + * @param sceneId + * @return + */ + private boolean checkSilent(int silent, Long sceneId) { + if (silent == 0 || sceneId == 0) { + return true; + } + // silent:scene_场景编号 + String key = "silent:" + "scene_" + sceneId; + Calendar calendar = Calendar.getInstance(); + // 查询静默截止时间 + Long expireTime = redisCache.getCacheObject(key); + if (expireTime == null) { + // 添加场景静默时间 + calendar.add(Calendar.MINUTE, silent); + redisCache.setCacheObject(key, calendar.getTimeInMillis()); + return true; + } else { + Long NowTimestamp = Calendar.getInstance().getTimeInMillis(); + if (NowTimestamp > expireTime) { + return true; + } + return false; + } + } + + /** + * 更新静默时间 + * + * @param sceneId + * @param silent + */ + private void updateSilent(int silent, Long sceneId) { + if (silent == 0 || sceneId == 0) { + return; + } + // 更新场景静默时间 + String key = "silent:" + "scene_" + sceneId; + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, silent); + redisCache.setCacheObject(key, calendar.getTimeInMillis()); + } + + /** + * 判断字符串是否为整数或小数 + */ + private boolean isNumeric(String str) { + Pattern pattern = compile("[0-9]*\\.?[0-9]+"); + Matcher isNum = pattern.matcher(str); + if (!isNum.matches()) { + return false; + } + return true; + } + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/ruleEngine/WhenExecutorBuilder.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/ruleEngine/WhenExecutorBuilder.java new file mode 100644 index 0000000..f416ccb --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/ruleEngine/WhenExecutorBuilder.java @@ -0,0 +1,20 @@ +package com.bnhz.mq.ruleEngine; + +import com.yomahub.liteflow.thread.ExecutorBuilder; +import org.springframework.scheduling.concurrent.CustomizableThreadFactory; + +import java.util.concurrent.*; + +public class WhenExecutorBuilder implements ExecutorBuilder { + private ThreadFactory springThreadFactory = new CustomizableThreadFactory("liteflow-when-"); + @Override + public ExecutorService buildExecutor() { + return new ThreadPoolExecutor( + 10, + 30, + 5, + TimeUnit.MINUTES, + new ArrayBlockingQueue(1000), + springThreadFactory); + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IDataHandler.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IDataHandler.java new file mode 100644 index 0000000..e1241ee --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IDataHandler.java @@ -0,0 +1,32 @@ +package com.bnhz.mq.service; + +import com.bnhz.mq.model.ReportDataBo; + +/** + * 客户端上报数据处理方法集合 + * @author bill + */ +public interface IDataHandler { + + /** + * 上报属性或功能处理 + * + * @param bo 上报数据模型 + */ + void reportData(ReportDataBo bo); + + + /** + * 上报事件 + * + * @param bo 上报数据模型 + */ + void reportEvent(ReportDataBo bo); + + /** + * 上报设备信息 + * @param bo 上报数据模型 + */ + void reportDevice(ReportDataBo bo); + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IDeviceReportMessageService.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IDeviceReportMessageService.java new file mode 100644 index 0000000..fec463f --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IDeviceReportMessageService.java @@ -0,0 +1,54 @@ +package com.bnhz.mq.service; + +import com.bnhz.common.core.mq.DeviceReport; +import com.bnhz.common.core.mq.DeviceReportBo; +import com.bnhz.iot.domain.Device; +import com.bnhz.protocol.base.protocol.IProtocol; + +/** + * 处理设备上报数据解析 + * @author gsb + * @date 2022/10/10 13:48 + */ +public interface IDeviceReportMessageService { + + /** + * 处理设备主动上报数据 + * @param bo + */ + public void parseReportMsg(DeviceReportBo bo); + + /** + * 处理设备普通消息回调 + * @param bo + */ + public void parseReplyMsg(DeviceReportBo bo); + + /** + * 处理设备OTA升级 + * @param bo + */ + public void parseOTAUpdateReply(DeviceReportBo bo); + + + /** + * 构建消息 + * @param bo + */ + public Device buildReport(DeviceReportBo bo); + + + /** + * 根据产品id获取协议处理器 + */ + IProtocol selectedProtocol(Long productId); + + /** + * 处理设备主动上报属性 + * + * @param topicName + * @param message + */ + public void handlerReportMessage(DeviceReport message, String topicName); + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IFunctionInvoke.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IFunctionInvoke.java new file mode 100644 index 0000000..7ed38c0 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IFunctionInvoke.java @@ -0,0 +1,27 @@ +package com.bnhz.mq.service; + +import com.bnhz.common.core.mq.InvokeReqDto; + +import java.util.Map; + +/** + * 设备指令下发接口 + * @author gsb + * @date 2022/12/5 11:03 + */ +public interface IFunctionInvoke { + + /** + * 服务调用,等待设备响应 + * @param reqDto 服务下发对象 + * @return 数据结果 + */ + public Map invokeReply(InvokeReqDto reqDto); + + /** + * 服务调用,设备不响应 + * @param reqDto 服务下发对象 + * @return 消息id messageId + */ + public String invokeNoReply(InvokeReqDto reqDto); +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IMessagePublishService.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IMessagePublishService.java new file mode 100644 index 0000000..1704122 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IMessagePublishService.java @@ -0,0 +1,17 @@ +package com.bnhz.mq.service; + +/** + * 设备消息推送mq + * @author bill + */ +public interface IMessagePublishService { + + + /** + * 发布消息到mq + * @param message 设备消息 + * @param channel 推送channel + */ + public void publish(Object message,String channel); + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IMqttMessagePublish.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IMqttMessagePublish.java new file mode 100644 index 0000000..f1b8e7a --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IMqttMessagePublish.java @@ -0,0 +1,88 @@ +package com.bnhz.mq.service; + +import com.bnhz.common.core.mq.DeviceReportBo; +import com.bnhz.common.core.mq.MQSendMessageBo; +import com.bnhz.common.core.mq.message.DeviceDownMessage; +import com.bnhz.common.core.mq.message.InstructionsMessage; +import com.bnhz.common.core.mq.ota.OtaUpgradeBo; +import com.bnhz.common.core.thingsModel.ThingsModelSimpleItem; +import com.bnhz.common.enums.TopicType; +import com.bnhz.iot.domain.Device; +import com.bnhz.mq.model.ReportDataBo; + +import java.util.List; + +public interface IMqttMessagePublish { + + /** + * 下发数据编码 + */ + InstructionsMessage buildMessage(DeviceDownMessage downMessage, TopicType type); + + /** + * 服务(指令)下发 + */ + public void funcSend(MQSendMessageBo bo); + + /** + * OTA升级下发 + */ + public void upGradeOTA(OtaUpgradeBo bo); + + public void sendFunctionMessage(DeviceReportBo bo); + + + /** + * 1.发布设备状态 + */ + public void publishStatus(Long productId, String deviceNum, int deviceStatus, int isShadow, int rssi); + + + /** + * 2.发布设备信息 + */ + public void publishInfo(Long productId, String deviceNum); + + + /** + * 3.发布时钟同步信息 + * + * @param bo 数据模型 + */ + public void publishNtp(ReportDataBo bo); + + + /** + * 4.发布属性 + * delay 延时,秒为单位 + */ + public void publishProperty(Long productId, String deviceNum, List thingsList, int delay); + + + /** + * 5.发布功能 + * delay 延时,秒为单位 + */ + public void publishFunction(Long productId, String deviceNum, List thingsList, int delay); + + /** + * 设备数据同步 + * + * @param deviceNumber 设备编号 + * @return 设备 + */ + public Device deviceSynchronization(String deviceNumber); + + /** + * 用于发送模拟客户端数据 + */ + public void sendSimulatorMessage(String topic,byte[] data); + + /** + * 模拟设备写客户端数据 + * @param topic + * @param data + */ + public void getSimulatorInfo(String topic,byte[] data); + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IRuleEngine.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IRuleEngine.java new file mode 100644 index 0000000..4047b04 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/IRuleEngine.java @@ -0,0 +1,19 @@ +package com.bnhz.mq.service; + +import com.bnhz.mq.model.ReportDataBo; + +/** + * 规则引擎处理数据方法集合 + * @author bill + */ +public interface IRuleEngine { + + + /** + * 规则匹配(告警和场景联动) + * + * @param bo 上报数据模型 + */ + public void ruleMatch(ReportDataBo bo); + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/DeviceOtherMsgHandler.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/DeviceOtherMsgHandler.java new file mode 100644 index 0000000..9c4b501 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/DeviceOtherMsgHandler.java @@ -0,0 +1,94 @@ +package com.bnhz.mq.service.impl; + +import com.bnhz.common.core.mq.DeviceReportBo; +import com.bnhz.common.enums.TopicType; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.gateway.mq.TopicsUtils; +import com.bnhz.mq.model.ReportDataBo; +import com.bnhz.mq.service.IDataHandler; +import com.bnhz.mq.service.IMqttMessagePublish; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * @author gsb + * @date 2023/2/27 14:42 + */ +@Component +@Slf4j +public class DeviceOtherMsgHandler { + + @Resource + private TopicsUtils topicsUtils; + @Resource + private IDataHandler dataHandler; + @Resource + private IMqttMessagePublish messagePublish; + + /** + * true: 使用netty搭建的mqttBroker false: 使用emq + */ + @Value("${server.broker.enabled}") + private Boolean enabled; + + + /** + * 非属性消息消息处理入口 + * + * @param bo + */ + public void messageHandler(DeviceReportBo bo) { + String type = ""; + String name = topicsUtils.parseTopicName(bo.getTopicName()); + if (StringUtils.isEmpty(name)) return; + ReportDataBo data = this.buildReportData(bo); + TopicType topicType = TopicType.getType(name); + switch (topicType) { + case INFO_POST: + dataHandler.reportDevice(data); + break; + case NTP_POST: + messagePublish.publishNtp(data); + break; + // 接收 property/get 模拟设备数据 + case PROPERTY_GET: + messagePublish.sendSimulatorMessage(bo.getTopicName(), bo.getData()); + break; + case PROPERTY_SET: + messagePublish.getSimulatorInfo(bo.getTopicName(), bo.getData()); + break; + case FUNCTION_POST: + data.setShadow(false); + data.setType(2); + data.setRuleEngine(true); + dataHandler.reportData(data); + break; + case EVENT_POST: + data.setType(3); + data.setRuleEngine(true); + dataHandler.reportEvent(data); + break; + } + } + + /** + * 组装数据 + */ + private ReportDataBo buildReportData(DeviceReportBo bo) { + String message = new String(bo.getData()); + log.info("收到设备信息[{}]", message); + Long productId = topicsUtils.parseProductId(bo.getTopicName()); + String serialNumber = topicsUtils.parseSerialNumber(bo.getTopicName()); + ReportDataBo dataBo = new ReportDataBo(); + dataBo.setMessage(message); + dataBo.setProductId(productId); + dataBo.setSerialNumber(serialNumber); + dataBo.setRuleEngine(false); + dataBo.setValuesInput(bo.getValuesInput()); + return dataBo; + } + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/FunctionInvokeImpl.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/FunctionInvokeImpl.java new file mode 100644 index 0000000..042dd6d --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/FunctionInvokeImpl.java @@ -0,0 +1,80 @@ +package com.bnhz.mq.service.impl; + +import com.bnhz.common.core.mq.DeviceReplyBo; +import com.bnhz.common.core.mq.InvokeReqDto; +import com.bnhz.common.core.mq.MQSendMessageBo; +import com.bnhz.common.core.mq.MessageReplyBo; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.core.redis.RedisKeyBuilder; +import com.bnhz.common.enums.ThingsModelType; +import com.bnhz.common.utils.bean.BeanUtils; +import com.bnhz.iot.util.SnowflakeIdWorker; +import com.bnhz.mq.redischannel.producer.MessageProducer; +import com.bnhz.mq.service.IFunctionInvoke; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @author gsb + * @date 2022/12/5 11:34 + */ +@Slf4j +@Service +public class FunctionInvokeImpl implements IFunctionInvoke { + + @Resource + private RedisCache redisCache; + + private SnowflakeIdWorker snowflakeIdWorker = new SnowflakeIdWorker(2); + + /** + * 服务调用,等待设备响应 + * @param reqDto 服务下发对象 + * @return 数据结果 + */ + @Override + public Map invokeReply(InvokeReqDto reqDto){ + Map result = new HashMap<>(); + invokeNoReply(reqDto); + //TODO- 根据消息id查询回执,暂时没有消息Id回执 + return result; + } + + /** + * 服务调用,设备不响应 + * @param reqDto 服务下发对象 + * @return 消息id messageId + */ + @Override + public String invokeNoReply(InvokeReqDto reqDto){ + log.debug("=>下发指令请求:[{}]",reqDto); + MQSendMessageBo bo = new MQSendMessageBo(); + BeanUtils.copyBeanProp(bo,reqDto); + long id = snowflakeIdWorker.nextId(); + String messageId = id+""; + bo.setMessageId(messageId+""); + bo.setType(ThingsModelType.getType(reqDto.getType())); + MessageProducer.sendFunctionInvoke(bo); + //10s,设备不回复,认为指令下发失败 + DeviceReplyBo replyBo = new DeviceReplyBo(); + replyBo.setId(reqDto.getIdentifier()); + replyBo.setMessageId(messageId); + replyBo.setValue(reqDto.getValue().get(reqDto.getIdentifier()).toString()); + String cacheKey = RedisKeyBuilder.buildDownMessageIdCacheKey(reqDto.getSerialNumber()); + redisCache.setCacheObject(cacheKey, replyBo, 10000, TimeUnit.MILLISECONDS); + return messageId; + } + + /** + * TODO- 轮询拿返回值 + */ + private MessageReplyBo queryResult(InvokeReqDto reqDto){ + MessageReplyBo replyBo = null; + return replyBo; + } +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/MessageManager.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/MessageManager.java new file mode 100644 index 0000000..45dcde9 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/MessageManager.java @@ -0,0 +1,77 @@ +package com.bnhz.mq.service.impl; + +import com.bnhz.base.session.Session; +import com.bnhz.base.session.SessionManager; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.protocol.Message; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.modbus.model.ModbusRtu; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +/** + * @author gsb + * @date 2022/11/22 10:30 + */ +@Component +@Slf4j +public class MessageManager { + + private static final Mono NEVER = Mono.never(); + private static final Mono OFFLINE_EXCEPTION = Mono.error( new ServiceException("离线的客户端",4000)); + private static final Mono OFFLINE_RESULT = Mono.just(new AjaxResult(4000, "离线的客户端")); + private static final Mono SEND_FAIL_RESULT = Mono.just(new AjaxResult(4001, "消息发送失败")); + + private SessionManager sessionManager; + + public MessageManager(SessionManager sessionManager){ + this.sessionManager = sessionManager; + } + + public Mono notifyR(String sessionId, ModbusRtu request){ + Session session = sessionManager.getSession(sessionId); + if (session == null){ + return OFFLINE_EXCEPTION; + } + return session.notify(request); + } + + public Mono requestR(String sessionId, Message request, Class responseClass){ + Session session = sessionManager.getSession(sessionId); + if (session == null){ + return OFFLINE_RESULT; + } + return session.request(request,responseClass) + .map(message -> AjaxResult.success(message)) + .onErrorResume(e ->{ + log.warn("消息发送失败:{}",e); + return SEND_FAIL_RESULT; + }); + } + + /** + * 下发指令等待回复 + * @param sessionId + * @return + */ + public Mono request(String sessionId, ModbusRtu request, Class responseClass, long timeout){ + return request(sessionId,request,responseClass).timeout(Duration.ofMillis(timeout)); + } + + /** + * 下发指令,不等待回复 + * @param sessionId + * @return + */ + public Mono request(String sessionId, ModbusRtu request, Class responseClass){ + Session session = sessionManager.getSession(sessionId); + if (session == null){ + return OFFLINE_EXCEPTION; + } + return session.request(request,responseClass); + } + +} diff --git a/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/RuleEngineHandler.java b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/RuleEngineHandler.java new file mode 100644 index 0000000..03ed389 --- /dev/null +++ b/bnhz-gateway/bnhz-mq/src/main/java/com/bnhz/mq/service/impl/RuleEngineHandler.java @@ -0,0 +1,83 @@ +package com.bnhz.mq.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.bnhz.common.core.thingsModel.ThingsModelSimpleItem; +import com.bnhz.iot.domain.*; +import com.bnhz.iot.mapper.*; +import com.bnhz.mq.model.ReportDataBo; +import com.bnhz.mq.service.IFunctionInvoke; +import com.bnhz.mq.service.IRuleEngine; +import com.bnhz.mq.ruleEngine.SceneContext; +import com.yomahub.liteflow.core.FlowExecutor; +import com.yomahub.liteflow.flow.LiteflowResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static java.util.regex.Pattern.compile; + +/** + * 规则引擎处理数据方法 + * + * @author bill + */ +@Component +@Slf4j +public class RuleEngineHandler implements IRuleEngine { + + @Resource + private IFunctionInvoke functionInvoke; + @Resource + private SceneDeviceMapper sceneDeviceMapper; + + @Resource + private FlowExecutor flowExecutor; + + /** + * 规则匹配(告警和场景联动) + * + * @param bo 上报数据模型bo + * @see ReportDataBo + */ + public void ruleMatch(ReportDataBo bo) { + try { + // 场景联动处理 + this.sceneProcess(bo); + } catch (Exception e) { + log.error("接收数据,解析数据时异常 message={}", e, e.getMessage()); + } + } + + /** + * 场景规则处理 + */ + public void sceneProcess(ReportDataBo bo) throws ExecutionException, InterruptedException { + // 查询设备关联的场景 + SceneDevice sceneDeviceParam = new SceneDevice(); + sceneDeviceParam.setProductId(bo.getProductId()); + sceneDeviceParam.setSerialNumber(bo.getSerialNumber()); + List sceneList = sceneDeviceMapper.selectTriggerDeviceRelateScenes(sceneDeviceParam); + + int type = bo.getType(); + // 获取上报的物模型 + List thingsModelSimpleItems = bo.getDataList(); + if (CollectionUtils.isEmpty(bo.getDataList())) { + thingsModelSimpleItems = JSON.parseArray(bo.getMessage(), ThingsModelSimpleItem.class); + } + // 执行场景规则,异步非阻塞 + for (Scene scene : sceneList) { + SceneContext context = new SceneContext(bo.getSerialNumber(), bo.getProductId(),type,thingsModelSimpleItems); + Future future= flowExecutor.execute2Future(String.valueOf(scene.getChainName()), null, context); + if (!future.get().isSuccess()) { + Exception e = future.get().getCause(); + log.error("场景联动执行失败 message={}", e, e.getMessage()); + } + } + } + +} diff --git a/bnhz-gateway/gateway-boot/pom.xml b/bnhz-gateway/gateway-boot/pom.xml new file mode 100644 index 0000000..527ff0c --- /dev/null +++ b/bnhz-gateway/gateway-boot/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + bnhz-gateway + com.bnhz + 3.8.5 + + gateway-boot + + 网关模块 + + + + + com.bnhz + bnhz-mq + + + + + + diff --git a/bnhz-gateway/gateway-boot/src/main/java/com/bnhz/gateway/boot/start/StartBoot.java b/bnhz-gateway/gateway-boot/src/main/java/com/bnhz/gateway/boot/start/StartBoot.java new file mode 100644 index 0000000..4b5a268 --- /dev/null +++ b/bnhz-gateway/gateway-boot/src/main/java/com/bnhz/gateway/boot/start/StartBoot.java @@ -0,0 +1,63 @@ +package com.bnhz.gateway.boot.start; + +import com.bnhz.mq.redischannel.listen.*; +import com.bnhz.mqttclient.PubMqttClient; +import com.bnhz.protocol.service.IProtocolManagerService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 启动类 + * + * @author bill + */ +@Component +@Slf4j +@Order(2) +public class StartBoot implements ApplicationRunner { + + + @Autowired + private PubMqttClient mqttClient; + @Autowired + private DeviceReplyListen replyListen; + @Autowired + private DeviceReportListen reportListen; + @Autowired + private DeviceStatusListen statusListen; + @Autowired + private DevicePropFetchListen propFetchListen; + @Autowired + private UpgradeListen upgradeListen; + @Autowired + private FunctionInvokeListen invokeListen; + @Resource + private DeviceOtherListen otherListen; + @Resource + private IProtocolManagerService protocolManagerService; + + + @Override + public void run(ApplicationArguments args) throws Exception { + try { + replyListen.listen(); + statusListen.listen(); + propFetchListen.listen(); + upgradeListen.listen(); + invokeListen.listen(); + otherListen.listen(); + /*启动内部客户端,用来下发客户端服务*/ + mqttClient.initialize(); + protocolManagerService.getAllProtocols(); + log.info("=>设备监听队列启动成功"); + } catch (Exception e) { + log.error("=>客户端启动失败:{}", e.getMessage(),e); + } + } +} diff --git a/bnhz-gateway/pom.xml b/bnhz-gateway/pom.xml new file mode 100644 index 0000000..63ab222 --- /dev/null +++ b/bnhz-gateway/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + daqi-back + com.bnhz + 3.8.5 + + + pom + + + gateway-boot + bnhz-mq + + + bnhz-gateway + bnhz-gateway + + + + com.bnhz + bnhz-common + + + + com.bnhz + bnhz-iot-service + + + + com.bnhz + mqtt-client + + + + + + + diff --git a/bnhz-notify/bnhz-notify-core/pom.xml b/bnhz-notify/bnhz-notify-core/pom.xml new file mode 100644 index 0000000..0455099 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + com.bnhz + bnhz-notify + 3.8.5 + + + 通知核心发送模块 + bnhz-notify-core + + + 8 + 8 + UTF-8 + + + + + com.bnhz + bnhz-common + + + + com.bnhz + bnhz-notify-web + + + + org.springframework + spring-web + + + org.springframework + spring-context + + + com.bnhz + bnhz-iot-service + + + + com.aliyun + dyvmsapi20170525 + 2.1.4 + + + + com.tencentcloudapi + tencentcloud-sdk-java + 3.1.952 + + + + org.dromara.sms4j + sms4j-Email-core + 3.1.0 + + + + com.aliyun + dingtalk + 1.1.32 + + + com.aliyun + alibaba-dingtalk-service-sdk + 2.0.0 + + + + diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/controller/NotifyController.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/controller/NotifyController.java new file mode 100644 index 0000000..69562e7 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/controller/NotifyController.java @@ -0,0 +1,76 @@ +package com.bnhz.notify.core.controller; + +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.notify.core.service.NotifySendService; +import com.bnhz.notify.core.vo.SendParams; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author fastb + * @version 1.0 + * @description: 通知控制器 + * @date 2023-12-15 11:44 + */ +@Api(tags = "通知发送") +@RestController +@RequestMapping("/notify") +public class NotifyController { + + @Resource + private NotifySendService notifySendService; + + /** + * @description: 通知测试发送接口 + * @param: sendParams 发送参数 + * @return: com.bnhz.common.core.domain.AjaxResult + */ + @PreAuthorize("@ss.hasPermi('notify:template:send')") + @PostMapping("/send") + @ApiOperation("通知模版测试发送接口") + public AjaxResult send(@RequestBody SendParams sendParams){ + return notifySendService.send(sendParams); + } + + /** + * @description: 短信登录获取验证码 + * @param: phoneNumber 手机号 + * @return: com.bnhz.common.core.domain.AjaxResult + */ + @GetMapping("/smsLoginCaptcha") + @ApiOperation("短信登录获取验证码") + public AjaxResult smsLoginCaptcha(String phoneNumber){ + return notifySendService.smsLoginCaptcha(phoneNumber); + } + + /** + * 企业微信验证url有效性 + * @param msgSignature + * @param: timestamp + * @param: nonce + * @param: echostr + * @param: response + * @return void + */ + @ApiOperation("企业微信验证url有效性") + @GetMapping("/weComVerifyUrl") + public void weComVerifyUrl(@RequestParam(value = "msg_signature") String msgSignature, + @RequestParam(value = "timestamp") String timestamp, + @RequestParam(value = "nonce") String nonce, + @RequestParam(value = "echostr") String echostr, + HttpServletResponse response) { + String msg = notifySendService.weComVerifyUrl(msgSignature, timestamp, nonce, echostr); + try { + response.getWriter().print(msg); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/dingtalk/service/DingTalkService.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/dingtalk/service/DingTalkService.java new file mode 100644 index 0000000..754b8c5 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/dingtalk/service/DingTalkService.java @@ -0,0 +1,21 @@ +package com.bnhz.notify.core.dingtalk.service; + +import com.bnhz.common.core.notify.NotifySendResponse; +import com.bnhz.notify.vo.NotifyVO; + +/** + * 钉钉通知服务类 + * @author fastb + * @date 2024-01-12 17:57 + * @version 1.0 + */ +public interface DingTalkService { + + /** + * 钉钉统一发送方法 + * @param notifyVO 通知参数 + * @return com.bnhz.common.core.notify.NotifySendResponse + */ + NotifySendResponse send(NotifyVO notifyVO); + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/dingtalk/service/impl/DingTalkServiceImpl.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/dingtalk/service/impl/DingTalkServiceImpl.java new file mode 100644 index 0000000..fdcf743 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/dingtalk/service/impl/DingTalkServiceImpl.java @@ -0,0 +1,225 @@ +package com.bnhz.notify.core.dingtalk.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.aliyun.dingtalkoauth2_1_0.Client; +import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest; +import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponse; +import com.aliyun.tea.TeaException; +import com.aliyun.teaopenapi.models.Config; +import com.aliyun.teautil.Common; +import com.dingtalk.api.DefaultDingTalkClient; +import com.dingtalk.api.DingTalkClient; +import com.dingtalk.api.request.OapiMessageCorpconversationAsyncsendV2Request; +import com.dingtalk.api.request.OapiRobotSendRequest; +import com.dingtalk.api.response.OapiMessageCorpconversationAsyncsendV2Response; +import com.dingtalk.api.response.OapiRobotSendResponse; +import com.bnhz.common.core.notify.NotifySendResponse; +import com.bnhz.common.core.notify.config.DingTalkConfigParams; +import com.bnhz.common.core.notify.msg.DingTalkMsgParams; +import com.bnhz.common.enums.NotifyChannelProviderEnum; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.notify.core.dingtalk.service.DingTalkService; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.domain.NotifyTemplate; +import com.bnhz.notify.vo.NotifyVO; +import com.taobao.api.ApiException; +import org.springframework.stereotype.Service; + +/** + * @author fastb + * @version 1.0 + * @description: 钉钉通知服务类 + * @date 2024-01-12 17:58 + */ +@Service +public class DingTalkServiceImpl implements DingTalkService { + + + @Override + public NotifySendResponse send(NotifyVO notifyVO) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + NotifyChannel notifyChannel = notifyVO.getNotifyChannel(); + NotifyTemplate notifyTemplate = notifyVO.getNotifyTemplate(); + String content = JSONObject.parseObject(notifyTemplate.getMsgParams()).get("content").toString(); + String sendContent = StringUtils.strReplaceVariable("${", "}", content, notifyVO.getMap()); + DingTalkMsgParams dingTalkMsgParams = JSONObject.parseObject(notifyTemplate.getMsgParams(), DingTalkMsgParams.class); + // 获取AppKey和AppSecret + DingTalkConfigParams dingTalkConfigParams = JSONObject.parseObject(notifyChannel.getConfigContent(), DingTalkConfigParams.class); + if (NotifyChannelProviderEnum.DING_TALK_WORK.equals(notifyVO.getNotifyChannelProviderEnum())) { + notifySendResponse = this.workSend(dingTalkConfigParams, dingTalkMsgParams, notifyVO.getSendAccount(), sendContent); + } else if (NotifyChannelProviderEnum.DING_TALK_GROUP_ROBOT.equals(notifyVO.getNotifyChannelProviderEnum())) { + notifySendResponse = this.customizeRobotSend(dingTalkConfigParams, dingTalkMsgParams, sendContent); + } + return notifySendResponse; + } + + /** + * 自定义机器人发送 + * @param dingTalkConfigParams 渠道配置参数 + * @param: dingTalkMsgParams 模版配置参数 + * @param: sendContent 发送内容 + * @return com.bnhz.common.core.notify.NotifySendResponse + */ + private NotifySendResponse customizeRobotSend(DingTalkConfigParams dingTalkConfigParams, DingTalkMsgParams dingTalkMsgParams, String sendContent) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + DefaultDingTalkClient client = new DefaultDingTalkClient(dingTalkConfigParams.getWebHook()); + OapiRobotSendRequest request = this.createOapiRobotMsg(dingTalkMsgParams, sendContent); + try { + OapiRobotSendResponse execute = client.execute(request); + notifySendResponse.setStatus("0".equals(execute.getErrorCode()) ? 1 : 0); + notifySendResponse.setResultContent(JSON.toJSONString(execute)); + } catch (ApiException e) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent(e.toString()); + } + return notifySendResponse; + } + + /** + * 工作通知发送 + * @param dingTalkConfigParams 渠道配置参数 + * @param: dingTalkMsgParams 模版配置参数 + * @param: sendAccount 发送账号,多个以,隔开 + * @param: sendContent 发送内容 + * @return com.bnhz.common.core.notify.NotifySendResponse + */ + private NotifySendResponse workSend(DingTalkConfigParams dingTalkConfigParams, DingTalkMsgParams dingTalkMsgParams, String sendAccount, String sendContent) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + notifySendResponse.setSendContent(sendContent); + notifySendResponse.setStatus(1); + // 发送 + try { + // 获取应用访问凭证accessToken + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + GetAccessTokenRequest accessTokenRequest = new GetAccessTokenRequest(); + accessTokenRequest.setAppKey(dingTalkConfigParams.getAppKey()); + accessTokenRequest.setAppSecret(dingTalkConfigParams.getAppSecret()); + GetAccessTokenResponse accessTokenResponse = client.getAccessToken(accessTokenRequest); + String accessToken = accessTokenResponse.getBody().getAccessToken(); + DingTalkClient dingTalkClient = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2"); + OapiMessageCorpconversationAsyncsendV2Request req = new OapiMessageCorpconversationAsyncsendV2Request(); + req.setAgentId(Long.valueOf(dingTalkConfigParams.getAgentId())); + // 优先取发送账号、然后是部门、然后是所有人 + if (StringUtils.isNotEmpty(sendAccount)) { + // 根据手机号获取钉钉userid +// DingTalkClient dingTalkClientUser = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/getbymobile"); +// OapiV2UserGetbymobileRequest phoneReq = new OapiV2UserGetbymobileRequest(); +// phoneReq.setMobile(sendAccount); +// OapiV2UserGetbymobileResponse rsp = dingTalkClientUser.execute(phoneReq, accessToken); +// userId = rsp.getResult().getUserid(); + req.setUseridList(sendAccount); + } else if (StringUtils.isNotEmpty(dingTalkMsgParams.getDeptId())) { + req.setDeptIdList(dingTalkMsgParams.getDeptId()); + notifySendResponse.setOtherSendAccount(dingTalkMsgParams.getDeptId()); + } else if (Boolean.TRUE.toString().equals(dingTalkMsgParams.getSendAllEnable())) { + req.setToAllUser(Boolean.TRUE); + notifySendResponse.setOtherSendAccount("allUser"); + } + OapiMessageCorpconversationAsyncsendV2Request.Msg msg = this.createOapiMessageMsg(dingTalkMsgParams, sendContent); + req.setMsg(msg); + OapiMessageCorpconversationAsyncsendV2Response rsp = dingTalkClient.execute(req, accessToken); + notifySendResponse.setStatus(0 == rsp.getErrcode() ? 1 : 0); + notifySendResponse.setResultContent(JSON.toJSONString(rsp)); + } catch (Exception e) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent(e.toString()); + } + return notifySendResponse; + } + + private OapiRobotSendRequest createOapiRobotMsg(DingTalkMsgParams dingTalkMsgParams, String sendContent) { + OapiRobotSendRequest request = new OapiRobotSendRequest(); + request.setMsgtype(dingTalkMsgParams.getMsgType()); + switch (request.getMsgtype()) { + case "text": + OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text(); + text.setContent(sendContent); + request.setText(text); + break; + case "link": + OapiRobotSendRequest.Link link = new OapiRobotSendRequest.Link(); + link.setTitle(dingTalkMsgParams.getTitle()); + link.setText(sendContent); + link.setMessageUrl(dingTalkMsgParams.getMessageUrl()); + link.setPicUrl(dingTalkMsgParams.getPicUrl()); + request.setLink(link); + break; + case "markdown": + OapiRobotSendRequest.Markdown markdown = new OapiRobotSendRequest.Markdown(); + markdown.setText(sendContent); + markdown.setTitle(dingTalkMsgParams.getTitle()); + request.setMarkdown(markdown); + break; + default: + break; + } + return request; + } + + /** + * 构建钉钉发送消息 + * @param msgParams 消息参数 + * @return + */ + private OapiMessageCorpconversationAsyncsendV2Request.Msg createOapiMessageMsg(DingTalkMsgParams msgParams, String content) { + OapiMessageCorpconversationAsyncsendV2Request.Msg msg = new OapiMessageCorpconversationAsyncsendV2Request.Msg(); + msg.setMsgtype(msgParams.getMsgType()); + switch (msg.getMsgtype()) { + case "text": + msg.setText(new OapiMessageCorpconversationAsyncsendV2Request.Text()); + msg.getText().setContent(content); + break; + case "link": + msg.setLink(new OapiMessageCorpconversationAsyncsendV2Request.Link()); + msg.getLink().setTitle(msgParams.getTitle()); + msg.getLink().setText(content); + msg.getLink().setMessageUrl(msgParams.getMessageUrl()); + msg.getLink().setPicUrl(msgParams.getPicUrl()); + break; + case "markdown": + msg.setMarkdown(new OapiMessageCorpconversationAsyncsendV2Request.Markdown()); + msg.getMarkdown().setText(content); + msg.getMarkdown().setTitle(msgParams.getTitle()); + break; + default: + break; + } + return msg; + } + + /** + * 获取AccessToken + * @param appKey + * @param: appSecret + * @return java.lang.String + */ + private String getAccessToken(String appKey, String appSecret) { + try { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + Client client = new Client(config); + GetAccessTokenRequest accessTokenRequest = new GetAccessTokenRequest(); + accessTokenRequest.setAppKey(appKey); + accessTokenRequest.setAppSecret(appSecret); + GetAccessTokenResponse accessToken = client.getAccessToken(accessTokenRequest); + return accessToken.getBody().getAccessToken(); + } catch (TeaException err) { + if (!Common.empty(err.code) && !Common.empty(err.message)) { + // err 中含有 code 和 message 属性,可帮助开发定位问题 + } + } catch (Exception _err) { + TeaException err = new TeaException(_err.getMessage(), _err); + if (!Common.empty(err.code) && !Common.empty(err.message)) { + // err 中含有 code 和 message 属性,可帮助开发定位问题 + } + + } + return ""; + } + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/email/config/EmailNotifyConfig.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/email/config/EmailNotifyConfig.java new file mode 100644 index 0000000..e0f9543 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/email/config/EmailNotifyConfig.java @@ -0,0 +1,32 @@ +package com.bnhz.notify.core.email.config; + +import com.bnhz.common.core.notify.config.EmailConfigParams; +import org.dromara.email.api.MailClient; +import org.dromara.email.comm.config.MailSmtpConfig; +import org.dromara.email.core.factory.MailFactory; + +/** + * @author fastb + * @version 1.0 + * @description: 邮箱获取发送配置类 + * @date 2023-12-20 10:23 + */ +public class EmailNotifyConfig { + + public static MailClient create(String mailClientKey, EmailConfigParams emailNotifyConfig) { + MailSmtpConfig config = MailSmtpConfig.builder() + .smtpServer(emailNotifyConfig.getSmtpServer()) + .port(emailNotifyConfig.getPort()) + .fromAddress(emailNotifyConfig.getUsername()) + .username(emailNotifyConfig.getUsername()) + .password(emailNotifyConfig.getPassword()) + .isSSL(emailNotifyConfig.getSslEnable().toString()) + .isAuth(emailNotifyConfig.getAuthEnable().toString()) + .retryInterval(emailNotifyConfig.getRetryInterval()) + .maxRetries(emailNotifyConfig.getMaxRetries()) + .build(); + MailFactory.put(mailClientKey, config); + return MailFactory.createMailClient(mailClientKey); + } + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/email/service/EmailService.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/email/service/EmailService.java new file mode 100644 index 0000000..8ca5f29 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/email/service/EmailService.java @@ -0,0 +1,21 @@ +package com.bnhz.notify.core.email.service; + +import com.bnhz.common.core.notify.NotifySendResponse; +import com.bnhz.notify.vo.NotifyVO; + +/** + * @description: 邮箱发送业务类 + * @author fastb + * @date 2023-12-29 16:20 + * @version 1.0 + */ +public interface EmailService { + + /** + * @description: 邮件简要内容发送 + * @param: notifyVO 发送VO类 + * @return: void + */ + NotifySendResponse send(NotifyVO notifyVO); + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/email/service/impl/EmailServiceImpl.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/email/service/impl/EmailServiceImpl.java new file mode 100644 index 0000000..8f574a7 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/email/service/impl/EmailServiceImpl.java @@ -0,0 +1,90 @@ +package com.bnhz.notify.core.email.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.core.notify.NotifySendResponse; +import com.bnhz.common.core.notify.config.EmailConfigParams; +import com.bnhz.common.core.notify.msg.EmailMsgParams; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.notify.core.email.config.EmailNotifyConfig; +import com.bnhz.notify.core.email.service.EmailService; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.domain.NotifyTemplate; +import com.bnhz.notify.vo.NotifyVO; +import lombok.extern.slf4j.Slf4j; +import org.dromara.email.api.MailClient; +import org.dromara.email.comm.entity.MailMessage; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author fastb + * @version 1.0 + * @description: 邮箱发送业务类 + * @date 2023-12-20 10:21 + */ +@Slf4j +@Service +public class EmailServiceImpl implements EmailService { + + + public MailClient createMailClient(NotifyChannel notifyChannel, NotifyTemplate notifyTemplate) { + // 构建邮箱配置对象,key为 provider_id + String mailClientKey = notifyChannel.getProvider() + "_" + notifyTemplate.getId(); + EmailConfigParams emailNotifyConfig = JSONObject.parseObject(notifyChannel.getConfigContent(), EmailConfigParams.class); + return EmailNotifyConfig.create(mailClientKey, emailNotifyConfig); + } + + @Override + public NotifySendResponse send(NotifyVO notifyVO) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + if (StringUtils.isEmpty(notifyVO.getSendAccount())) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent("发送邮箱号为空,请先配置发送邮箱号!"); + return notifySendResponse; + } + MailClient mailClient; + try { + mailClient = this.createMailClient(notifyVO.getNotifyChannel(), notifyVO.getNotifyTemplate()); + } catch (Exception e) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent("获取邮箱发送类失败," + e); + return notifySendResponse; + } + NotifyTemplate notifyTemplate = notifyVO.getNotifyTemplate(); + EmailMsgParams emailMsgParams = JSONObject.parseObject(notifyTemplate.getMsgParams(), EmailMsgParams.class); + // 目前附件就支持一个 + Map filesMap = new HashMap<>(2); + if (StringUtils.isNotEmpty(emailMsgParams.getAttachment())) { + String fileName = emailMsgParams.getAttachment().substring(emailMsgParams.getAttachment().lastIndexOf("/")); + if (fileName.contains(".")) { + filesMap.put(fileName, emailMsgParams.getAttachment()); + } + } + // 多个邮箱以,分隔 + List mailList = StringUtils.str2List(notifyVO.getSendAccount(), ",", true, true); + MailMessage mailMessage = MailMessage.Builder() + .mailAddress(mailList) + .title(emailMsgParams.getTitle()) + .html(new ByteArrayInputStream(emailMsgParams.getContent().getBytes())) + .htmlValues(notifyVO.getMap()) + .files(filesMap) + .build(); + try { + mailClient.send(mailMessage); + } catch (Exception e) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent("邮箱发送失败," + e); + log.error("邮箱发送失败=====>", e); + return notifySendResponse; + } + String sendContent = StringUtils.strReplaceVariable("#{", "}", emailMsgParams.getContent(), notifyVO.getMap()); + notifySendResponse.setSendContent(sendContent); + notifySendResponse.setStatus(1); + return notifySendResponse; + } + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/service/NotifySendService.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/service/NotifySendService.java new file mode 100644 index 0000000..6c2f91e --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/service/NotifySendService.java @@ -0,0 +1,73 @@ +package com.bnhz.notify.core.service; + +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.notify.AlertPushParams; +import com.bnhz.notify.core.vo.SendParams; +import com.bnhz.notify.vo.NotifyVO; + +/** + * @description: 所有通知发送入口 + * 以后有通知业务可写在这里 + * @author fastb + * @date 2023-12-26 15:47 + * @version 1.0 + */ +public interface NotifySendService { + + /** + * @description: 通知测试发送接口 + * @param: sendParams + * @return: void + */ + AjaxResult send(SendParams sendParams); + + /** + * @description: 通知统一发送方法 + * @param: notifyVO 通知发送参数VO + * @return: void + */ + AjaxResult notifySend(NotifyVO notifyVO); + + /** + * 告警通知统一推送 + * @param alertPushParams 告警推送参数 + * @return void + */ + void alertSend(AlertPushParams alertPushParams); + + /** + * @description: 发送短信验证码 + * @author fastb + * @date 2023-12-26 15:49 + * @version 1.0 + */ + void sendCaptchaSms(String phone, String captcha); + + /** + * @description: 根据业务编码、渠道、服务商获取唯一启用通知配置信息 + * @param serviceCode 业务编码,一定传 + * @param channelType 渠道类型 一定传 + * @param provider 钉钉和微信渠道 一定传 + * @author fastb + * @date 2024-01-02 11:13 + */ + NotifyVO selectOnlyEnable(String serviceCode, String channelType, String provider, Long tenantId); + + /** + * @description: 短信登录获取验证码 + * @param: phoneNumber + * @return: com.bnhz.common.core.domain.AjaxResult + */ + AjaxResult smsLoginCaptcha(String phoneNumber); + + /** + * 企业微信验证url有效性 + * @param msgSignature + * @param: timestamp + * @param: nonce + * @param: echostr + * @param: response + * @return void + */ + String weComVerifyUrl(String msgSignature, String timestamp, String nonce, String echostr); +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/service/impl/NotifySendServiceImpl.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/service/impl/NotifySendServiceImpl.java new file mode 100644 index 0000000..0b9c617 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/service/impl/NotifySendServiceImpl.java @@ -0,0 +1,318 @@ +package com.bnhz.notify.core.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.constant.CacheConstants; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.notify.AlertPushParams; +import com.bnhz.common.core.notify.NotifySendResponse; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.enums.NotifyChannelEnum; +import com.bnhz.common.enums.NotifyChannelProviderEnum; +import com.bnhz.common.enums.NotifyServiceCodeEnum; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.ValidationUtils; +import com.bnhz.common.utils.VerifyCodeUtils; +import com.bnhz.common.utils.wechat.AesException; +import com.bnhz.common.utils.wechat.WXBizMsgCrypt; +import com.bnhz.notify.core.dingtalk.service.DingTalkService; +import com.bnhz.notify.core.email.service.EmailService; +import com.bnhz.notify.core.service.NotifySendService; +import com.bnhz.notify.core.sms.service.ISmsService; +import com.bnhz.notify.core.vo.SendParams; +import com.bnhz.notify.core.voice.service.VoiceService; +import com.bnhz.notify.core.wechat.service.WeChatPushService; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.domain.NotifyLog; +import com.bnhz.notify.domain.NotifyTemplate; +import com.bnhz.notify.service.INotifyChannelService; +import com.bnhz.notify.service.INotifyLogService; +import com.bnhz.notify.service.INotifyTemplateService; +import com.bnhz.notify.vo.NotifyVO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * @author fastb + * @version 1.0 + * @description: 通知业务类 + * @date 2023-12-26 10:38 + */ +@Slf4j +@Service +public class NotifySendServiceImpl implements NotifySendService { + + @Resource + private INotifyChannelService notifyChannelService; + @Resource + private INotifyTemplateService notifyTemplateService; + @Resource + private INotifyLogService notifyLogService; + @Resource + private ISmsService smsService; + @Resource + private VoiceService voiceService; + @Resource + private EmailService emailService; + @Resource + private WeChatPushService weChatPushService; + @Resource + private RedisCache redisCache; + @Resource + private DingTalkService dingTalkService; + + @Override + public AjaxResult send(SendParams sendParams) { + // 获取配置参数 + NotifyTemplate notifyTemplate = notifyTemplateService.selectNotifyTemplateById(sendParams.getId()); + NotifyChannel notifyChannel = notifyChannelService.selectNotifyChannelById(notifyTemplate.getChannelId()); + LinkedHashMap map = new LinkedHashMap<>(); + if (StringUtils.isNotEmpty(sendParams.getVariables())) { + map = JSONObject.parseObject(sendParams.getVariables(), LinkedHashMap.class); + } + NotifyChannelProviderEnum notifyChannelProviderEnum = NotifyChannelProviderEnum.getByChannelTypeAndProvider(notifyChannel.getChannelType(), notifyChannel.getProvider()); + NotifyVO notifyVO = new NotifyVO(); + notifyVO.setNotifyChannel(notifyChannel).setNotifyTemplate(notifyTemplate) + .setSendAccount(sendParams.getSendAccount()).setMap(map).setNotifyChannelProviderEnum(notifyChannelProviderEnum); + return this.notifySend(notifyVO); + } + + @Override + public AjaxResult notifySend(NotifyVO notifyVO) { + // 获取发送参数 + NotifyChannel notifyChannel = notifyVO.getNotifyChannel(); + NotifyTemplate notifyTemplate = notifyVO.getNotifyTemplate(); + String sendAccount = notifyVO.getSendAccount(); + if (StringUtils.isNotEmpty(notifyVO.getSendAccount())) { + String s = notifyVO.getSendAccount().replaceAll(",", ","); + sendAccount = s; + notifyVO.setSendAccount(s); + } + NotifyChannelEnum notifyChannelEnum = NotifyChannelEnum.getNotifyChannelEnum(notifyChannel.getChannelType()); + // 组装模板内容参数,发送通知 + NotifySendResponse notifySendResponse = new NotifySendResponse(); + switch (Objects.requireNonNull(notifyChannelEnum)) { + case SMS: + notifySendResponse = smsService.send(notifyVO); + break; + case EMAIL: + notifySendResponse = emailService.send(notifyVO); + break; + case VOICE: + notifySendResponse = voiceService.send(notifyVO); + break; + case WECHAT: + notifySendResponse = weChatPushService.send(notifyVO); + break; + case DING_TALK: + notifySendResponse = dingTalkService.send(notifyVO); + break; + default: + break; + } + // 保存日志 + NotifyLog notifyLog = new NotifyLog(); + notifyLog.setChannelId(notifyChannel.getId()).setNotifyTemplateId(notifyTemplate.getId()) + .setSendAccount(StringUtils.isNotEmpty(notifySendResponse.getOtherSendAccount()) ? notifySendResponse.getOtherSendAccount() : sendAccount) + .setServiceCode(notifyTemplate.getServiceCode()) + .setMsgContent(notifySendResponse.getSendContent()) + .setSendStatus(notifySendResponse.getStatus()).setResultContent(notifySendResponse.getResultContent()) + .setTenantId(notifyTemplate.getTenantId()).setTenantName(notifyTemplate.getTenantName()); + notifyLogService.insertNotifyLog(notifyLog); + return AjaxResult.success(); + } + + @Override + public void alertSend(AlertPushParams alertPushParams) { + // 获取发送模版 + NotifyTemplate notifyTemplate = notifyTemplateService.selectNotifyTemplateById(alertPushParams.getNotifyTemplateId()); + if (Objects.isNull(notifyTemplate) || 0 == notifyTemplate.getStatus()) { + log.info("告警关联通知模版未启用,模版编号:{}", alertPushParams.getNotifyTemplateId()); + return; + } + NotifyChannel notifyChannel = notifyChannelService.selectNotifyChannelById(notifyTemplate.getChannelId()); + NotifyChannelProviderEnum notifyChannelProviderEnum = NotifyChannelProviderEnum.getByChannelTypeAndProvider(notifyChannel.getChannelType(), notifyChannel.getProvider()); + NotifyVO notifyVO = new NotifyVO(); + notifyVO.setNotifyChannel(notifyChannel).setNotifyTemplate(notifyTemplate).setNotifyChannelProviderEnum(notifyChannelProviderEnum); + // 获取模版参数 + JSONObject jsonMsgParams = JSONObject.parseObject(notifyVO.getNotifyTemplate().getMsgParams()); + String content = jsonMsgParams.get("content").toString(); + List variables = notifyTemplateService.listVariables(content, notifyChannelProviderEnum); + // 获取模版变量 + assert notifyChannelProviderEnum != null; + NotifyChannelEnum notifyChannelEnum = NotifyChannelEnum.getNotifyChannelEnum(notifyChannelProviderEnum.getChannelType()); + LinkedHashMap map = new LinkedHashMap<>(); + switch (Objects.requireNonNull(notifyChannelEnum)) { + // 示例内容变量顺序:您的设备:${name},设备编号:${serialnumber},在${address}发生${alert}告警; 可自行修改 + case SMS: + case EMAIL: + case WECHAT: + case DING_TALK: + // 按顺序依次替换变量信息 + for (int i = 0; i < variables.size(); i++) { + if (i == 0) { + map.put(variables.get(i), alertPushParams.getDeviceName()); + } else if (i == 1) { + map.put(variables.get(i), alertPushParams.getSerialNumber()); + } else if (i == 2) { + map.put(variables.get(i), alertPushParams.getAddress()); + } else { + map.put(variables.get(i), alertPushParams.getAlertName()); + } + } + break; + // 示例内容变量顺序:您的设备:${name},在${address}发生告警,请尽快处理; + // 阿里云语音模版只支持两个变量,所有语音统一使用两个变量,可自行修改 + case VOICE: + // 按顺序依次替换变量信息 + for (int i = 0; i < variables.size(); i++) { + if (i == 0) { + map.put(variables.get(i), alertPushParams.getDeviceName()); + } else { + map.put(variables.get(i), alertPushParams.getAddress()); + } + } + break; + default: + break; + } + // 获取发送账号 + Object sendAccountObject = jsonMsgParams.get("sendAccount"); + Set sendAccountSet = new HashSet<>(); + if (ObjectUtil.isNotEmpty(sendAccountObject)) { + Collections.addAll(sendAccountSet, sendAccountObject.toString()); + } + // 短信、语音、微信小程序需要取设备所属及分享用户信息+模版配置账号,其余使用模版配置的账号 + if (NotifyChannelEnum.SMS.equals(notifyChannelEnum) || NotifyChannelEnum.VOICE.equals(notifyChannelEnum)) { + if (CollectionUtils.isNotEmpty(alertPushParams.getUserPhoneSet())) { + sendAccountSet.addAll(alertPushParams.getUserPhoneSet()); + } + } + if (NotifyChannelProviderEnum.WECHAT_MINI_PROGRAM.equals(notifyChannelProviderEnum) + || NotifyChannelProviderEnum.WECHAT_PUBLIC_ACCOUNT.equals(notifyChannelProviderEnum)) { + if (CollectionUtils.isNotEmpty(alertPushParams.getUserIdSet())) { + for (Long userId : alertPushParams.getUserIdSet()) { + sendAccountSet.add(userId.toString()); + } + } + } + notifyVO.setSendAccount(StringUtils.join(sendAccountSet, ",")); + // 发送 + notifyVO.setMap(map); + this.notifySend(notifyVO); + } + + @Override + public void sendCaptchaSms(String phone, String captcha) { + NotifyVO notifyVO = this.selectOnlyEnable(NotifyServiceCodeEnum.CAPTCHA.getServiceCode(), NotifyChannelEnum.SMS.getType(), null, 1L); + NotifyChannelProviderEnum notifyChannelProviderEnum = notifyVO.getNotifyChannelProviderEnum(); + // 获取模板参数 + JSONObject jsonMsgParams = JSONObject.parseObject(notifyVO.getNotifyTemplate().getMsgParams()); + String content = jsonMsgParams.get("content").toString(); + // 从模板内容中获取 占位符 关键字 + LinkedHashMap map = new LinkedHashMap<>(); + List variables = new ArrayList<>(); + if (NotifyChannelProviderEnum.SMS_ALIBABA.equals(notifyChannelProviderEnum)) { + variables = StringUtils.getVariables("${}", content); + } else if (NotifyChannelProviderEnum.SMS_TENCENT.equals(notifyChannelProviderEnum)) { + variables = StringUtils.getVariables("{}", content); + } + map.put(variables.get(0), captcha); + notifyVO.setSendAccount(phone); + notifyVO.setMap(map); + this.notifySend(notifyVO); + } + + @Override + public NotifyVO selectOnlyEnable(String serviceCode, String channelType, String provider, Long tenantId) { + // 获取查询条件 + NotifyTemplate enableQueryCondition = notifyTemplateService.getEnableQueryCondition(serviceCode, channelType, provider, tenantId); + NotifyTemplate notifyTemplate = notifyTemplateService.selectOnlyEnable(enableQueryCondition); + if (Objects.isNull(notifyTemplate)) { + throw new ServiceException("查询不到启用的通知模板"); + } + NotifyChannel notifyChannel = notifyChannelService.selectNotifyChannelById(notifyTemplate.getChannelId()); + if (Objects.isNull(notifyChannel)) { + throw new ServiceException("查询不到通知渠道"); + } + NotifyVO notifyVO = new NotifyVO(); + notifyVO.setNotifyChannel(notifyChannel); + notifyVO.setNotifyTemplate(notifyTemplate); + NotifyChannelProviderEnum notifyChannelProviderEnum = NotifyChannelProviderEnum.getByChannelTypeAndProvider(notifyVO.getNotifyChannel().getChannelType(), notifyVO.getNotifyChannel().getProvider()); + notifyVO.setNotifyChannelProviderEnum(notifyChannelProviderEnum); + return notifyVO; + } + + @Override + public AjaxResult smsLoginCaptcha(String phoneNumber) { + String userIdKey = CacheConstants.LOGIN_SMS_CAPTCHA_PHONE + phoneNumber; + String captcha = VerifyCodeUtils.generateVerifyCode(6, "0123456789"); + this.sendCaptchaSms(phoneNumber, captcha); + redisCache.setCacheObject(userIdKey, captcha, 5, TimeUnit.MINUTES); + return AjaxResult.success(); + } + + @Override + public String weComVerifyUrl(String msgSignature, String timestamp, String nonce, String echostr) { + // 因为只用验证一次,下面三个参数就不写在配置文件里了,需要验证的可以把下面的验证参数改为自己公司的,然后部署到服务器验证就行 + //token + String token = "pr77kdcA5mzJwNeAwV86UcIS"; + // encodingAESKey + String encodingAesKey = "efNILsQxM6wOCsrNPiBeuLOBDgDSnNtOVFBbtf6jwTe"; + //企业ID + String corpId = "ww4761023a5d81550f"; + // 通过检验msg_signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败 + String result = null; + try { + WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(token, encodingAesKey, corpId); + result = wxcpt.VerifyURL(msgSignature, timestamp, nonce, echostr); + } catch (AesException e) { + log.error("企业微信验证url错误,error:{}", e.getMessage()); + } + return result; + } + + /** + * 校验发送账号格式 + * @param sendAccount 发送账号 + * @param: notifyChannelEnum 通知枚举 + * @return java.lang.String + */ + private String checkSendAccountMsg(String sendAccount, NotifyChannelProviderEnum notifyChannelProviderEnum) { + boolean matches; + switch (Objects.requireNonNull(notifyChannelProviderEnum)) { + case SMS_ALIBABA: + case SMS_TENCENT: + case VOICE_ALIBABA: + case DING_TALK_WORK: + case WECHAT_WECOM_APPLY: + matches = ValidationUtils.isMobile(sendAccount); + if (!matches) { + return "请输入正确的电话号码!"; + } + break; + case EMAIL_QQ: + case EMAIL_163: + matches = ValidationUtils.isEmail(sendAccount); + if (!matches) { + return "请输入正确的邮箱地址!"; + } + break; + case WECHAT_MINI_PROGRAM: + if (!StringUtils.isNumeric(sendAccount)) { + return "请输入正确的用户id"; + } + break; + default: + return ""; + } + return ""; + } + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/sms/config/ReadConfig.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/sms/config/ReadConfig.java new file mode 100644 index 0000000..184b7f4 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/sms/config/ReadConfig.java @@ -0,0 +1,46 @@ +package com.bnhz.notify.core.sms.config; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.enums.NotifyChannelProviderEnum; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.domain.NotifyTemplate; +import com.bnhz.notify.service.INotifyChannelService; +import com.bnhz.notify.service.INotifyTemplateService; +import org.dromara.sms4j.core.datainterface.SmsReadConfig; +import org.dromara.sms4j.provider.config.BaseConfig; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author gsb + * @date 2023/12/14 17:10 + */ +@Component +public class ReadConfig implements SmsReadConfig { + + @Resource + private INotifyChannelService notifyChannelService; + @Resource + private INotifyTemplateService notifyTemplateService; + + @Override + public BaseConfig getSupplierConfig(String notifyTemplateId) { + NotifyTemplate notifyTemplate = notifyTemplateService.selectNotifyTemplateById(Long.valueOf(notifyTemplateId)); + NotifyChannel notifyChannel = notifyChannelService.selectNotifyChannelById(notifyTemplate.getChannelId()); + NotifyChannelProviderEnum notifyChannelProviderEnum = NotifyChannelProviderEnum.getByChannelTypeAndProvider(notifyChannel.getChannelType(), notifyChannel.getProvider()); + // 注意:因为配置参数是分开渠道和模版配的,所以这里需要先转换,再copy一下 + BaseConfig baseConfig = (BaseConfig) JSONObject.parseObject(notifyChannel.getConfigContent(), notifyChannelProviderEnum.getConfigContentClass()); + CopyOptions copyOptions = CopyOptions.create(null, true); + BeanUtil.copyProperties(JSONObject.parseObject(notifyTemplate.getMsgParams(), notifyChannelProviderEnum.getMsgParamsClass()), baseConfig, copyOptions); + return baseConfig; + } + + @Override + public List getSupplierConfigList() { + return null; + } +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/sms/service/ISmsService.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/sms/service/ISmsService.java new file mode 100644 index 0000000..52fe8a5 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/sms/service/ISmsService.java @@ -0,0 +1,68 @@ +package com.bnhz.notify.core.sms.service; + +import com.bnhz.common.core.notify.NotifySendResponse; +import com.bnhz.notify.vo.NotifyVO; +import org.dromara.sms4j.api.SmsBlend; +import org.dromara.sms4j.api.entity.SmsResponse; + +import java.util.LinkedHashMap; +import java.util.List; + +/** + * 短信发送接口 + * @author gsb + * @date 2023/12/15 11:01 + */ +public interface ISmsService { + + /** + * 根据模版统一发送短信 + * @param notifyVO 发送类 + * @return com.bnhz.common.core.notify.NotifySendResponse + */ + NotifySendResponse send(NotifyVO notifyVO); + + + /** + * 单个电话发送短信 + * @param phone 电话 + * @param message 模板内容 + * @return 结果 + */ + SmsResponse sendMessage(SmsBlend smsBlend, String phone, String message); + + /** + * 根据模板id发送 --多参数 + * @param phone 电话 + * @param templateId 模板id + * @param messages 内容集合 + * @return 结果 + */ + SmsResponse sendMessage(SmsBlend smsBlend, String phone, String templateId, LinkedHashMap messages); + + /** + * 群发短信 + * @param phones 电话集合 + * @param templateId 模板id + * @param messages 内容集合 + * @return 结果 + */ + SmsResponse massTexting(SmsBlend smsBlend, List phones, String templateId, LinkedHashMap messages); + + /** + * 延迟发送 + * @param phone 电话 + * @param message 模板内容 + * @param delayedTime 延迟时间 + */ + void delayedMessage(SmsBlend smsBlend,String phone ,String message,Long delayedTime); + + /** + * 根据模板延迟发送 + * @param phone 电话 + * @param messages 模板内容集合 + * @param delayedTime 延迟时间 + */ + void delayedMessage(SmsBlend smsBlend, String phone ,String templateId, LinkedHashMap messages,Long delayedTime); + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/sms/service/Impl/SmsServiceImpl.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/sms/service/Impl/SmsServiceImpl.java new file mode 100644 index 0000000..b7f9df7 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/sms/service/Impl/SmsServiceImpl.java @@ -0,0 +1,139 @@ +package com.bnhz.notify.core.sms.service.Impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.core.notify.NotifySendResponse; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.notify.core.sms.config.ReadConfig; +import com.bnhz.notify.core.sms.service.ISmsService; +import com.bnhz.notify.vo.NotifyVO; +import org.dromara.sms4j.api.SmsBlend; +import org.dromara.sms4j.api.entity.SmsResponse; +import org.dromara.sms4j.core.factory.SmsFactory; +import org.dromara.sms4j.provider.config.BaseConfig; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; + +/** + * @author gsb + * @date 2023/12/15 11:03 + */ +@Service +public class SmsServiceImpl implements ISmsService { + + @Resource + private ReadConfig config; + + @Override + public NotifySendResponse send(NotifyVO notifyVO) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + // 注意:因为配置参数是分开渠道和模版配的,所以这里需要先转换,再copy一下 + BaseConfig baseConfig = (BaseConfig) JSONObject.parseObject(notifyVO.getNotifyChannel().getConfigContent(), notifyVO.getNotifyChannelProviderEnum().getConfigContentClass()); + CopyOptions copyOptions = CopyOptions.create(null, true); + BeanUtil.copyProperties(JSONObject.parseObject(notifyVO.getNotifyTemplate().getMsgParams(), notifyVO.getNotifyChannelProviderEnum().getMsgParamsClass()), baseConfig, copyOptions); + JSONObject jsonMsgParams = JSONObject.parseObject(notifyVO.getNotifyTemplate().getMsgParams()); + String content = jsonMsgParams.get("content").toString(); + LinkedHashMap map = notifyVO.getMap(); + String sendContent = ""; + switch (notifyVO.getNotifyChannelProviderEnum()) { + case SMS_ALIBABA: + sendContent = StringUtils.strReplaceVariable("${", "}", content, map); + break; + case SMS_TENCENT: + sendContent = StringUtils.strReplaceVariable("{", "}", content, map); + break; + default: + break; + } + notifySendResponse.setSendContent(sendContent); + SmsBlend smsBlend = this.getSmsInstance(notifyVO.getNotifyTemplate().getId().toString()); + List phoneList = StringUtils.str2List(notifyVO.getSendAccount(), ",", true, true); + SmsResponse smsResponse = this.massTexting(smsBlend, phoneList, baseConfig.getTemplateId(), map); + notifySendResponse.setStatus(smsResponse.isSuccess() ? 1 : 0); + notifySendResponse.setResultContent(JSON.toJSONString(smsResponse.getData())); + return notifySendResponse; + } + + /** + * 获取短信实例 + * @return + */ + private SmsBlend getSmsInstance(String configId){ + SmsBlend smsBlend = SmsFactory.getSmsBlend(configId); + if (Objects.isNull(smsBlend)){ + //如果没有初始化,则先进行初始化 + SmsFactory.createSmsBlend(config, configId); + return SmsFactory.getSmsBlend(configId); + } + return smsBlend; + } + + /** + * 单个电话发送短信 + * + * @param phone 电话 + * @param message 模板内容 + * @return 结果 + */ + @Override + public SmsResponse sendMessage(SmsBlend smsBlend, String phone, String message) { + return smsBlend.sendMessage(phone, message); + } + + /** + * 根据模板id发送 --多参数 + * + * @param phone 电话 + * @param templateId 模板id + * @param messages 内容集合 + * @return 结果 + */ + @Override + public SmsResponse sendMessage(SmsBlend smsBlend, String phone, String templateId, LinkedHashMap messages) { + return smsBlend.sendMessage(phone, templateId, messages); + } + + /** + * 群发短信 + * + * @param phones 电话集合 + * @param templateId 模板id + * @param messages 内容集合 + * @return 结果 + */ + @Override + public SmsResponse massTexting(SmsBlend smsBlend, List phones, String templateId, LinkedHashMap messages) { + return smsBlend.massTexting(phones, templateId, messages); + } + + /** + * 延迟发送 + * + * @param phone 电话 + * @param message 模板内容 + * @param delayedTime 延迟时间 + */ + @Override + public void delayedMessage(SmsBlend smsBlend, String phone, String message, Long delayedTime) { + smsBlend.delayedMessage(phone, message, delayedTime); + } + + /** + * 根据模板延迟发送 + * + * @param phone 电话 + * @param messages 模板内容集合 + * @param delayedTime 延迟时间 + */ + @Override + public void delayedMessage(SmsBlend smsBlend, String phone, String templateId, LinkedHashMap messages, Long delayedTime) { + smsBlend.delayedMessage(phone, templateId, messages, delayedTime); + } + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/vo/SendParams.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/vo/SendParams.java new file mode 100644 index 0000000..0317855 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/vo/SendParams.java @@ -0,0 +1,26 @@ +package com.bnhz.notify.core.vo; + +import lombok.Data; + +/** + * @author fastb + * @version 1.0 + * @description: 通知测试传参 + * @date 2023-12-28 15:15 + */ +@Data +public class SendParams { + + /** + * 模板编号 + */ + private Long id; + /** + * 发送账号:手机号、邮箱、用户id + */ + private String sendAccount; + /** + * 模板内容变量json字符串 + */ + private String variables; +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/voice/config/VoiceConfig.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/voice/config/VoiceConfig.java new file mode 100644 index 0000000..778346f --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/voice/config/VoiceConfig.java @@ -0,0 +1,31 @@ +package com.bnhz.notify.core.voice.config; + +import com.aliyun.dyvmsapi20170525.Client; +import com.aliyun.teaopenapi.models.Config; + +/** + * @author fastb + * @version 1.0 + * @description: 语音配置类 + * @date 2024-01-11 16:06 + */ +public class VoiceConfig { + + /** + * 使用AK&SK初始化账号Client + * @param accessKeyId + * @param accessKeySecret + * @return Client + * @throws Exception + */ + public static Client createClient(String accessKeyId, String accessKeySecret) throws Exception { + Config config = new Config() + // 必填,您的 AccessKey ID + .setAccessKeyId(accessKeyId) + // 必填,您的 AccessKey Secret + .setAccessKeySecret(accessKeySecret); + // Endpoint 请参考 https://api.aliyun.com/product/Dyvmsapi + config.endpoint = "dyvmsapi.aliyuncs.com"; + return new Client(config); + } +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/voice/service/VoiceService.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/voice/service/VoiceService.java new file mode 100644 index 0000000..66f20ff --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/voice/service/VoiceService.java @@ -0,0 +1,20 @@ +package com.bnhz.notify.core.voice.service; + +import com.bnhz.common.core.notify.NotifySendResponse; +import com.bnhz.notify.vo.NotifyVO; + +/** + * @description: 语音通知服务类 + * @author fastb + * @date 2023-12-15 11:05 + * @version 1.0 + */ +public interface VoiceService { + + /** + * 语音发送 + * @param notifyVO 发送参数 + * @return + */ + NotifySendResponse send(NotifyVO notifyVO); +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/voice/service/impl/VoiceServiceImpl.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/voice/service/impl/VoiceServiceImpl.java new file mode 100644 index 0000000..c15127b --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/voice/service/impl/VoiceServiceImpl.java @@ -0,0 +1,212 @@ +package com.bnhz.notify.core.voice.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.aliyun.dyvmsapi20170525.Client; +import com.aliyun.dyvmsapi20170525.models.SingleCallByTtsRequest; +import com.aliyun.dyvmsapi20170525.models.SingleCallByTtsResponse; +import com.aliyun.teautil.models.RuntimeOptions; +import com.bnhz.common.core.notify.NotifySendResponse; +import com.bnhz.common.core.notify.config.VoiceConfigParams; +import com.bnhz.common.core.notify.msg.VoiceMsgParams; +import com.bnhz.common.enums.NotifyChannelProviderEnum; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.notify.core.voice.config.VoiceConfig; +import com.bnhz.notify.core.voice.service.VoiceService; +import com.bnhz.notify.vo.NotifyVO; +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import com.tencentcloudapi.vms.v20200902.VmsClient; +import com.tencentcloudapi.vms.v20200902.models.SendTtsVoiceRequest; +import com.tencentcloudapi.vms.v20200902.models.SendTtsVoiceResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +/** + * @author fastb + * @version 1.0 + * @description: 语音通知发送业务类 + * @date 2023-12-26 9:54 + */ +@Slf4j +@Service +public class VoiceServiceImpl implements VoiceService { + + @Override + public NotifySendResponse send(NotifyVO notifyVO) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + VoiceConfigParams configParams = JSONObject.parseObject(notifyVO.getNotifyChannel().getConfigContent(), VoiceConfigParams.class); + VoiceMsgParams msgParams = JSONObject.parseObject(notifyVO.getNotifyTemplate().getMsgParams(), VoiceMsgParams.class); + LinkedHashMap map = notifyVO.getMap(); + NotifyChannelProviderEnum notifyChannelProviderEnum = notifyVO.getNotifyChannelProviderEnum(); + if (StringUtils.isEmpty(notifyVO.getSendAccount())) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent("发送电话不能为空,请先配置!"); + return notifySendResponse; + } + String phoneStr = notifyVO.getSendAccount(); + List phoneList = StringUtils.str2List(phoneStr, ",", true, true); + String sendContent = ""; + List resultList = new ArrayList<>(); + for (String phone : phoneList) { + switch (notifyChannelProviderEnum) { + case VOICE_ALIBABA: + sendContent = StringUtils.strReplaceVariable("${", "}", msgParams.getContent(), map); + try { + notifySendResponse = singleCallByTts(configParams, msgParams, map, phone); + } catch (Exception e) { + log.error("阿里云语音通知异常,phone:{}, exception:{}", phone, e.toString()); + } + break; + case VOICE_TENCENT: + sendContent = StringUtils.strReplaceVariable("{", "}", msgParams.getContent(), map); + notifySendResponse = this.sendTtsVoice(configParams, msgParams, map, phone); + break; + default: + break; + } + resultList.add("phone:[" + phone + "],status:[" + notifySendResponse.getStatus() + "],resultContent:[" + notifySendResponse.getResultContent() + "]"); + } + notifySendResponse.setSendContent(sendContent); + notifySendResponse.setResultContent(StringUtils.join(resultList, "; ")); + return notifySendResponse; + } + + + + /** + * @description: 阿里云文本转语音通知 + * @param: configParams 渠道服务商配置参数 + * @param: msgParams 模版通知内容 + * @param: map 变量参数 + * @param: phone 通知电话 + * @return: com.aliyun.dyvmsapi20170525.models.SingleCallByTtsResponse + */ + public NotifySendResponse singleCallByTts(VoiceConfigParams configParams, VoiceMsgParams msgParams, LinkedHashMap map, String phone) throws Exception { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + try { + Client client = VoiceConfig.createClient(configParams.getAccessKeyId(), configParams.getAccessKeySecret()); + SingleCallByTtsRequest singleCallByTtsRequest = new SingleCallByTtsRequest() + .setTtsCode(msgParams.getTemplateId()) + .setCalledNumber(phone) + .setTtsParam(JSON.toJSONString(map)) + .setPlayTimes(StringUtils.isNotEmpty(msgParams.getPlayTimes()) ? Integer.parseInt(msgParams.getPlayTimes()) : 1) + .setVolume(StringUtils.isNotEmpty(msgParams.getVolume()) ? Integer.parseInt(msgParams.getVolume()) : 50) + .setSpeed(StringUtils.isNotEmpty(msgParams.getSpeed()) ? Integer.parseInt(msgParams.getSpeed()) : 0); + RuntimeOptions runtimeOptions = new RuntimeOptions(); + SingleCallByTtsResponse singleCallByTtsResponse = client.singleCallByTtsWithOptions(singleCallByTtsRequest, runtimeOptions); + notifySendResponse.setStatus("OK".equals(singleCallByTtsResponse.getBody().getCode()) ? 1 : 0); + notifySendResponse.setResultContent(JSON.toJSONString(singleCallByTtsResponse.getBody())); + } catch (Exception e) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent(e.toString()); + } + return notifySendResponse; + } + + public NotifySendResponse sendTtsVoice(VoiceConfigParams configParams, VoiceMsgParams msgParams, LinkedHashMap map, String phone) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + try { + /* 必要步骤: + * 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。 + * 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。 + * 您也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人, + * 以免泄露密钥对危及您的财产安全。 + * CAM密匙查询: https://console.cloud.tencent.com/cam/capi*/ + Credential cred = new Credential(configParams.getAccessKeyId(), configParams.getAccessKeySecret()); + + + // 实例化一个http选项,可选,没有特殊需求可以跳过 + HttpProfile httpProfile = new HttpProfile(); + // 设置代理 +// httpProfile.setProxyHost("host"); +// httpProfile.setProxyPort(port); + // SDK默认使用POST方法。 + // 如果您一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 + httpProfile.setReqMethod("POST"); + /* SDK有默认的超时时间,非必要请不要进行调整 + * 如有需要请在代码中查阅以获取最新的默认值 */ + httpProfile.setConnTimeout(60); + /* SDK会自动指定域名。通常是不需要特地指定域名的,但是如果您访问的是金融区的服务 + * 则必须手动指定域名,例如vms的上海金融区域名: vms.ap-shanghai-fsi.tencentcloudapi.com */ + httpProfile.setEndpoint("vms.tencentcloudapi.com"); + + + /* 非必要步骤: + * 实例化一个客户端配置对象,可以指定超时时间等配置 */ + ClientProfile clientProfile = new ClientProfile(); + /* SDK默认用TC3-HMAC-SHA256进行签名 + * 非必要请不要修改这个字段 */ + clientProfile.setSignMethod("TC3-HMAC-SHA256"); + clientProfile.setHttpProfile(httpProfile); + /* 实例化要请求产品(以vms为例)的client对象 + * 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,或者引用预设的常量 */ + VmsClient client = new VmsClient(cred, "ap-guangzhou", clientProfile); + /* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数 + * 您可以直接查询SDK源码确定接口有哪些属性可以设置 + * 属性可能是基本类型,也可能引用了另一个数据结构 + * 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */ + SendTtsVoiceRequest req = new SendTtsVoiceRequest(); + + + /* 填充请求参数,这里request对象的成员变量即对应接口的入参 + * 您可以通过官网接口文档或跳转到request对象的定义处查看请求参数的定义 + * 基本类型的设置: + * 帮助链接: + * 语音消息控制台:https://console.cloud.tencent.com/vms + * vms helper:https://cloud.tencent.com/document/product/1128/37720 */ + + + // 模板 ID,必须填写在控制台审核通过的模板 ID,可登录 [语音消息控制台] 查看模板 ID + String templateId = msgParams.getTemplateId(); + req.setTemplateId(templateId); + + + // 模板参数,若模板没有参数,请提供为空数组 + String[] templateParamSet = map.values().toArray(new String[0]);; + req.setTemplateParamSet(templateParamSet); + + + /* 被叫手机号码,采用 e.164 标准,格式为+[国家或地区码][用户号码] + * 例如:+8613711112222,其中前面有一个+号,86为国家码,13711112222为手机号 */ + String calledNumber = "+86" + phone; + req.setCalledNumber(calledNumber); + + + // 在 [语音控制台] 添加应用后生成的实际SdkAppid,示例如1400006666 + String voiceSdkAppid = msgParams.getSdkAppId(); + req.setVoiceSdkAppid(voiceSdkAppid); + + + // 播放次数,可选,最多3次,默认2次 + Long playTimes = 2L; + req.setPlayTimes(playTimes); + + + // 用户的 session 内容,腾讯 server 回包中会原样返回 + String sessionContext = phone; + req.setSessionContext(sessionContext); + + + /* 通过 client 对象调用 SendTtsVoice 方法发起请求。注意请求方法名与请求对象是对应的 + * 返回的 res 是一个 SendTtsVoiceResponse 类的实例,与请求对象对应 */ + SendTtsVoiceResponse response = client.SendTtsVoice(req); + notifySendResponse.setStatus(1); + notifySendResponse.setResultContent(JSON.toJSONString(response)); + + + } catch (TencentCloudSDKException e) { +// log.error("腾讯云语音通知异常,phone:{}, exception:{}", phone, e.toString()); + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent(e.toString()); + } + return notifySendResponse; + } + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/service/WeChatPushService.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/service/WeChatPushService.java new file mode 100644 index 0000000..d322591 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/service/WeChatPushService.java @@ -0,0 +1,40 @@ +package com.bnhz.notify.core.wechat.service; + +import com.bnhz.common.core.notify.NotifySendResponse; +import com.bnhz.notify.core.wechat.vo.WeChatMiniPushVO; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.domain.NotifyTemplate; +import com.bnhz.notify.vo.NotifyVO; + +/** + * @description: 微信通知推送业务类 + * @author fastb + * @date 2023-12-29 16:39 + * @version 1.0 + */ +public interface WeChatPushService { + + /** + * 统一发送接口 + * @param notifyVO 发送参数 + * @return + */ + NotifySendResponse send(NotifyVO notifyVO); + + /** + * @description: 推送消息给指定的用户 --微信小程序服务号推送 + * @param: wxMssVo + * @param: url + * @return: java.lang.String + */ + NotifySendResponse weChatPostPush(String json, String url); + + /** + * @description: 生成微信小程序基本推送参数 + * @param: notifyTemplateId + * @return: com.bnhz.notify.core.wechat.vo.WeChatMiniPushVO + */ + WeChatMiniPushVO createWeChatMiniPushVO(NotifyChannel notifyChannel, NotifyTemplate notifyTemplate, Long userId); + +} + diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/service/impl/WeChatPushServiceImpl.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/service/impl/WeChatPushServiceImpl.java new file mode 100644 index 0000000..8dd29ec --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/service/impl/WeChatPushServiceImpl.java @@ -0,0 +1,399 @@ +package com.bnhz.notify.core.wechat.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.constant.BnhzConstant; +import com.bnhz.common.core.notify.NotifySendResponse; +import com.bnhz.common.core.notify.config.WeChatConfigParams; +import com.bnhz.common.core.notify.msg.WeComMsgParams; +import com.bnhz.common.core.notify.msg.WechatMsgParams; +import com.bnhz.common.core.redis.RedisCache; +import com.bnhz.common.enums.NotifyChannelProviderEnum; +import com.bnhz.common.enums.SocialPlatformType; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.http.HttpUtils; +import com.bnhz.common.utils.wechat.WechatUtils; +import com.bnhz.common.wechat.WeChatAppResult; +import com.bnhz.iot.domain.SocialUser; +import com.bnhz.iot.service.ISocialUserService; +import com.bnhz.notify.core.wechat.service.WeChatPushService; +import com.bnhz.notify.core.wechat.vo.TemplateDataVo; +import com.bnhz.notify.core.wechat.vo.WeChatMiniPushVO; +import com.bnhz.notify.core.wechat.vo.WeChatPublicAccountPushVO; +import com.bnhz.notify.core.wechat.vo.WxMssVo; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.domain.NotifyTemplate; +import com.bnhz.notify.vo.NotifyVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @author fastb + * @version 1.0 + * @description: 微信相关发送服务类 + * @date 2023-12-26 17:15 + */ +@Slf4j +@Service +public class WeChatPushServiceImpl implements WeChatPushService { + + @Resource + private ISocialUserService socialUserService; + private static RestTemplate restTemplate; + @Resource + private RedisCache redisCache; + + + @Override + public NotifySendResponse send(NotifyVO notifyVO) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + NotifyChannelProviderEnum notifyChannelProviderEnum = notifyVO.getNotifyChannelProviderEnum(); + switch (notifyChannelProviderEnum) { + case WECHAT_MINI_PROGRAM: + notifySendResponse = this.weChatMiniSend(notifyVO); + break; + case WECHAT_WECOM_ROBOT: + notifySendResponse = this.weComRobotSend(notifyVO); + break; + case WECHAT_WECOM_APPLY: + notifySendResponse = this.weComApplySend(notifyVO); + break; + case WECHAT_PUBLIC_ACCOUNT: + notifySendResponse = this.weChatPublicAccountSend(notifyVO); + default: + break; + } + return notifySendResponse; + } + + /** + * 微信公众号发送 + * @param notifyVO 通知参数 + * @return com.bnhz.common.core.notify.NotifySendResponse + */ + private NotifySendResponse weChatPublicAccountSend(NotifyVO notifyVO) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + if (StringUtils.isEmpty(notifyVO.getSendAccount())) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent("发送用户id为空,请先配置发送用户id!"); + return notifySendResponse; + } + LinkedHashMap map = notifyVO.getMap(); + LinkedHashMap mapVariable = new LinkedHashMap<>(); + Map sendMap = new HashMap<>(5); + for (Map.Entry m : map.entrySet()) { + sendMap.put(m.getKey(), new TemplateDataVo(m.getValue())); + mapVariable.put(m.getKey() + ".DATA", m.getValue()); + } + NotifyChannel notifyChannel = notifyVO.getNotifyChannel(); + NotifyTemplate notifyTemplate = notifyVO.getNotifyTemplate(); + WeChatConfigParams weChatConfigParams = JSONObject.parseObject(notifyChannel.getConfigContent(), WeChatConfigParams.class); + WechatMsgParams wechatMsgParams = JSONObject.parseObject(notifyTemplate.getMsgParams(), WechatMsgParams.class); + // 获取accessToken + if (StringUtils.isEmpty(weChatConfigParams.getAppId()) || StringUtils.isEmpty(weChatConfigParams.getAppSecret())) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent("通知渠道配置参数为空,请先配置!"); + return notifySendResponse; + } + WeChatAppResult weChatAppResult = WechatUtils.getAccessToken(weChatConfigParams.getAppId(), weChatConfigParams.getAppSecret()); + if (ObjectUtil.isNull(weChatAppResult) || StringUtils.isEmpty(weChatAppResult.getAccessToken())) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent("获取AccessToken失败,原因:" + JSON.toJSONString(weChatAppResult)); + return notifySendResponse; + } + String pushUrl = BnhzConstant.URL.WX_PUBLIC_ACCOUNT_TEMPLATE_SEND_URL_PREFIX + weChatAppResult.getAccessToken(); + + // 组装发送参数 + WeChatPublicAccountPushVO weChatPublicAccountPushVO = new WeChatPublicAccountPushVO(); + weChatPublicAccountPushVO.setData(sendMap); + weChatPublicAccountPushVO.setTemplateId(wechatMsgParams.getTemplateId()); + if (StringUtils.isNotEmpty(wechatMsgParams.getRedirectUrl())) { + weChatPublicAccountPushVO.setUrl(wechatMsgParams.getRedirectUrl()); + } + if (StringUtils.isNotEmpty(wechatMsgParams.getPagePath())) { + WeChatPublicAccountPushVO.MiniProgram miniProgram = new WeChatPublicAccountPushVO.MiniProgram(); + miniProgram.setAppId(wechatMsgParams.getAppid()); + miniProgram.setPagePath(wechatMsgParams.getPagePath()); + weChatPublicAccountPushVO.setMiniProgram(miniProgram); + } + // 获取用户id + List userIdList = StringUtils.str2List(notifyVO.getSendAccount(), ",", true, true); + List socialUserList = socialUserService.listWechatPublicAccountOpenId(userIdList); + Map userMap = socialUserList.stream().collect(Collectors.toMap(SocialUser::getSysUserId, SocialUser::getOpenId, (o, n) -> n)); + + List resultContentList = new ArrayList<>(); + for (String userId : userIdList) { + String openId = userMap.get(Long.valueOf(userId)); + if (StringUtils.isEmpty(openId)) { + resultContentList.add("userId:[" + userId + "],status:[0],resultContent:[该用户未绑定微信,请先绑定后重试!]"); + notifySendResponse.setStatus(0); + continue; + } + weChatPublicAccountPushVO.setTouser(openId); + notifySendResponse = this.weChatPostPush(JSON.toJSONString(weChatPublicAccountPushVO), pushUrl); + resultContentList.add("userId:[" + userId + "],status:[" + notifySendResponse.getStatus() +"],resultContent:[" + notifySendResponse.getResultContent() + "]"); + } + String content = JSONObject.parseObject(notifyVO.getNotifyTemplate().getMsgParams()).get("content").toString(); + String sendContent = StringUtils.strReplaceVariable("{{", "}}", content, mapVariable); + notifySendResponse.setSendContent(sendContent); + notifySendResponse.setResultContent(StringUtils.join(resultContentList, "; ")); + return notifySendResponse; + } + + /** + * 企业微信应用消息发送 + * @param notifyVO 发送vo类 + * @return com.bnhz.common.core.notify.NotifySendResponse + */ + private NotifySendResponse weComApplySend(NotifyVO notifyVO) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + if (StringUtils.isEmpty(notifyVO.getSendAccount())) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent("发送成员账号为空,请先配置!"); + return notifySendResponse; + } + WeChatConfigParams weChatConfigParams = JSONObject.parseObject(notifyVO.getNotifyChannel().getConfigContent(), WeChatConfigParams.class); + if (StringUtils.isEmpty(weChatConfigParams.getCorpId()) || StringUtils.isEmpty(weChatConfigParams.getCorpSecret())) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent("企业微信应用消息渠道配置信息为空,请先配置!"); + return notifySendResponse; + } + WeComMsgParams weComMsgParams = JSONObject.parseObject(notifyVO.getNotifyTemplate().getMsgParams(), WeComMsgParams.class); + // 获取accessToken,优先从缓存获取,不能频繁的获取 + String accessToken; + Object accessTokenRedis = redisCache.getCacheObject(BnhzConstant.REDIS.NOTIFY_WECOM_APPLY_ACCESSTOKEN + weChatConfigParams.getAgentId()); + if (Objects.nonNull(accessTokenRedis)) { + accessToken = accessTokenRedis.toString(); + } else { + String s = HttpUtils.sendGet(BnhzConstant.URL.WECOM_GET_ACCESSTOKEN + "?corpid=" + weChatConfigParams.getCorpId() + "&corpsecret=" + weChatConfigParams.getCorpSecret()); + JSONObject accessTokenJson = JSONObject.parseObject(s); + if (!"0".equals(accessTokenJson.get("errcode").toString())) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent(s); + return notifySendResponse; + } + accessToken = accessTokenJson.get("access_token").toString(); + redisCache.setCacheObject(BnhzConstant.REDIS.NOTIFY_WECOM_APPLY_ACCESSTOKEN + weChatConfigParams.getAgentId(), accessToken, 2, TimeUnit.HOURS); + } + // 通过手机号获取企业微信用户名 +// JSONObject phoneReq = new JSONObject(); +// phoneReq.put("mobile", notifyVO.getSendAccount()); +// String phoneRes = HttpUtils.sendPost("https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?access_token=" + accessToken, phoneReq.toString()); +// JSONObject phoneResJson = JSONObject.parseObject(phoneRes); +// if (!"0".equals(phoneResJson.get("errcode").toString())) { +// notifySendResponse.setStatus(0); +// notifySendResponse.setResultContent(phoneRes); +// return notifySendResponse; +// } +// String userid = phoneResJson.get("userid").toString(); + // 构建消息内容 + String sendContent = StringUtils.strReplaceVariable("${", "}", weComMsgParams.getContent(), notifyVO.getMap()); + notifySendResponse.setSendContent(sendContent); + JSONObject msg = this.createWeComMsg(weComMsgParams, sendContent); + // 多个用户用|分隔 + List userIdList = StringUtils.str2List(notifyVO.getSendAccount(), ",", true, true); + String userIdStr = String.join("|", userIdList); + msg.put("touser", userIdStr); + msg.put("agentid", weChatConfigParams.getAgentId()); + // 发送 + String sendUrl = BnhzConstant.URL.WECOM_APPLY_SEND + accessToken; + String result = HttpUtils.sendPost(sendUrl, msg.toString()); + notifySendResponse.setStatus("0".equals(JSONObject.parseObject(result).get("errcode").toString()) ? 1 : 0); + notifySendResponse.setResultContent(result); + return notifySendResponse; + } + + /** + * 企业微信群机器人发送 + * @param notifyVO 发送配置类 + * @return com.bnhz.common.core.notify.NotifySendResponse + */ + private NotifySendResponse weComRobotSend(NotifyVO notifyVO) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + NotifyChannel notifyChannel = notifyVO.getNotifyChannel(); + NotifyTemplate notifyTemplate = notifyVO.getNotifyTemplate(); + WeChatConfigParams weChatConfigParams = JSONObject.parseObject(notifyChannel.getConfigContent(), WeChatConfigParams.class); + if (StringUtils.isEmpty(weChatConfigParams.getWebHook())) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent("企业微信群机器人webHook为空,请先去通知渠道下配置!"); + return notifySendResponse; + } + WeComMsgParams weComMsgParams = JSONObject.parseObject(notifyTemplate.getMsgParams(), WeComMsgParams.class); + String sendContent = StringUtils.strReplaceVariable("${", "}", weComMsgParams.getContent(), notifyVO.getMap()); + JSONObject sendJson = this.createWeComMsg(weComMsgParams, sendContent); + String s = HttpUtils.sendPost(weChatConfigParams.getWebHook(), sendJson.toString()); + notifySendResponse.setSendContent(sendJson.toJSONString()); + notifySendResponse.setStatus("0".equals(JSONObject.parseObject(s).get("errcode").toString()) ? 1 : 0); + notifySendResponse.setResultContent(s); + return notifySendResponse; + } + + /** + * 构建企业微信发送参数 + * @param weComMsgParams 消息配置参数 + * @param: sendContent + * @return java.lang.String + */ + private JSONObject createWeComMsg(WeComMsgParams weComMsgParams, String sendContent) { + JSONObject req = new JSONObject(); + String msgType = weComMsgParams.getMsgType(); + req.put("msgtype", msgType); + switch (msgType) { + case "text": + JSONObject text = new JSONObject(); + text.put("content", sendContent); + req.put("text", text); + break; + case "markdown": + JSONObject markdown = new JSONObject(); + markdown.put("content", sendContent); + req.put("markdown", markdown); + break; + case "news": + JSONObject articles = new JSONObject(); + articles.put("title", weComMsgParams.getTitle()); + articles.put("description", sendContent); + articles.put("url", weComMsgParams.getUrl()); + articles.put("picurl", weComMsgParams.getPicUrl()); + JSONObject news = new JSONObject(); + news.put("articles", articles); + req.put("news", news); + break; + default: + break; + } + return req; + } + + /** + * 小程序发送 + * @param notifyVO 发送vo类 + * @return com.bnhz.common.core.notify.NotifySendResponse + */ + private NotifySendResponse weChatMiniSend(NotifyVO notifyVO) { + // 微信小程序 + NotifySendResponse notifySendResponse = new NotifySendResponse(); + if (StringUtils.isEmpty(notifyVO.getSendAccount())) { + notifySendResponse.setStatus(0); + notifySendResponse.setResultContent("发送用户id为空,请先配置发送用户id!"); + return notifySendResponse; + } + LinkedHashMap map = notifyVO.getMap(); + LinkedHashMap mapVariable = new LinkedHashMap<>(); + Map sendMap = new HashMap<>(5); + for (Map.Entry m : map.entrySet()) { + sendMap.put(m.getKey(), new TemplateDataVo(m.getValue())); + mapVariable.put(m.getKey() + ".DATA", m.getValue()); + } + List userIdList = StringUtils.str2List(notifyVO.getSendAccount(), ",", true, true); + List resultContentList = new ArrayList<>(); + for (String userId : userIdList) { + WeChatMiniPushVO weChatMiniPushVO = this.createWeChatMiniPushVO(notifyVO.getNotifyChannel(), notifyVO.getNotifyTemplate(), Long.valueOf(userId)); + if (Objects.isNull(weChatMiniPushVO)) { + resultContentList.add("userId:[" + userId + "],status:[0],resultContent:[获取微信小程序推送配置信息失败!]"); + notifySendResponse.setStatus(0); + continue; + } + if (StringUtils.isNotEmpty(weChatMiniPushVO.getErrorMsg())) { + resultContentList.add("userId:[" + userId + "],status:[0],resultContent:[" + weChatMiniPushVO.getErrorMsg() + "]"); + notifySendResponse.setStatus(0); + continue; + } + WxMssVo wxMssVo = weChatMiniPushVO.getWxMssVo(); + wxMssVo.setData(sendMap); + notifySendResponse = this.weChatPostPush(JSON.toJSONString(wxMssVo), weChatMiniPushVO.getUrl()); + resultContentList.add("userId:[" + userId + "],status:[" + notifySendResponse.getStatus() +"],resultContent:[" + notifySendResponse.getResultContent() + "]"); + } + String content = JSONObject.parseObject(notifyVO.getNotifyTemplate().getMsgParams()).get("content").toString(); + String sendContent = StringUtils.strReplaceVariable("{{", "}}", content, mapVariable); + notifySendResponse.setSendContent(sendContent); + notifySendResponse.setResultContent(StringUtils.join(resultContentList, "; ")); + return notifySendResponse; + } + + /** + * 推送消息给指定的用户 --微信小程序服务号推送 + * @param json 推送参数 + * @return 推送结果 + */ + @Override + public NotifySendResponse weChatPostPush(String json, String url) { + NotifySendResponse notifySendResponse = new NotifySendResponse(); + if(restTemplate==null){ + restTemplate = new RestTemplate(); + } + HttpHeaders headers = new HttpHeaders(); + MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); + headers.setContentType(type); + headers.add("Accept", MediaType.APPLICATION_JSON.toString()); + HttpEntity httpEntity = new HttpEntity<>(json, headers); + ResponseEntity responseEntity = + restTemplate.postForEntity(url, httpEntity, String.class); + log.warn("小程序推送结果={}", responseEntity.getBody()); + String response = responseEntity.getBody(); + notifySendResponse.setStatus("0".equals(JSONObject.parseObject(response).get("errcode").toString()) ? 1 : 0); + notifySendResponse.setResultContent(response); + return notifySendResponse; + } + + @Override + public WeChatMiniPushVO createWeChatMiniPushVO(NotifyChannel notifyChannel, NotifyTemplate notifyTemplate, Long userId) { + WeChatMiniPushVO weChatMiniPushVO = new WeChatMiniPushVO(); + //获取微信与用户关联信息 + SocialUser socialUser = socialUserService.selectByUserIdAndSourceClient(userId, SocialPlatformType.WECHAT_OPEN_MINI_PROGRAM.sourceClient); + if (Objects.isNull(socialUser) || StringUtils.isEmpty(socialUser.getOpenId())) { + weChatMiniPushVO.setErrorMsg("该用户未绑定微信小程序,请先绑定后重试"); + return weChatMiniPushVO; + } + //获取openId + String openId = socialUser.getOpenId(); + //拼接推送的模版 + WxMssVo wxMssVo = new WxMssVo(); + //用户openid + wxMssVo.setTouser(openId); + if (notifyTemplate == null) { + weChatMiniPushVO.setErrorMsg("推送模板为空,请先配置微信小程序推送模板"); + return weChatMiniPushVO; + } + //获取微信服务号推送的配置参数 + if (notifyChannel == null) { + weChatMiniPushVO.setErrorMsg("推送渠道为空,请检查微信小程序推送渠道"); + return weChatMiniPushVO; + } + WeChatConfigParams weChatConfigParams = JSONObject.parseObject(notifyChannel.getConfigContent(), WeChatConfigParams.class); + if (StringUtils.isEmpty(weChatConfigParams.getAppId()) || StringUtils.isEmpty(weChatConfigParams.getAppSecret())) { + weChatMiniPushVO.setErrorMsg("微信小程序渠道配置信息为空,请先配置!"); + return weChatMiniPushVO; + } + //获取access_token + WeChatAppResult weChatAppResult = WechatUtils.getAccessToken(weChatConfigParams.getAppId(), weChatConfigParams.getAppSecret()); + if (weChatAppResult == null || StringUtils.isEmpty(weChatAppResult.getAccessToken())) { + weChatMiniPushVO.setErrorMsg("获取用户调用凭据失败,请重新登录!"); + return weChatMiniPushVO; + } + //微信推送URL + String url = BnhzConstant.URL.WX_MINI_PROGRAM_PUSH_URL_PREFIX + "?access_token=" + weChatAppResult.getAccessToken(); + WechatMsgParams msgParams = JSONObject.parseObject(notifyTemplate.getMsgParams(), WechatMsgParams.class); + //模版id + wxMssVo.setTemplateId(msgParams.getTemplateId()); + //推送路径 + if (StringUtils.isNotEmpty(msgParams.getRedirectUrl())){ + wxMssVo.setPage(msgParams.getRedirectUrl()); + } + weChatMiniPushVO.setWxMssVo(wxMssVo); + weChatMiniPushVO.setUrl(url); + return weChatMiniPushVO; + } + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/TemplateDataVo.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/TemplateDataVo.java new file mode 100644 index 0000000..b2a60dc --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/TemplateDataVo.java @@ -0,0 +1,19 @@ +package com.bnhz.notify.core.wechat.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TemplateDataVo { + + /*微信文档中要求的格式 "data": { "name01": {"value": "某某"},"thing01": {"value": "广州至北京" + } ,"date01": {"value": "2018-01-01"} + }*/ + @JSONField(name = "value") + private String value; + +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/WeChatMiniPushVO.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/WeChatMiniPushVO.java new file mode 100644 index 0000000..83ef4ba --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/WeChatMiniPushVO.java @@ -0,0 +1,19 @@ +package com.bnhz.notify.core.wechat.vo; + +import lombok.Data; + +/** + * @author fastb + * @version 1.0 + * @description: 获取微信小程序服务通知推送类,推送内容变量参数需自己组装 + * @date 2023-12-28 16:38 + */ +@Data +public class WeChatMiniPushVO { + + private WxMssVo wxMssVo; + + private String url; + + private String errorMsg; +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/WeChatPublicAccountPushVO.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/WeChatPublicAccountPushVO.java new file mode 100644 index 0000000..3aa2aae --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/WeChatPublicAccountPushVO.java @@ -0,0 +1,62 @@ +package com.bnhz.notify.core.wechat.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; + +import java.util.Map; + +/** + * @author fastb + * @version 1.0 + * @description: 微信公众号推送参数 + * @date 2024-03-09 14:10 + */ +@Data +public class WeChatPublicAccountPushVO { + /** + * 接收者(用户)的 openid + */ + @JSONField(name = "touser") + private String touser; + /** + * 所需下发的订阅模板id + */ + @JSONField(name = "template_id") + private String templateId; + /** + * 模板跳转链接(海外账号没有跳转能力) + */ + @JSONField(name = "url") + private String url; + /** + * 跳小程序所需数据,不需跳小程序可不用传该数据 + */ + @JSONField(name = "miniprogram") + private MiniProgram miniProgram; + /** + * 防重入id。对于同一个openid + client_msg_id, 只发送一条消息,10分钟有效,超过10分钟不保证效果。若无防重入需求,可不填 + */ + @JSONField(name = "client_msg_id") + private String clientMsgId; + /** + * 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } } + */ + @JSONField(name = "data") + private Map data; + + @Data + public static class MiniProgram { + + /** + * 所需跳转到的小程序appid + */ + @JSONField(name = "appid") + private String appId; + + /** + * 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar),要求该小程序已发布,暂不支持小游戏 + */ + @JSONField(name = "pagepath") + private String pagePath; + } +} diff --git a/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/WxMssVo.java b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/WxMssVo.java new file mode 100644 index 0000000..6cab543 --- /dev/null +++ b/bnhz-notify/bnhz-notify-core/src/main/java/com/bnhz/notify/core/wechat/vo/WxMssVo.java @@ -0,0 +1,50 @@ +package com.bnhz.notify.core.wechat.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; + +import java.util.Map; + +/* + * 小程序推送所需数据 + * */ +@Data +public class WxMssVo { + /** + * 接收者(用户)的 openid + */ + @JSONField(name = "touser") + private String touser; + /** + * 所需下发的订阅模板id + */ + @JSONField(name = "template_id") + private String templateId; + /** + * 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。 + */ + @JSONField(name = "page") + private String page = "pages/index/index"; + /** + * 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } } + */ + @JSONField(name = "data") + private Map data; + /** + * 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 + */ + @JSONField(name = "miniprogram_state") + private String miniprogramState; + /** + * 进入小程序查看”的语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN + */ + @JSONField(name = "lang") + private String lang; + /** + * 默认正式版 和 简体中文 + */ + public WxMssVo() { + this.miniprogramState = "formal"; + this.lang = "zh_CN"; + } +} diff --git a/bnhz-notify/bnhz-notify-web/pom.xml b/bnhz-notify/bnhz-notify-web/pom.xml new file mode 100644 index 0000000..0ea4a5d --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + com.bnhz + bnhz-notify + 3.8.5 + + + bnhz-notify-web + + + 8 + 8 + UTF-8 + + + + + com.bnhz + bnhz-common + + + com.bnhz + bnhz-system-service + + + + diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/controller/NotifyChannelController.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/controller/NotifyChannelController.java new file mode 100644 index 0000000..f59a63e --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/controller/NotifyChannelController.java @@ -0,0 +1,129 @@ +package com.bnhz.notify.controller; + +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.service.INotifyChannelService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 通知渠道Controller + * + * @author kerwincui + * @date 2023-12-01 + */ +@RestController +@RequestMapping("/notify/channel") +@Api(tags = "通知渠道") +public class NotifyChannelController extends BaseController +{ + @Resource + private INotifyChannelService notifyChannelService; + + /** + * 查询通知渠道列表 + */ + @PreAuthorize("@ss.hasPermi('notify:channel:list')") + @GetMapping("/list") + @ApiOperation(value = "查询通知渠道列表") + public TableDataInfo list(NotifyChannel notifyChannel) + { + startPage(); + List list = notifyChannelService.selectNotifyChannelList(notifyChannel); + return getDataTable(list); + } + + /** + * 导出通知渠道列表 + */ + @ApiOperation(value = "导出通知渠道列表") + @PreAuthorize("@ss.hasPermi('notify:channel:export')") + @Log(title = "通知渠道", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, NotifyChannel notifyChannel) + { + List list = notifyChannelService.selectNotifyChannelList(notifyChannel); + ExcelUtil util = new ExcelUtil(NotifyChannel.class); + util.exportExcel(response, list, "通知渠道数据"); + } + + /** + * 获取通知渠道详细信息 + */ + @PreAuthorize("@ss.hasPermi('notify:channel:query')") + @GetMapping(value = "/{id}") + @ApiOperation(value = "获取通知渠道详细信息") + public AjaxResult getInfo(@PathVariable("id") Long id) + { + return success(notifyChannelService.selectNotifyChannelById(id)); + } + + /** + * 新增通知渠道 + */ + @PreAuthorize("@ss.hasPermi('notify:channel:add')") + @Log(title = "通知渠道", businessType = BusinessType.INSERT) + @PostMapping + @ApiOperation(value = "新增通知渠道") + public AjaxResult add(@RequestBody NotifyChannel notifyChannel) + { + return toAjax(notifyChannelService.insertNotifyChannel(notifyChannel)); + } + + /** + * 修改通知渠道 + */ + @PreAuthorize("@ss.hasPermi('notify:channel:edit')") + @Log(title = "通知渠道", businessType = BusinessType.UPDATE) + @PutMapping + @ApiOperation(value = "修改通知渠道") + public AjaxResult edit(@RequestBody NotifyChannel notifyChannel) + { + return toAjax(notifyChannelService.updateNotifyChannel(notifyChannel)); + } + + /** + * 删除通知渠道 + */ + @PreAuthorize("@ss.hasPermi('notify:channel:remove')") + @Log(title = "通知渠道", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + @ApiOperation(value = "删除通知渠道") + public AjaxResult remove(@PathVariable Long[] ids) + { + return toAjax(notifyChannelService.deleteNotifyChannelByIds(ids)); + } + + /** + * 查询通知渠道和服务商 + * @return 结果 + */ + @GetMapping("/listChannel") + @ApiOperation(value = "查询通知渠道和服务商") + public AjaxResult listChannel() { + return AjaxResult.success(notifyChannelService.listChannel()); + } + + /** + * 获取消息通知渠道参数信息 + * @param channelType 渠道类型 + * @param: provider 服务商 + * @return com.bnhz.common.core.domain.AjaxResult + */ + @GetMapping(value = "/getConfigContent") + @ApiOperation("获取渠道参数配置") + public AjaxResult msgParams(String channelType, String provider) { + return success(notifyChannelService.getConfigContent(channelType, provider)); + } +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/controller/NotifyLogController.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/controller/NotifyLogController.java new file mode 100644 index 0000000..a4b17e4 --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/controller/NotifyLogController.java @@ -0,0 +1,105 @@ +package com.bnhz.notify.controller; + +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.notify.domain.NotifyLog; +import com.bnhz.notify.service.INotifyLogService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 通知日志Controller + * + * @author bnhz + * @date 2023-12-16 + */ +@Api(tags = "通知日志") +@RestController +@RequestMapping("/notify/log") +public class NotifyLogController extends BaseController +{ + @Resource + private INotifyLogService notifyLogService; + + /** + * 查询通知日志列表 + */ + @PreAuthorize("@ss.hasPermi('notify:log:list')") + @ApiOperation(value = "查询通知日志列表") + @GetMapping("/list") + public TableDataInfo list(NotifyLog notifyLog) + { + startPage(); + List list = notifyLogService.selectNotifyLogList(notifyLog); + return getDataTable(list); + } + + /** + * 导出通知日志列表 + */ + @PreAuthorize("@ss.hasPermi('notify:log:export')") + @Log(title = "通知日志", businessType = BusinessType.EXPORT) + @ApiOperation(value = "导出通知日志列表") + @PostMapping("/export") + public void export(HttpServletResponse response, NotifyLog notifyLog) + { + List list = notifyLogService.selectNotifyLogList(notifyLog); + ExcelUtil util = new ExcelUtil(NotifyLog.class); + util.exportExcel(response, list, "通知日志数据"); + } + + /** + * 获取通知日志详细信息 + */ + @PreAuthorize("@ss.hasPermi('notify:log:query')") + @ApiOperation(value = "获取通知日志详细信息") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) + { + return success(notifyLogService.selectNotifyLogById(id)); + } + + /** + * 新增通知日志 + */ + @PreAuthorize("@ss.hasPermi('notify:log:add')") + @Log(title = "通知日志", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody NotifyLog notifyLog) + { + return toAjax(notifyLogService.insertNotifyLog(notifyLog)); + } + + /** + * 修改通知日志 + */ + @PreAuthorize("@ss.hasPermi('notify:log:edit')") + @Log(title = "通知日志", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody NotifyLog notifyLog) + { + return toAjax(notifyLogService.updateNotifyLog(notifyLog)); + } + + /** + * 删除通知日志 + */ + @PreAuthorize("@ss.hasPermi('notify:log:remove')") + @Log(title = "通知日志", businessType = BusinessType.DELETE) + @ApiOperation(value = "批量删除通知日志") + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) + { + return toAjax(notifyLogService.deleteNotifyLogByIds(ids)); + } +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/controller/NotifyTemplateController.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/controller/NotifyTemplateController.java new file mode 100644 index 0000000..8fa9cf9 --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/controller/NotifyTemplateController.java @@ -0,0 +1,185 @@ +package com.bnhz.notify.controller; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.enums.NotifyChannelProviderEnum; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.notify.domain.NotifyTemplate; +import com.bnhz.notify.service.INotifyTemplateService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.collections4.MapUtils; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; + +/** + * 通知模版Controller + * + * @author kerwincui + * @date 2023-12-01 + */ +@RestController +@RequestMapping("/notify/template") +@Api(tags = "通知模板配置") +public class NotifyTemplateController extends BaseController { + + @Resource + private INotifyTemplateService notifyTemplateService; + + /** + * 查询通知模版列表 + */ + @PreAuthorize("@ss.hasPermi('notify:template:list')") + @GetMapping("/list") + @ApiOperation("查询通知模版列表") + public TableDataInfo list(NotifyTemplate notifyTemplate) { + startPage(); + List list = notifyTemplateService.selectNotifyTemplateList(notifyTemplate); + return getDataTable(list); + } + + /** + * 导出通知模版列表 + */ + @PreAuthorize("@ss.hasPermi('notify:template:export')") + @Log(title = "通知模版", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @ApiOperation("导出通知模版列表") + public void export(HttpServletResponse response, NotifyTemplate notifyTemplate) { + List list = notifyTemplateService.selectNotifyTemplateList(notifyTemplate); + ExcelUtil util = new ExcelUtil(NotifyTemplate.class); + util.exportExcel(response, list, "通知模版数据"); + } + + /** + * 获取通知模版详细信息 + */ + @PreAuthorize("@ss.hasPermi('notify:template:query')") + @GetMapping(value = "/{id}") + @ApiOperation("获取通知模版详细信息") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return success(notifyTemplateService.selectNotifyTemplateById(id)); + } + + /** + * 新增通知模版 + */ + @PreAuthorize("@ss.hasPermi('notify:template:add')") + @Log(title = "通知模版", businessType = BusinessType.INSERT) + @PostMapping + @ApiOperation("新增通知模版") + public AjaxResult add(@RequestBody NotifyTemplate notifyTemplate) { + return notifyTemplateService.insertNotifyTemplate(notifyTemplate); + } + + /** + * 修改通知模版 + */ + @PreAuthorize("@ss.hasPermi('notify:template:edit')") + @Log(title = "通知模版", businessType = BusinessType.UPDATE) + @PutMapping + @ApiOperation("修改通知模版") + public AjaxResult edit(@RequestBody NotifyTemplate notifyTemplate) { + return notifyTemplateService.updateNotifyTemplate(notifyTemplate); + } + + /** + * 删除通知模版 + */ + @PreAuthorize("@ss.hasPermi('notify:template:remove')") + @Log(title = "通知模版", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + @ApiOperation("删除通知模版") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(notifyTemplateService.deleteNotifyTemplateByIds(ids)); + } + + /** + * 获取消息通知模版参数信息 + */ + @PreAuthorize("@ss.hasPermi('notify:template:query')") + @GetMapping(value = "/msgParams") + @ApiOperation("获取模板参数配置") + public AjaxResult msgParams(Long channelId, String msgType) { + return success(notifyTemplateService.getNotifyMsgParams(channelId, msgType)); + } + + + /** + * 获取通知模版详细信息 + */ + @PreAuthorize("@ss.hasPermi('notify:template:query')") + @GetMapping(value = "/getUsable") + @ApiOperation("获取同一业务的模板是否有可用的") + public AjaxResult getUsable(NotifyTemplate notifyTemplate) { + return success(notifyTemplateService.countNormalTemplate(notifyTemplate)); + } + + + /** + * 修改通知模版-更新选择的为可用,其他为不可用 + */ + @PreAuthorize("@ss.hasPermi('notify:template:edit')") + @PostMapping("/updateState") + @ApiOperation("修改模版启用状态") + public AjaxResult updateState(@RequestBody NotifyTemplate notifyTemplate) { + notifyTemplateService.updateTemplateStatus(notifyTemplate); + return AjaxResult.success(); + } + + /** + * 获取消息通知模版参数变量 + */ + @PreAuthorize("@ss.hasPermi('notify:template:query')") + @GetMapping(value = "/listVariables") + @ApiOperation("获取模板内容变量") + public AjaxResult listVariables(Long id, String channelType, String provider) { + NotifyTemplate notifyTemplate = notifyTemplateService.selectNotifyTemplateById(id); + if (Objects.isNull(notifyTemplate)) { + return success(); + } + String content = JSONObject.parseObject(notifyTemplate.getMsgParams()).get("content").toString(); + Object account = JSONObject.parseObject(notifyTemplate.getMsgParams()).get("sendAccount"); + NotifyChannelProviderEnum notifyChannelProviderEnum = NotifyChannelProviderEnum.getByChannelTypeAndProvider(channelType, provider); + List variables = notifyTemplateService.listVariables(content, notifyChannelProviderEnum); + LinkedHashMap map = new LinkedHashMap<>(); + for (String variable : variables) { + map.put(variable, ""); + } + JSONObject resultData = new JSONObject(); + // 企业微信、钉钉机器人没有发送账号 + if (NotifyChannelProviderEnum.WECHAT_WECOM_ROBOT == notifyChannelProviderEnum || + NotifyChannelProviderEnum.DING_TALK_GROUP_ROBOT == notifyChannelProviderEnum) { + if (MapUtils.isEmpty(map)) { + return AjaxResult.success("操作成功", ""); + } else { + resultData.put("variables", JSON.toJSONString(map)); + return success(resultData); + } + } + resultData.put("sendAccount", Objects.isNull(account) ? "" : account.toString()); + resultData.put("variables", MapUtils.isNotEmpty(map) ? JSON.toJSONString(map) : ""); + return success(resultData); + } + + /** + * 获取告警微信小程序模板id + */ + @GetMapping(value = "/getAlertWechatMini") + @ApiOperation("获取告警微信小程序模板id") + public AjaxResult getAlertWechatMini() { + return success(notifyTemplateService.getAlertWechatMini()); + } + +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/domain/NotifyChannel.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/domain/NotifyChannel.java new file mode 100644 index 0000000..228da51 --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/domain/NotifyChannel.java @@ -0,0 +1,50 @@ +package com.bnhz.notify.domain; + +import com.bnhz.common.annotation.Excel; +import com.bnhz.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 通知渠道对象 notify_channel + * + * @author kerwincui + * @date 2023-12-01 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class NotifyChannel extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long id; + + /** 通知名称 */ + @Excel(name = "通知名称") + private String name; + + /** 发送渠道类型 */ + @Excel(name = "发送渠道类型") + private String channelType; + + /** 服务商 */ + @Excel(name = "服务商") + private String provider; + + /** 配置内容 */ + @Excel(name = "配置内容") + private String configContent; + + /** 租户id */ + private Long tenantId; + + /** 租户名称 */ + private String tenantName; + + /** 逻辑删除标识 */ + private Integer delFlag; + +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/domain/NotifyLog.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/domain/NotifyLog.java new file mode 100644 index 0000000..009b70c --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/domain/NotifyLog.java @@ -0,0 +1,67 @@ +package com.bnhz.notify.domain; + +import com.bnhz.common.annotation.Excel; +import com.bnhz.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 通知日志对象 notify_log + * + * @author bnhz + * @date 2023-12-16 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class NotifyLog extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long id; + + /** 通知模版编号 */ + @Excel(name = "通知模版编号") + private Long notifyTemplateId; + + /** 渠道编号 */ + @Excel(name = "渠道编号") + private Long channelId; + + /** 消息内容 */ + @Excel(name = "消息内容") + private String msgContent; + + /** 发送账号 */ + @Excel(name = "发送账号") + private String sendAccount; + + /** 发送状态 */ + @Excel(name = "发送状态") + private Integer sendStatus; + + /** 返回内容 */ + @Excel(name = "返回内容") + private String resultContent; + + /** 逻辑删除标识 */ + private Integer delFlag; + + /** 渠道名称 */ + private String channelName; + + /** 模板名称 */ + private String templateName; + + /** 租户id */ + private Long tenantId; + + /** 租户名称 */ + private String tenantName; + /** 业务编码 */ + @Excel(name = "业务编码") + private String serviceCode; + +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/domain/NotifyTemplate.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/domain/NotifyTemplate.java new file mode 100644 index 0000000..e443003 --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/domain/NotifyTemplate.java @@ -0,0 +1,65 @@ +package com.bnhz.notify.domain; + +import com.bnhz.common.annotation.Excel; +import com.bnhz.common.core.domain.BaseEntity; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 通知模版对象 notify_template + * + * @author kerwincui + * @date 2023-12-01 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class NotifyTemplate extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 编号 */ + private Long id; + + /** 模版名称 */ + @Excel(name = "模版名称") + private String name; + + /** 通知渠道 */ + @Excel(name = "通知渠道") + private Long channelId; + + /** 业务编码 */ + @Excel(name = "业务编码") + private String serviceCode; + + @ApiModelProperty("模版配置参数") + private String msgParams; + + /** 发送账号 */ + @Excel(name = "是否启用,0-否 1-是") + private Integer status; + + /** 逻辑删除标识 */ + private Integer delFlag; + + private String channelName; + + /** 发送渠道类型 */ + @Excel(name = "发送渠道类型") + private String channelType; + + /** 服务商 */ + @Excel(name = "服务商") + private String provider; + + /** 租户id */ + private Long tenantId; + + /** 租户名称 */ + private String tenantName; + + +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/mapper/NotifyChannelMapper.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/mapper/NotifyChannelMapper.java new file mode 100644 index 0000000..1b6439f --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/mapper/NotifyChannelMapper.java @@ -0,0 +1,70 @@ +package com.bnhz.notify.mapper; + +import com.bnhz.notify.domain.NotifyChannel; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 通知渠道Mapper接口 + * + * @author kerwincui + * @date 2023-12-01 + */ +public interface NotifyChannelMapper +{ + /** + * 查询通知渠道 + * + * @param id 通知渠道主键 + * @return 通知渠道 + */ + public NotifyChannel selectNotifyChannelById(Long id); + + /** + * 查询通知渠道列表 + * + * @param notifyChannel 通知渠道 + * @return 通知渠道集合 + */ + public List selectNotifyChannelList(NotifyChannel notifyChannel); + + /** + * 新增通知渠道 + * + * @param notifyChannel 通知渠道 + * @return 结果 + */ + public int insertNotifyChannel(NotifyChannel notifyChannel); + + /** + * 修改通知渠道 + * + * @param notifyChannel 通知渠道 + * @return 结果 + */ + public int updateNotifyChannel(NotifyChannel notifyChannel); + + /** + * 删除通知渠道 + * + * @param id 通知渠道主键 + * @return 结果 + */ + public int deleteNotifyChannelById(Long id); + + /** + * 批量删除通知渠道 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteNotifyChannelByIds(Long[] ids); + + /** + * 批量查询通知渠道 + * @param idList 主键id集合 + * @return java.util.List + */ + List selectNotifyChannelByIds(@Param("idList") List idList); +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/mapper/NotifyLogMapper.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/mapper/NotifyLogMapper.java new file mode 100644 index 0000000..667a9c7 --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/mapper/NotifyLogMapper.java @@ -0,0 +1,62 @@ +package com.bnhz.notify.mapper; + +import com.bnhz.notify.domain.NotifyLog; + +import java.util.List; + +/** + * 通知日志Mapper接口 + * + * @author bnhz + * @date 2023-12-16 + */ +public interface NotifyLogMapper +{ + /** + * 查询通知日志 + * + * @param id 通知日志主键 + * @return 通知日志 + */ + public NotifyLog selectNotifyLogById(Long id); + + /** + * 查询通知日志列表 + * + * @param notifyLog 通知日志 + * @return 通知日志集合 + */ + public List selectNotifyLogList(NotifyLog notifyLog); + + /** + * 新增通知日志 + * + * @param notifyLog 通知日志 + * @return 结果 + */ + public int insertNotifyLog(NotifyLog notifyLog); + + /** + * 修改通知日志 + * + * @param notifyLog 通知日志 + * @return 结果 + */ + public int updateNotifyLog(NotifyLog notifyLog); + + /** + * 删除通知日志 + * + * @param id 通知日志主键 + * @return 结果 + */ + public int deleteNotifyLogById(Long id); + + /** + * 批量删除通知日志 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteNotifyLogByIds(Long[] ids); +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/mapper/NotifyTemplateMapper.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/mapper/NotifyTemplateMapper.java new file mode 100644 index 0000000..ae5b95f --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/mapper/NotifyTemplateMapper.java @@ -0,0 +1,112 @@ +package com.bnhz.notify.mapper; + +import com.bnhz.notify.domain.NotifyTemplate; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 通知模版Mapper接口 + * + * @author kerwincui + * @date 2023-12-01 + */ +public interface NotifyTemplateMapper +{ + /** + * 查询通知模版 + * + * @param id 通知模版主键 + * @return 通知模版 + */ + public NotifyTemplate selectNotifyTemplateById(Long id); + + /** + * 查询通知模版列表 + * + * @param notifyTemplate 通知模版 + * @return 通知模版集合 + */ + public List selectNotifyTemplateList(NotifyTemplate notifyTemplate); + + /** + * 查询同一业务已启用的模板 + * @param notifyTemplate + * @return + */ + public Integer selectEnableNotifyTemplateCount(NotifyTemplate notifyTemplate); + + /** + * 新增通知模版 + * + * @param notifyTemplate 通知模版 + * @return 结果 + */ + public int insertNotifyTemplate(NotifyTemplate notifyTemplate); + + /** + * 修改通知模版 + * + * @param notifyTemplate 通知模版 + * @return 结果 + */ + public int updateNotifyTemplate(NotifyTemplate notifyTemplate); + + /** + * 批量更新渠道状态 + * @param ids ids + * @return + */ + public int updateNotifyBatch(@Param("ids") List ids, @Param("status") Integer status); + + /** + * 删除通知模版 + * + * @param id 通知模版主键 + * @return 结果 + */ + public int deleteNotifyTemplateById(Long id); + + /** + * 批量删除通知模版 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteNotifyTemplateByIds(Long[] ids); + + /** + * 根据业务编码查询启用模板 + * @param notifyTemplate 通知模板 + * @return com.bnhz.notify.domain.NotifyTemplate + */ + NotifyTemplate selectOnlyEnable(NotifyTemplate notifyTemplate); + + /** + * @description: 批量删除通知模板 + * @param: ids 渠道id数组 + * @return: void + */ + void deleteNotifyTemplateByChannelIds(Long[] channelIds); + + /** + * @description: 查询通知模板 + * @param: templateIdList + * @return: java.util.List + */ + List selectNotifyTemplateByIds(@Param("idList") List idList); + + /** + * 根据渠道id查询模板 + * @param channelId 渠道id + * @return java.util.List + */ + List selectNotifyTemplateByChannelId(Long channelId); + + /** + * 根据场景ID批量删除告警场景 + * @param notifyTemplateIds + * @return + */ + public int deleteAlertNotifyTemplateByNotifyTemplateIds(Long[] notifyTemplateIds); +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/INotifyChannelService.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/INotifyChannelService.java new file mode 100644 index 0000000..87b026a --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/INotifyChannelService.java @@ -0,0 +1,78 @@ +package com.bnhz.notify.service; + +import com.bnhz.common.core.notify.NotifyConfigVO; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.vo.ChannelProviderVO; + +import java.util.List; + +/** + * 通知渠道Service接口 + * + * @author kerwincui + * @date 2023-12-01 + */ +public interface INotifyChannelService +{ + /** + * 查询通知渠道 + * + * @param id 通知渠道主键 + * @return 通知渠道 + */ + public NotifyChannel selectNotifyChannelById(Long id); + + /** + * 查询通知渠道列表 + * + * @param notifyChannel 通知渠道 + * @return 通知渠道集合 + */ + public List selectNotifyChannelList(NotifyChannel notifyChannel); + + /** + * 新增通知渠道 + * + * @param notifyChannel 通知渠道 + * @return 结果 + */ + public int insertNotifyChannel(NotifyChannel notifyChannel); + + /** + * 修改通知渠道 + * + * @param notifyChannel 通知渠道 + * @return 结果 + */ + public int updateNotifyChannel(NotifyChannel notifyChannel); + + /** + * 批量删除通知渠道 + * + * @param ids 需要删除的通知渠道主键集合 + * @return 结果 + */ + public int deleteNotifyChannelByIds(Long[] ids); + + /** + * 删除通知渠道信息 + * + * @param id 通知渠道主键 + * @return 结果 + */ + public int deleteNotifyChannelById(Long id); + + /** + * 查询通知渠道和服务商 + * @return + */ + List listChannel(); + + /** + * 获取消息通知渠道参数信息 + * @param channelType 渠道类型 + * @param: provider 服务商 + * @return 结果集 + */ + List getConfigContent(String channelType, String provider); +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/INotifyLogService.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/INotifyLogService.java new file mode 100644 index 0000000..d442fdf --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/INotifyLogService.java @@ -0,0 +1,62 @@ +package com.bnhz.notify.service; + +import com.bnhz.notify.domain.NotifyLog; + +import java.util.List; + +/** + * 通知日志Service接口 + * + * @author kerwincui + * @date 2023-12-16 + */ +public interface INotifyLogService +{ + /** + * 查询通知日志 + * + * @param id 通知日志主键 + * @return 通知日志 + */ + public NotifyLog selectNotifyLogById(Long id); + + /** + * 查询通知日志列表 + * + * @param notifyLog 通知日志 + * @return 通知日志集合 + */ + public List selectNotifyLogList(NotifyLog notifyLog); + + /** + * 新增通知日志 + * + * @param notifyLog 通知日志 + * @return 结果 + */ + public int insertNotifyLog(NotifyLog notifyLog); + + /** + * 修改通知日志 + * + * @param notifyLog 通知日志 + * @return 结果 + */ + public int updateNotifyLog(NotifyLog notifyLog); + + /** + * 批量删除通知日志 + * + * @param ids 需要删除的通知日志主键集合 + * @return 结果 + */ + public int deleteNotifyLogByIds(Long[] ids); + + /** + * 删除通知日志信息 + * + * @param id 通知日志主键 + * @return 结果 + */ + public int deleteNotifyLogById(Long id); +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/INotifyTemplateService.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/INotifyTemplateService.java new file mode 100644 index 0000000..89262ef --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/INotifyTemplateService.java @@ -0,0 +1,117 @@ +package com.bnhz.notify.service; + +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.notify.NotifyConfigVO; +import com.bnhz.common.enums.NotifyChannelProviderEnum; +import com.bnhz.notify.domain.NotifyTemplate; + +import java.util.List; + +/** + * 通知模版Service接口 + * + * @author kerwincui + * @date 2023-12-01 + */ +public interface INotifyTemplateService +{ + /** + * 查询通知模版 + * + * @param id 通知模版主键 + * @return 通知模版 + */ + public NotifyTemplate selectNotifyTemplateById(Long id); + + /** + * 查询通知模版列表 + * + * @param notifyTemplate 通知模版 + * @return 通知模版集合 + */ + public List selectNotifyTemplateList(NotifyTemplate notifyTemplate); + + /** + * 新增通知模版 + * + * @param notifyTemplate 通知模版 + * @return 结果 + */ + public AjaxResult insertNotifyTemplate(NotifyTemplate notifyTemplate); + + /** + * 修改通知模版 + * + * @param notifyTemplate 通知模版 + * @return 结果 + */ + public AjaxResult updateNotifyTemplate(NotifyTemplate notifyTemplate); + + /** + * 批量删除通知模版 + * + * @param ids 需要删除的通知模版主键集合 + * @return 结果 + */ + public int deleteNotifyTemplateByIds(Long[] ids); + + /** + * 删除通知模版信息 + * + * @param id 通知模版主键 + * @return 结果 + */ + public int deleteNotifyTemplateById(Long id); + + /** + * 查询某一业务通知通道是否有启动的(业务编码唯一启用一个模板) + * @param notifyTemplate 通知模板 + */ + public Integer countNormalTemplate(NotifyTemplate notifyTemplate); + + /** + * 更新某一类型为不可用状态,选中的为可用状态 + * @param notifyTemplate 通知模板 + */ + public void updateTemplateStatus(NotifyTemplate notifyTemplate); + + /** + * @description: 查询启用通知模板 + * @param: serviceCode 业务编码 + * @return: com.bnhz.notify.domain.NotifyTemplate + */ + NotifyTemplate selectOnlyEnable(NotifyTemplate notifyTemplate); + + /** + * @description: 获取消息通知模版参数信息 + * @author fastb + * @date 2023-12-22 11:01 + * @version 1.0 + */ + List getNotifyMsgParams(Long channelId, String msgType); + + /** + * @description: 统一获取模板参数内容变量,调用这个方法 + * @param: channelId + * @return: java.lang.String + */ + List listVariables(String content, NotifyChannelProviderEnum notifyChannelProviderEnum); + + /** + * @description: 获取告警微信小程序模板id + * @param: + * @return: java.lang.String + */ + String getAlertWechatMini(); + + /** + * 获取唯一启用模版查询条件 + * 短信、语音、邮箱以业务编码+渠道保证唯一启用,微信、钉钉以业务编码+渠道+服务商保证唯一启用 + * @param: serviceCode + * @param: channelType + * @param: provider + * @return com.bnhz.notify.domain.NotifyTemplate + */ + NotifyTemplate getEnableQueryCondition(String serviceCode, String channelType, String provider, Long tenantId); + +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/impl/NotifyChannelServiceImpl.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/impl/NotifyChannelServiceImpl.java new file mode 100644 index 0000000..49a7771 --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/impl/NotifyChannelServiceImpl.java @@ -0,0 +1,195 @@ +package com.bnhz.notify.service.impl; + +import com.bnhz.common.core.domain.entity.SysDictData; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.notify.NotifyConfigVO; +import com.bnhz.common.enums.NotifyChannelProviderEnum; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.domain.NotifyTemplate; +import com.bnhz.notify.mapper.NotifyChannelMapper; +import com.bnhz.notify.mapper.NotifyTemplateMapper; +import com.bnhz.notify.service.INotifyChannelService; +import com.bnhz.notify.vo.ChannelProviderVO; +import com.bnhz.system.service.ISysDictDataService; +import org.apache.commons.collections4.CollectionUtils; +import org.dromara.sms4j.core.factory.SmsFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static com.bnhz.common.utils.SecurityUtils.getLoginUser; + +/** + * 通知渠道Service业务层处理 + * + * @author kerwincui + * @date 2023-12-01 + */ +@Service +public class NotifyChannelServiceImpl implements INotifyChannelService +{ + @Resource + private NotifyChannelMapper notifyChannelMapper; + @Resource + private ISysDictDataService sysDictDataService; + @Resource + private NotifyTemplateMapper notifyTemplateMapper; + + /** + * 查询通知渠道 + * + * @param id 通知渠道主键 + * @return 通知渠道 + */ + @Override + public NotifyChannel selectNotifyChannelById(Long id) + { + return notifyChannelMapper.selectNotifyChannelById(id); + } + + /** + * 查询通知渠道列表 + * + * @param notifyChannel 通知渠道 + * @return 通知渠道 + */ + @Override + public List selectNotifyChannelList(NotifyChannel notifyChannel) + { + SysUser user = getLoginUser().getUser(); +// List roles=user.getRoles(); +// // 租户 +// if(roles.stream().anyMatch(a-> "tenant".equals(a.getRoleKey()))){ +// notifyChannel.setTenantId(user.getUserId()); +// } + // 查询所属机构 + if (null != user.getDeptId()) { + notifyChannel.setTenantId(user.getDept().getDeptUserId()); + } else { + notifyChannel.setTenantId(user.getUserId()); + } + return notifyChannelMapper.selectNotifyChannelList(notifyChannel); + } + + /** + * 新增通知渠道 + * + * @param notifyChannel 通知渠道 + * @return 结果 + */ + @Override + public int insertNotifyChannel(NotifyChannel notifyChannel) + { + SysUser user = getLoginUser().getUser(); + if (null == user.getDeptId()) { + throw new ServiceException("只允许租户配置"); + } + notifyChannel.setTenantId(user.getDept().getDeptUserId()); + notifyChannel.setTenantName(user.getDept().getDeptUserName()); + return notifyChannelMapper.insertNotifyChannel(notifyChannel); + } + + /** + * 修改通知渠道 + * + * @param notifyChannel 通知渠道 + * @return 结果 + */ + @Override + public int updateNotifyChannel(NotifyChannel notifyChannel) + { + notifyChannel.setUpdateTime(DateUtils.getNowDate()); + List notifyTemplateList = notifyTemplateMapper.selectNotifyTemplateByChannelId(notifyChannel.getId()); + for (NotifyTemplate notifyTemplate : notifyTemplateList) { + SmsFactory.unregister(notifyTemplate.getId().toString()); + } + return notifyChannelMapper.updateNotifyChannel(notifyChannel); + } + + /** + * 批量删除通知渠道 + * + * @param ids 需要删除的通知渠道主键 + * @return 结果 + */ + @Override + public int deleteNotifyChannelByIds(Long[] ids) + { + int result = notifyChannelMapper.deleteNotifyChannelByIds(ids); + // 删除渠道下的模板 + if (result > 0) { + notifyTemplateMapper.deleteNotifyTemplateByChannelIds(ids); + } + return result; + } + + /** + * 删除通知渠道信息 + * + * @param id 通知渠道主键 + * @return 结果 + */ + @Override + public int deleteNotifyChannelById(Long id) + { + int result = notifyChannelMapper.deleteNotifyChannelById(id); + // 删除渠道下的模板 + if (result > 0) { + notifyTemplateMapper.deleteNotifyTemplateByChannelIds(new Long[]{id}); + } + return result; + } + + @Override + public List listChannel() { + SysDictData sysDictData = new SysDictData(); + sysDictData.setDictType("notify_channel_type"); + sysDictData.setStatus("0"); + List parentDataList = sysDictDataService.selectDictDataList(sysDictData); + if (CollectionUtils.isEmpty(parentDataList)) { + return new ArrayList<>(); + } + List dictValueList = parentDataList.stream().map(SysDictData::getDictValue).collect(Collectors.toList()); + List dictTypeList = new ArrayList<>(); + for (String s : dictValueList) { + dictTypeList.add("notify_channel_" + s + "_provider"); + } + List childerDataList = sysDictDataService.selectDictDataListByDictTypes(dictTypeList); + Map> map = childerDataList.stream().collect(Collectors.groupingBy(SysDictData::getDictType)); + List result = new ArrayList<>(); + for (SysDictData dictData : parentDataList) { + ChannelProviderVO channelProviderVO = new ChannelProviderVO(); + channelProviderVO.setChannelType(dictData.getDictValue()); + channelProviderVO.setChannelName(dictData.getDictLabel()); + String key = "notify_channel_" + dictData.getDictValue() + "_provider"; + if (!map.containsKey(key)) { + result.add(channelProviderVO); + continue; + } + List dataList = map.get(key); + List providerList = new ArrayList<>(); + for (SysDictData data : dataList) { + ChannelProviderVO.Provider provider = new ChannelProviderVO.Provider(); + provider.setProvider(data.getDictValue()); + provider.setProviderName(data.getDictLabel()); + provider.setCategory(dictData.getDictValue()); + providerList.add(provider); + } + channelProviderVO.setProviderList(providerList); + result.add(channelProviderVO); + } + return result; + } + + @Override + public List getConfigContent(String channelType, String provider) { + return NotifyChannelProviderEnum.getConfigContent(Objects.requireNonNull(NotifyChannelProviderEnum.getByChannelTypeAndProvider(channelType, provider))); + } +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/impl/NotifyLogServiceImpl.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/impl/NotifyLogServiceImpl.java new file mode 100644 index 0000000..06694d5 --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/impl/NotifyLogServiceImpl.java @@ -0,0 +1,142 @@ +package com.bnhz.notify.service.impl; + +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.domain.NotifyLog; +import com.bnhz.notify.domain.NotifyTemplate; +import com.bnhz.notify.mapper.NotifyChannelMapper; +import com.bnhz.notify.mapper.NotifyLogMapper; +import com.bnhz.notify.mapper.NotifyTemplateMapper; +import com.bnhz.notify.service.INotifyLogService; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.bnhz.common.utils.SecurityUtils.getLoginUser; + +/** + * 通知日志Service业务层处理 + * + * @author bnhz + * @date 2023-12-16 + */ +@Service +public class NotifyLogServiceImpl implements INotifyLogService +{ + @Resource + private NotifyLogMapper notifyLogMapper; + @Resource + private NotifyChannelMapper notifyChannelMapper; + @Resource + private NotifyTemplateMapper notifyTemplateMapper; + + /** + * 查询通知日志 + * + * @param id 通知日志主键 + * @return 通知日志 + */ + @Override + public NotifyLog selectNotifyLogById(Long id) + { + return notifyLogMapper.selectNotifyLogById(id); + } + + /** + * 查询通知日志列表 + * + * @param notifyLog 通知日志 + * @return 通知日志 + */ + @Override + public List selectNotifyLogList(NotifyLog notifyLog) + { + SysUser user = getLoginUser().getUser(); +// List roles=user.getRoles(); +// // 租户 +// if(roles.stream().anyMatch(a->a.getRoleKey().equals("tenant"))){ +// notifyLog.setTenantId(user.getUserId()); +// } + // 查询所属机构 + if (null != user.getDeptId()) { + notifyLog.setTenantId(user.getDept().getDeptUserId()); + } else { + notifyLog.setTenantId(user.getUserId()); + } + List notifyLogs = notifyLogMapper.selectNotifyLogList(notifyLog); + if (CollectionUtils.isEmpty(notifyLogs)) { + return notifyLogs; + } + List channelIdList = notifyLogs.stream().map(NotifyLog::getChannelId).collect(Collectors.toList()); + List notifyChannelList = notifyChannelMapper.selectNotifyChannelByIds(channelIdList); + Map notifyChannelMap = notifyChannelList.stream().collect(Collectors.toMap(NotifyChannel::getId, Function.identity())); + List templateIdList = notifyLogs.stream().map(NotifyLog::getNotifyTemplateId).collect(Collectors.toList()); + List notifyTemplateList = notifyTemplateMapper.selectNotifyTemplateByIds(templateIdList); + Map notifyTemplateMap = notifyTemplateList.stream().collect(Collectors.toMap(NotifyTemplate::getId, Function.identity())); + for (NotifyLog log : notifyLogs) { + if (notifyChannelMap.containsKey(log.getChannelId())) { + log.setChannelName(notifyChannelMap.get(log.getChannelId()).getName()); + } + if (notifyTemplateMap.containsKey(log.getNotifyTemplateId())) { + log.setTemplateName(notifyTemplateMap.get(log.getNotifyTemplateId()).getName()); + } + } + return notifyLogs; + } + + /** + * 新增通知日志 + * + * @param notifyLog 通知日志 + * @return 结果 + */ + @Override + public int insertNotifyLog(NotifyLog notifyLog) + { + notifyLog.setCreateTime(DateUtils.getNowDate()); + return notifyLogMapper.insertNotifyLog(notifyLog); + } + + /** + * 修改通知日志 + * + * @param notifyLog 通知日志 + * @return 结果 + */ + @Override + public int updateNotifyLog(NotifyLog notifyLog) + { + notifyLog.setUpdateTime(DateUtils.getNowDate()); + return notifyLogMapper.updateNotifyLog(notifyLog); + } + + /** + * 批量删除通知日志 + * + * @param ids 需要删除的通知日志主键 + * @return 结果 + */ + @Override + public int deleteNotifyLogByIds(Long[] ids) + { + return notifyLogMapper.deleteNotifyLogByIds(ids); + } + + /** + * 删除通知日志信息 + * + * @param id 通知日志主键 + * @return 结果 + */ + @Override + public int deleteNotifyLogById(Long id) + { + return notifyLogMapper.deleteNotifyLogById(id); + } +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/impl/NotifyTemplateServiceImpl.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/impl/NotifyTemplateServiceImpl.java new file mode 100644 index 0000000..d0b2f3f --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/service/impl/NotifyTemplateServiceImpl.java @@ -0,0 +1,287 @@ +package com.bnhz.notify.service.impl; + +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.core.notify.NotifyConfigVO; +import com.bnhz.common.enums.NotifyChannelEnum; +import com.bnhz.common.enums.NotifyChannelProviderEnum; +import com.bnhz.common.enums.NotifyServiceCodeEnum; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.domain.NotifyTemplate; +import com.bnhz.notify.mapper.NotifyChannelMapper; +import com.bnhz.notify.mapper.NotifyTemplateMapper; +import com.bnhz.notify.service.INotifyTemplateService; +import lombok.extern.slf4j.Slf4j; +import org.dromara.sms4j.core.factory.SmsFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.bnhz.common.utils.SecurityUtils.getLoginUser; + +/** + * 通知模版Service业务层处理 + * + * @author kerwincui + * @date 2023-12-01 + */ +@Service +@Slf4j +public class NotifyTemplateServiceImpl implements INotifyTemplateService { + @Resource + private NotifyTemplateMapper notifyTemplateMapper; + @Resource + private NotifyChannelMapper notifyChannelMapper; + + + /** + * 查询通知模版 + * + * @param id 通知模版主键 + * @return 通知模版 + */ + @Override + public NotifyTemplate selectNotifyTemplateById(Long id) { + return notifyTemplateMapper.selectNotifyTemplateById(id); + } + + /** + * 查询通知模版列表 + * + * @param notifyTemplate 通知模版 + * @return 通知模版 + */ + @Override + public List selectNotifyTemplateList(NotifyTemplate notifyTemplate) { + SysUser user = getLoginUser().getUser(); +// List roles=user.getRoles(); +// // 租户 +// if(roles.stream().anyMatch(a-> "tenant".equals(a.getRoleKey()))){ +// notifyTemplate.setTenantId(user.getUserId()); +// } + // 查询所属机构 + if (null != user.getDeptId()) { + notifyTemplate.setTenantId(user.getDept().getDeptUserId()); + } else { + notifyTemplate.setTenantId(user.getUserId()); + } + List notifyTemplates = notifyTemplateMapper.selectNotifyTemplateList(notifyTemplate); + if (org.apache.commons.collections4.CollectionUtils.isEmpty(notifyTemplates)) { + return notifyTemplates; + } + List collect = notifyTemplates.stream().map(NotifyTemplate::getChannelId).collect(Collectors.toList()); + List notifyChannelList = notifyChannelMapper.selectNotifyChannelByIds(collect); + Map notifyChannelMap = notifyChannelList.stream().collect(Collectors.toMap(NotifyChannel::getId, Function.identity())); + for (NotifyTemplate template : notifyTemplates) { + if (notifyChannelMap.containsKey(template.getChannelId())) { + NotifyChannel notifyChannel = notifyChannelMap.get(template.getChannelId()); + template.setChannelName(notifyChannel.getName()); + } + } + return notifyTemplates; + } + + /** + * 新增通知模版 + * + * @param notifyTemplate 通知模版 + * @return 结果 + */ + @Override + public AjaxResult insertNotifyTemplate(NotifyTemplate notifyTemplate) { + SysUser user = getLoginUser().getUser(); + if (null == user.getDeptId()) { + throw new ServiceException("只允许租户配置"); + } + notifyTemplate.setTenantId(user.getDept().getDeptUserId()); + notifyTemplate.setTenantName(user.getDept().getDeptUserName()); + notifyTemplate.setCreateTime(DateUtils.getNowDate()); + return notifyTemplateMapper.insertNotifyTemplate(notifyTemplate) > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 修改通知模版 + * + * @param notifyTemplate 通知模版 + * @return 结果 + */ + @Override + public AjaxResult updateNotifyTemplate(NotifyTemplate notifyTemplate) { + notifyTemplate.setUpdateTime(DateUtils.getNowDate()); + if (NotifyChannelEnum.SMS.getType().equals(notifyTemplate.getChannelType())) { + SmsFactory.unregister(notifyTemplate.getId().toString()); + } + return notifyTemplateMapper.updateNotifyTemplate(notifyTemplate) > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 批量删除通知模版 + * + * @param ids 需要删除的通知模版主键 + * @return 结果 + */ + @Override + public int deleteNotifyTemplateByIds(Long[] ids) { + int i = notifyTemplateMapper.deleteNotifyTemplateByIds(ids); + if (i > 0) { + notifyTemplateMapper.deleteAlertNotifyTemplateByNotifyTemplateIds(ids); + } + return i; + } + + /** + * 删除通知模版信息 + * + * @param id 通知模版主键 + * @return 结果 + */ + @Override + public int deleteNotifyTemplateById(Long id) { + int i = notifyTemplateMapper.deleteNotifyTemplateById(id); + if (i > 0) { + notifyTemplateMapper.deleteAlertNotifyTemplateByNotifyTemplateIds(new Long[]{id}); + } + return i; + } + + /** + * 查询某一业务通知通道是否有启动的(业务编码唯一启用一个模板) + * @param notifyTemplate 通知模板 + */ + @Override + public Integer countNormalTemplate(NotifyTemplate notifyTemplate){ + LoginUser loginUser = getLoginUser(); + assert !Objects.isNull(notifyTemplate.getServiceCode()) : "业务编码不能为空"; + NotifyTemplate selectOne = this.getEnableQueryCondition(notifyTemplate.getServiceCode(), notifyTemplate.getChannelType(), notifyTemplate.getProvider(), loginUser.getUser().getDept().getDeptUserId()); + selectOne.setId(notifyTemplate.getId()); + return notifyTemplateMapper.selectEnableNotifyTemplateCount(selectOne); + } + + /** + * 获取唯一启用模版查询条件 + * 唯一启用条件:同一业务编码的模板短信、语音、邮箱渠道分别可以启用一个,微信、钉钉渠道下不同服务商分别可以启用一个 + * @param: serviceCode + * @param: channelType + * @param: provider + * @return com.bnhz.notify.domain.NotifyTemplate + */ + @Override + public NotifyTemplate getEnableQueryCondition(String serviceCode, String channelType, String provider, Long tenantId) { + NotifyTemplate notifyTemplate = new NotifyTemplate(); + notifyTemplate.setServiceCode(serviceCode); + notifyTemplate.setStatus(1); + notifyTemplate.setTenantId(tenantId); + NotifyChannelEnum notifyChannelEnum = NotifyChannelEnum.getNotifyChannelEnum(channelType); + switch (Objects.requireNonNull(notifyChannelEnum)) { + case SMS: + case VOICE: + case EMAIL: + notifyTemplate.setChannelType(channelType); + break; + case WECHAT: + case DING_TALK: + notifyTemplate.setChannelType(channelType); + notifyTemplate.setProvider(provider); + break; + default: + break; + } + return notifyTemplate; + } + + /** + * 更新某一类型为不可用状态,选中的为可用状态 + * @param notifyTemplate 通知模板 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateTemplateStatus(NotifyTemplate notifyTemplate){ + LoginUser loginUser = getLoginUser(); + // 查询所有统一类型可用的渠道 + NotifyTemplate selectEnable = this.getEnableQueryCondition(notifyTemplate.getServiceCode(), notifyTemplate.getChannelType(), notifyTemplate.getProvider(), loginUser.getUser().getDept().getDeptUserId()); + selectEnable.setId(notifyTemplate.getId()); + List notifyTemplateList = this.selectNotifyTemplateList(selectEnable); + if (!CollectionUtils.isEmpty(notifyTemplateList)){ + //如果有同一类型的渠道为可用,要先将更新为不可用 + List ids = notifyTemplateList.stream().map(NotifyTemplate::getId).filter(id -> !Objects.equals(id, notifyTemplate.getId())).collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(ids)) { + notifyTemplateMapper.updateNotifyBatch(ids, 0); + } + } + //更新选中的为可用状态 + NotifyTemplate updateBo = new NotifyTemplate(); + updateBo.setStatus(1); + updateBo.setId(notifyTemplate.getId()); + notifyTemplateMapper.updateNotifyTemplate(updateBo); + } + + @Override + public NotifyTemplate selectOnlyEnable(NotifyTemplate notifyTemplate) { + return notifyTemplateMapper.selectOnlyEnable(notifyTemplate); + } + + @Override + public List getNotifyMsgParams(Long channelId, String msgType) { + NotifyChannel notifyChannel = notifyChannelMapper.selectNotifyChannelById(channelId); + if (Objects.isNull(notifyChannel)) { + return new ArrayList<>(); + } + NotifyChannelProviderEnum notifyChannelProviderEnum = NotifyChannelProviderEnum.getByChannelTypeAndProvider(notifyChannel.getChannelType(), notifyChannel.getProvider()); + return NotifyChannelProviderEnum.getMsgParams(notifyChannelProviderEnum, msgType); + } + + @Override + public List listVariables(String content, NotifyChannelProviderEnum notifyChannelProviderEnum) { + List variables; + switch (Objects.requireNonNull(notifyChannelProviderEnum)) { + case WECHAT_MINI_PROGRAM: + case WECHAT_PUBLIC_ACCOUNT: + variables = StringUtils.getWeChatMiniVariables(content); + break; + case SMS_TENCENT: + case VOICE_TENCENT: + variables = StringUtils.getVariables("{}", content); + break; + case EMAIL_QQ: + case EMAIL_163: + variables = StringUtils.getVariables("#{}", content); + break; + default: + variables = StringUtils.getVariables("${}", content); + break; + } + return variables; + } + + @Override + public String getAlertWechatMini() { + NotifyTemplate selectOne = new NotifyTemplate(); + selectOne.setServiceCode(NotifyServiceCodeEnum.ALERT.getServiceCode()).setChannelType(NotifyChannelProviderEnum.WECHAT_MINI_PROGRAM.getChannelType()).setProvider(NotifyChannelProviderEnum.WECHAT_MINI_PROGRAM.getProvider()).setStatus(1); + SysUser user = getLoginUser().getUser(); + if (null != user.getDeptId()) { + selectOne.setTenantId(user.getDept().getDeptUserId()); + } else { + selectOne.setTenantId(1L); + } + NotifyTemplate notifyTemplate = notifyTemplateMapper.selectOnlyEnable(selectOne); + if (notifyTemplate == null || StringUtils.isEmpty(notifyTemplate.getMsgParams())) { + return ""; + } + JSONObject jsonObject = JSONObject.parseObject(notifyTemplate.getMsgParams()); + return jsonObject.get("templateId").toString(); + } + +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/vo/ChannelProviderVO.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/vo/ChannelProviderVO.java new file mode 100644 index 0000000..dd38f80 --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/vo/ChannelProviderVO.java @@ -0,0 +1,54 @@ +package com.bnhz.notify.vo; + +import lombok.Data; + +import java.util.List; + +/** + * 渠道服务商VO类 + * @author fastb + * @date 2023-12-01 14:06 + */ +@Data +public class ChannelProviderVO { + + /** + * 渠道类型 + */ + private String channelType; + + /** + * 渠道名称 + */ + private String channelName; + + /** + * 服务商集合 + */ + private List providerList; + + /** + * 服务商 + */ + @Data + public static class Provider{ + + /** + * 服务商英文标识 + */ + private String provider; + + /** + * 服务商名称 + */ + private String providerName; + + /** + * 所属渠道标识 + */ + private String category; + + } +} + + diff --git a/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/vo/NotifyVO.java b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/vo/NotifyVO.java new file mode 100644 index 0000000..54e2c65 --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/java/com/bnhz/notify/vo/NotifyVO.java @@ -0,0 +1,33 @@ +package com.bnhz.notify.vo; + +import com.bnhz.common.enums.NotifyChannelProviderEnum; +import com.bnhz.notify.domain.NotifyChannel; +import com.bnhz.notify.domain.NotifyTemplate; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.LinkedHashMap; + +/** + * @author fastb + * @version 1.0 + * @description: 通知发送参数 + * @date 2024-01-02 11:10 + */ +@Data +@Accessors(chain = true) +public class NotifyVO { + + private NotifyChannel notifyChannel; + + private NotifyTemplate notifyTemplate; + + /** + * 多个账号用英文逗号隔开 例如:21,51 + */ + private String sendAccount; + + private LinkedHashMap map; + + private NotifyChannelProviderEnum notifyChannelProviderEnum; +} diff --git a/bnhz-notify/bnhz-notify-web/src/main/resources/mapper/NotifyChannelMapper.xml b/bnhz-notify/bnhz-notify-web/src/main/resources/mapper/NotifyChannelMapper.xml new file mode 100644 index 0000000..d6cae73 --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/resources/mapper/NotifyChannelMapper.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + select id, name, channel_type, provider, config_content, tenant_id, tenant_name, create_by, create_time, update_by, update_time, del_flag from notify_channel + + + + + + + + + + insert into notify_channel + + name, + channel_type, + provider, + config_content, + tenant_id, + tenant_name, + create_by, + create_time, + update_by, + update_time, + del_flag, + + + #{name}, + #{channelType}, + #{provider}, + #{configContent}, + #{tenantId}, + #{tenantName}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{delFlag}, + + + + + update notify_channel + + name = #{name}, + channel_type = #{channelType}, + provider = #{provider}, + config_content = #{configContent}, + tenant_id = #{tenantId}, + tenant_name = #{tenantName}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + del_flag = #{delFlag}, + + where id = #{id} + + + + delete from notify_channel where id = #{id} + + + + delete from notify_channel where id in + + #{id} + + + diff --git a/bnhz-notify/bnhz-notify-web/src/main/resources/mapper/NotifyLogMapper.xml b/bnhz-notify/bnhz-notify-web/src/main/resources/mapper/NotifyLogMapper.xml new file mode 100644 index 0000000..1ea2d36 --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/resources/mapper/NotifyLogMapper.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + select id, notify_template_id, channel_id, msg_content, send_account, send_status, result_content,service_code, create_by, create_time, update_by, update_time, del_flag, tenant_id, tenant_name from notify_log + + + + + + + + insert into notify_log + + notify_template_id, + channel_id, + msg_content, + send_account, + send_status, + result_content, + create_by, + create_time, + update_by, + update_time, + del_flag, + tenant_id, + tenant_name, + service_code, + + + #{notifyTemplateId}, + #{channelId}, + #{msgContent}, + #{sendAccount}, + #{sendStatus}, + #{resultContent}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{delFlag}, + #{tenantId}, + #{tenantName}, + #{serviceCode}, + + + + + update notify_log + + notify_template_id = #{notifyTemplateId}, + channel_id = #{channelId}, + msg_content = #{msgContent}, + send_account = #{sendAccount}, + send_status = #{sendStatus}, + result_content = #{resultContent}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + del_flag = #{delFlag}, + tenant_id = #{tenantId}, + tenant_name = #{tenantName}, + service_code = #{serviceCode}, + + where id = #{id} + + + + delete from notify_log where id = #{id} + + + + delete from notify_log where id in + + #{id} + + + diff --git a/bnhz-notify/bnhz-notify-web/src/main/resources/mapper/NotifyTemplateMapper.xml b/bnhz-notify/bnhz-notify-web/src/main/resources/mapper/NotifyTemplateMapper.xml new file mode 100644 index 0000000..9e1340d --- /dev/null +++ b/bnhz-notify/bnhz-notify-web/src/main/resources/mapper/NotifyTemplateMapper.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + select id, service_code,msg_params,status, name, channel_id, channel_type, provider, create_by, create_time, update_by, update_time, del_flag, tenant_id, tenant_name from notify_template + + + + + + + + + + + + + + + + insert into notify_template + + service_code, + name, + channel_id, + channel_type, + provider, + create_by, + create_time, + update_by, + update_time, + del_flag, + msg_params, + status, + tenant_id, + tenant_name, + + + #{serviceCode}, + #{name}, + #{channelId}, + #{channelType}, + #{provider}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{delFlag}, + #{msgParams}, + #{status}, + #{tenantId}, + #{tenantName}, + + + + + update notify_template + + service_code = #{serviceCode}, + name = #{name}, + channel_id = #{channelId}, + channel_type = #{channelType}, + provider = #{provider}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + del_flag = #{delFlag}, + msg_params = #{msgParams}, + status = #{status}, + tenant_id = #{tenantId}, + tenant_name = #{tenantName}, + + where id = #{id} + + + + update notify_template + + status = #{status}, + + where id in + + #{item} + + + + + delete from notify_template where id = #{id} + + + + delete from notify_template where id in + + #{id} + + + + + delete from notify_template where channel_id in + + #{channelId} + + + + + delete from iot_alert_notify_template where notify_template_id in + + #{notifyTemplateId} + + + diff --git a/bnhz-notify/pom.xml b/bnhz-notify/pom.xml new file mode 100644 index 0000000..be6c166 --- /dev/null +++ b/bnhz-notify/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + com.bnhz + daqi-back + 3.8.5 + + + pom + bnhz-notify + 通知模块整合 + + + 8 + 8 + UTF-8 + + + + bnhz-notify-web + bnhz-notify-core + + + diff --git a/bnhz-open-api/pom.xml b/bnhz-open-api/pom.xml new file mode 100644 index 0000000..c8ee8dd --- /dev/null +++ b/bnhz-open-api/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + daqi-back + com.bnhz + 3.8.5 + + + + bnhz-open-api + controller层接口 + + + + com.bnhz + mqtt-broker + + + + com.bnhz + sip-server + + + + + + + + diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/config/DelayUpgradeQueue.java b/bnhz-open-api/src/main/java/com/bnhz/data/config/DelayUpgradeQueue.java new file mode 100644 index 0000000..040e755 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/config/DelayUpgradeQueue.java @@ -0,0 +1,38 @@ +package com.bnhz.data.config; + +import com.bnhz.common.core.mq.ota.OtaUpgradeDelayTask; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.DelayQueue; + +/** + * OTA延迟升级队列 + * + * @author gsb + * @date 2022/10/26 10:51 + */ +@Slf4j +public class DelayUpgradeQueue { + + /** + * 使用springboot的 DelayQueue实现延迟队列(OTA对单个设备延迟升级,提高升级容错率) + */ + private static DelayQueue queue = new DelayQueue<>(); + + public static void offerTask(OtaUpgradeDelayTask task) { + try { + queue.offer(task); + } catch (Exception e) { + log.error("OTA任务推送异常", e); + } + } + + public static OtaUpgradeDelayTask task() { + try { + return queue.take(); + } catch (Exception exception) { + log.error("=>OTA任务获取异常"); + return null; + } + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/config/TaskConfig.java b/bnhz-open-api/src/main/java/com/bnhz/data/config/TaskConfig.java new file mode 100644 index 0000000..8a9b94c --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/config/TaskConfig.java @@ -0,0 +1,8 @@ +package com.bnhz.data.config; + +/** + * @author gsb + * @date 2022/10/26 11:13 + */ +public class TaskConfig { +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/config/UpGradeListener.java b/bnhz-open-api/src/main/java/com/bnhz/data/config/UpGradeListener.java new file mode 100644 index 0000000..c61f885 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/config/UpGradeListener.java @@ -0,0 +1,93 @@ +package com.bnhz.data.config; + + +import com.bnhz.common.core.domain.entity.SysMenu; +import com.bnhz.common.core.mq.ota.OtaUpgradeDelayTask; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.data.service.IOtaUpgradeService; +import com.bnhz.system.service.ISysMenuService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Date; + +/** + * OTA升级监听 + * @author bill + */ +@Slf4j +@Component +@Order(3) +public class UpGradeListener implements ApplicationRunner { + + @Autowired + private IOtaUpgradeService otaUpgradeService; + @Resource + private ISysMenuService menuService; + + @Resource + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + /** + * true: 使用netty搭建的mqttBroker false: 使用emq + */ + @Value("${server.broker.enabled}") + private Boolean enabled; + + + + + //@Async(bnhzConstant.TASK.DELAY_UPGRADE_TASK) + public void listen(){ + while (true){ + try { + OtaUpgradeDelayTask task = DelayUpgradeQueue.task(); + if (StringUtils.isNotNull(task)){ + Date startTime = task.getStartTime(); + otaUpgradeService.upgrade(task); + log.info("=>开始OTA升级时间{}",startTime); + } + continue; + }catch (Exception e){ + log.error("=>OTA升级监听异常",e); + } + } + } + + + @Override + public void run(ApplicationArguments args) throws Exception { + log.info("=>OTA监听队列启动成功"); +// updateMenu(); + + threadPoolTaskExecutor.execute(this::listen); + log.info("=>OTA监听队列启动结束"); + } + + /** + * 切换菜单中Emqx控制台和netty管理 + */ + public void updateMenu(){ + SysMenu sysMenu = new SysMenu(); + if (enabled){ + sysMenu.setMenuId(2104L); + sysMenu.setVisible("1"); + menuService.updateMenu(sysMenu); + sysMenu.setMenuId(3031L); + }else { + sysMenu.setMenuId(3031L); + sysMenu.setVisible("1"); + menuService.updateMenu(sysMenu); + sysMenu.setMenuId(2104L); + } + sysMenu.setVisible("0"); + menuService.updateMenu(sysMenu); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/AlertController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/AlertController.java new file mode 100644 index 0000000..46c90e9 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/AlertController.java @@ -0,0 +1,152 @@ +package com.bnhz.data.controller; + +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.iot.domain.Alert; +import com.bnhz.iot.domain.Scene; +import com.bnhz.iot.service.IAlertService; +import com.bnhz.notify.domain.NotifyTemplate; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 设备告警Controller + * + * @author kerwincui + * @date 2022-01-13 + */ +@Api(tags = "设备告警alert模块") +@RestController +@RequestMapping("/iot/alert") +public class AlertController extends BaseController +{ + @Autowired + private IAlertService alertService; + + /** + * 查询设备告警列表 + */ + @ApiOperation("查询设备告警列表") + @PreAuthorize("@ss.hasPermi('iot:alert:list')") + @GetMapping("/list") + public TableDataInfo list(Alert alert) + { + startPage(); + List list = alertService.selectAlertList(alert); + return getDataTable(list); + } + + /** + * 查询设备告警列表 + */ + @ApiOperation("查询设备告警列表") + @GetMapping("/getScenesByAlertId/{alertId}") + public TableDataInfo getScenesByAlertId(@PathVariable("alertId") Long alertId) + { + List list = alertService.selectScenesByAlertId(alertId); + return getDataTable(list); + } + + /** + * 查询告警通知模版列表 + */ + @ApiOperation("查询告警通知模版列表") + @GetMapping("/listNotifyTemplate/{alertId}") + public TableDataInfo listNotifyTemplate(@PathVariable("alertId") Long alertId) + { + List list = alertService.listNotifyTemplateByAlertId(alertId); + return getDataTable(list); + } + + /** + * 导出设备告警列表 + */ + @ApiOperation("导出设备告警列表") + @PreAuthorize("@ss.hasPermi('iot:alert:export')") + @Log(title = "设备告警", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, Alert alert) + { + List list = alertService.selectAlertList(alert); + ExcelUtil util = new ExcelUtil(Alert.class); + util.exportExcel(response, list, "设备告警数据"); + } + + /** + * 获取设备告警详细信息 + */ + @ApiOperation("获取设备告警详细信息") + @PreAuthorize("@ss.hasPermi('iot:alert:query')") + @GetMapping(value = "/{alertId}") + public AjaxResult getInfo(@PathVariable("alertId") Long alertId) + { + return AjaxResult.success(alertService.selectAlertByAlertId(alertId)); + } + + /** + * 新增设备告警 + */ + @ApiOperation("新增设备告警") + @PreAuthorize("@ss.hasPermi('iot:alert:add')") + @Log(title = "设备告警", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody Alert alert) + { + // 查询所属机构 + SysUser user = getLoginUser().getUser(); + if (null != user.getDeptId()) { + alert.setTenantId(user.getDept().getDeptUserId()); + alert.setTenantName(user.getDept().getDeptUserName()); + } + return toAjax(alertService.insertAlert(alert)); + } + + /** + * 修改设备告警 + */ + @ApiOperation("修改设备告警") + @PreAuthorize("@ss.hasPermi('iot:alert:edit')") + @Log(title = "设备告警", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody Alert alert) + { + return toAjax(alertService.updateAlert(alert)); + } + + /** + * 删除设备告警 + */ + @ApiOperation("删除设备告警") + @PreAuthorize("@ss.hasPermi('iot:alert:remove')") + @Log(title = "设备告警", businessType = BusinessType.DELETE) + @DeleteMapping("/{alertIds}") + public AjaxResult remove(@PathVariable Long[] alertIds) + { + return toAjax(alertService.deleteAlertByAlertIds(alertIds)); + } + + /** + * 修改设备告警状态 + * @param alertId 告警id + * @param status 状态 + * @return 结果 + */ + @PreAuthorize("@ss.hasPermi('iot:alert:edit')") + @Log(title = "设备告警", businessType = BusinessType.UPDATE) + @PostMapping("/editStatus") + public AjaxResult editStatus(Long alertId, Integer status) + { + return toAjax(alertService.editStatus(alertId, status)); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/AlertLogController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/AlertLogController.java new file mode 100644 index 0000000..514cec2 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/AlertLogController.java @@ -0,0 +1,114 @@ +package com.bnhz.data.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.iot.domain.AlertLog; +import com.bnhz.iot.service.IAlertLogService; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.common.core.page.TableDataInfo; + +/** + * 设备告警Controller + * + * @author kerwincui + * @date 2022-01-13 + */ +@Api(tags = "设备告警alertLog模块") +@RestController +@RequestMapping("/iot/alertLog") +public class AlertLogController extends BaseController +{ + @Autowired + private IAlertLogService alertLogService; + + /** + * 查询设备告警列表 + */ + @ApiOperation("查询设备告警列表") + @PreAuthorize("@ss.hasPermi('iot:alertLog:list')") + @GetMapping("/list") + public TableDataInfo list(AlertLog alertLog) + { + startPage(); + List list = alertLogService.selectAlertLogList(alertLog); + return getDataTable(list); + } + + /** + * 导出设备告警列表 + */ + @ApiOperation("导出设备告警列表") + @PreAuthorize("@ss.hasPermi('iot:alertLog:export')") + @Log(title = "设备告警", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, AlertLog alertLog) + { + List list = alertLogService.selectAlertLogList(alertLog); + ExcelUtil util = new ExcelUtil(AlertLog.class); + util.exportExcel(response, list, "设备告警数据"); + } + + /** + * 获取设备告警详细信息 + */ + @ApiOperation("获取设备告警详细信息") + @PreAuthorize("@ss.hasPermi('iot:alertLog:query')") + @GetMapping(value = "/{alertLogId}") + public AjaxResult getInfo(@PathVariable("alertLogId") Long alertLogId) + { + return AjaxResult.success(alertLogService.selectAlertLogByAlertLogId(alertLogId)); + } + + /** + * 新增设备告警 + */ + @ApiOperation("新增设备告警") + @PreAuthorize("@ss.hasPermi('iot:alertLog:add')") + @Log(title = "设备告警", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody AlertLog alertLog) + { + return toAjax(alertLogService.insertAlertLog(alertLog)); + } + + /** + * 修改设备告警 + */ + @ApiOperation("修改设备告警") + @PreAuthorize("@ss.hasPermi('iot:alertLog:edit')") + @Log(title = "设备告警", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody AlertLog alertLog) + { + return toAjax(alertLogService.updateAlertLog(alertLog)); + } + + /** + * 修改设备告警 + */ + @ApiOperation("修改设备告警") + @PreAuthorize("@ss.hasPermi('iot:alertLog:remove')") + @Log(title = "设备告警", businessType = BusinessType.DELETE) + @DeleteMapping("/{alertLogIds}") + public AjaxResult remove(@PathVariable Long[] alertLogIds) + { + return toAjax(alertLogService.deleteAlertLogByAlertLogIds(alertLogIds)); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/AuthResourceController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/AuthResourceController.java new file mode 100644 index 0000000..27d7fe1 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/AuthResourceController.java @@ -0,0 +1,25 @@ +package com.bnhz.data.controller; + +import com.bnhz.common.core.controller.BaseController; +import org.springframework.web.bind.annotation.*; + +/** + * 设备告警Controller + * + * @author kerwincui + * @date 2022-01-13 + */ +@RestController +@RequestMapping("/oauth/resource") +public class AuthResourceController extends BaseController +{ + /** + * 查询设备告警列表 + */ + @GetMapping("/product") + public String findAll() { + return "查询产品列表成功!"; + } + + +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/CategoryController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/CategoryController.java new file mode 100644 index 0000000..8ad53ce --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/CategoryController.java @@ -0,0 +1,143 @@ +package com.bnhz.data.controller; + +import java.util.List; +import java.util.Objects; +import javax.servlet.http.HttpServletResponse; + +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.utils.SecurityUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.iot.domain.Category; +import com.bnhz.iot.service.ICategoryService; +import com.bnhz.common.utils.poi.ExcelUtil; + +/** + * 产品分类Controller + * + * @author kerwincui + * @date 2021-12-16 + */ +@Api(tags = "产品分类") +@RestController +@RequestMapping("/iot/category") +public class CategoryController extends BaseController +{ + @Autowired + private ICategoryService categoryService; + + /** + * 查询产品分类列表 + */ + @PreAuthorize("@ss.hasPermi('iot:category:list')") + @GetMapping("/list") + @ApiOperation("分类分页列表") + public TableDataInfo list(Category category) + { + Boolean showSenior = category.getShowSenior(); + if (Objects.isNull(showSenior)){ + category.setShowSenior(true); //默认展示上级品类 + } + Long deptUserId = getLoginUser().getUser().getDept().getDeptUserId(); + category.setAdmin(SecurityUtils.isAdmin(deptUserId)); + category.setDeptId(getLoginUser().getDeptId()); + category.setTenantId(deptUserId); + startPage(); + return getDataTable(categoryService.selectCategoryList(category)); + } + + /** + * 查询产品简短分类列表 + */ + @PreAuthorize("@ss.hasPermi('iot:category:list')") + @GetMapping("/shortlist") + @ApiOperation("分类简短列表") + public AjaxResult shortlist(Category category) + { + Boolean showSenior = category.getShowSenior(); + if (Objects.isNull(showSenior)){ + category.setShowSenior(true); //默认展示上级品类 + } + Long deptUserId = getLoginUser().getUser().getDept().getDeptUserId(); + category.setAdmin(SecurityUtils.isAdmin(deptUserId)); + category.setDeptId(getLoginUser().getDeptId()); + category.setTenantId(deptUserId); + startPage(); + return AjaxResult.success(categoryService.selectCategoryShortList()); + } + + /** + * 导出产品分类列表 + */ + @PreAuthorize("@ss.hasPermi('iot:category:export')") + @Log(title = "产品分类", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @ApiOperation("导出分类") + public void export(HttpServletResponse response, Category category) + { + List list = categoryService.selectCategoryList(category); + ExcelUtil util = new ExcelUtil(Category.class); + util.exportExcel(response, list, "产品分类数据"); + } + + /** + * 获取产品分类详细信息 + */ + @ApiOperation("获取分类详情") + @PreAuthorize("@ss.hasPermi('iot:category:query')") + @GetMapping(value = "/{categoryId}") + public AjaxResult getInfo(@PathVariable("categoryId") Long categoryId) + { + return AjaxResult.success(categoryService.selectCategoryByCategoryId(categoryId)); + } + + /** + * 新增产品分类 + */ + @PreAuthorize("@ss.hasPermi('iot:category:add')") + @Log(title = "产品分类", businessType = BusinessType.INSERT) + @PostMapping + @ApiOperation("添加分类") + public AjaxResult add(@RequestBody Category category) + { + return toAjax(categoryService.insertCategory(category)); + } + + /** + * 修改产品分类 + */ + @PreAuthorize("@ss.hasPermi('iot:category:edit')") + @Log(title = "产品分类", businessType = BusinessType.UPDATE) + @PutMapping + @ApiOperation("修改分类") + public AjaxResult edit(@RequestBody Category category) + { + return toAjax(categoryService.updateCategory(category)); + } + + /** + * 删除产品分类 + */ + @PreAuthorize("@ss.hasPermi('iot:category:remove')") + @Log(title = "产品分类", businessType = BusinessType.DELETE) + @DeleteMapping("/{categoryIds}") + @ApiOperation("批量删除分类") + public AjaxResult remove(@PathVariable Long[] categoryIds) + { + return categoryService.deleteCategoryByCategoryIds(categoryIds); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/DataPushTaskController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DataPushTaskController.java new file mode 100644 index 0000000..ff44149 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DataPushTaskController.java @@ -0,0 +1,65 @@ +package com.bnhz.data.controller; + +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.R; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.iot.domain.DataPushTask; +import com.bnhz.iot.model.ThingsModels.ThingsModelQuery; +import com.bnhz.iot.model.dto.DataPushTaskBo; +import com.bnhz.iot.service.IDataPushTaskService; +import com.bnhz.iot.tdengine.service.IColumnModeOperationsService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 数据补传任务Controller + * + * @author Leo + * @date 2024-11-05 + */ +@RequiredArgsConstructor +@RestController +@RequestMapping("/iot/task") +public class DataPushTaskController extends BaseController { + + private final IDataPushTaskService dataPushTaskService; + + + /** + * 新增补传任务 + * + * @param dataPushTaskBo 查询对象 + * @return 无 + */ + @PostMapping + public R addTask(@RequestBody DataPushTaskBo dataPushTaskBo) { + dataPushTaskService.rePushMsg(dataPushTaskBo); + return R.ok(); + } + + /** + * 查询数据补传任务列表 + */ + @GetMapping("/list") + public TableDataInfo list(DataPushTask dataPushTask) { + startPage(); + List list = dataPushTaskService.selectDataPushTaskList(dataPushTask); + return getDataTable(list); + } + + + /** + * 获取数据补传任务详细信息 + */ + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) { + return success(dataPushTaskService.selectDataPushTaskById(id)); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceAlertUserController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceAlertUserController.java new file mode 100644 index 0000000..b52f37b --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceAlertUserController.java @@ -0,0 +1,77 @@ +package com.bnhz.data.controller; + +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.iot.domain.DeviceAlertUser; +import com.bnhz.iot.model.DeviceAlertUserVO; +import com.bnhz.iot.service.IDeviceAlertUserService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 设备告警用户Controller + * + * @author kerwincui + * @date 2024-05-15 + */ +@RestController +@RequestMapping("/iot/deviceAlertUser") +public class DeviceAlertUserController extends BaseController +{ + @Resource + private IDeviceAlertUserService deviceAlertUserService; + + /** + * 查询设备告警用户列表 + */ + @PreAuthorize("@ss.hasPermi('iot:device:alert:user:list')") + @GetMapping("/list") + public TableDataInfo list(DeviceAlertUser deviceAlertUser) + { + startPage(); + List list = deviceAlertUserService.selectDeviceAlertUserList(deviceAlertUser); + return getDataTable(list); + } + + /** + * 获取设备告警用户详细信息 + */ + @PreAuthorize("@ss.hasPermi('iot:device:alert:user:query')") + @GetMapping("/query") + public TableDataInfo getUser(SysUser sysUser) + { + startPage(); + List list = deviceAlertUserService.selectUserList(sysUser); + return getDataTable(list); + } + + /** + * 新增设备告警用户 + */ + @PreAuthorize("@ss.hasPermi('iot:device:alert:user:add')") + @Log(title = "设备告警用户", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody DeviceAlertUserVO deviceAlertUserVO) + { + + return toAjax(deviceAlertUserService.insertDeviceAlertUser(deviceAlertUserVO)); + } + + /** + * 删除设备告警用户 + */ + @PreAuthorize("@ss.hasPermi('iot:device:alert:user:remove')") + @Log(title = "设备告警用户", businessType = BusinessType.DELETE) + @DeleteMapping + public AjaxResult remove(@RequestParam Long deviceId, @RequestParam Long userId) + { + return toAjax(deviceAlertUserService.deleteByDeviceIdAndUserId(deviceId, userId)); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceController.java new file mode 100644 index 0000000..d88266c --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceController.java @@ -0,0 +1,419 @@ +package com.bnhz.data.controller; + +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysRole; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.iot.domain.Device; +import com.bnhz.iot.model.DeviceAssignmentVO; +import com.bnhz.iot.model.DeviceImportVO; +import com.bnhz.iot.model.DeviceRelateUserInput; +import com.bnhz.iot.service.IDeviceService; +import com.bnhz.mq.service.IMqttMessagePublish; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.apache.commons.collections4.CollectionUtils; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Objects; + +/** + * 设备Controller + * + * @author kerwincui + * @date 2021-12-16 + */ +@Api(tags = "设备管理") +@RestController +@RequestMapping("/iot/device") +public class DeviceController extends BaseController +{ + @Autowired + private IDeviceService deviceService; + + // @Lazy + @Autowired + private IMqttMessagePublish messagePublish; + + /** + * 查询设备列表 + */ + @PreAuthorize("@ss.hasPermi('iot:device:list')") + @GetMapping("/list") + @ApiOperation("设备分页列表") + public TableDataInfo list(Device device) + { + startPage(); + // 限制当前用户机构 + if (null == device.getDeptId()) { + device.setDeptId(getLoginUser().getDeptId()); + } + return getDataTable(deviceService.selectDeviceList(device)); + } + + /** + * 查询未分配授权码设备列表 + */ + @PreAuthorize("@ss.hasPermi('iot:device:list')") + @GetMapping("/unAuthlist") + @ApiOperation("设备分页列表") + public TableDataInfo unAuthlist(Device device) + { + startPage(); + if (null == device.getDeptId()) { + device.setDeptId(getLoginUser().getDeptId()); + } + return getDataTable(deviceService.selectUnAuthDeviceList(device)); + } + + /** + * 查询分组可添加设备 + */ + @PreAuthorize("@ss.hasPermi('iot:device:list')") + @GetMapping("/listByGroup") + @ApiOperation("查询分组可添加设备分页列表") + public TableDataInfo listByGroup(Device device) + { + startPage(); + LoginUser loginUser = getLoginUser(); + if (null == loginUser.getDeptId()) { + device.setTenantId(loginUser.getUserId()); + return getDataTable(deviceService.listTerminalUserByGroup(device)); + } + if (null == device.getDeptId()) { + device.setDeptId(getLoginUser().getDeptId()); + } + return getDataTable(deviceService.selectDeviceListByGroup(device)); + } + + /** + * 查询设备简短列表,主页列表数据 + */ + @PreAuthorize("@ss.hasPermi('iot:device:list')") + @GetMapping("/shortList") + @ApiOperation("设备分页简短列表") + public TableDataInfo shortList(Device device) + { + startPage(); + LoginUser loginUser = getLoginUser(); + if (null == loginUser.getDeptId()) { + // 终端用户查询设备 + device.setTenantId(loginUser.getUserId()); + return getDataTable(deviceService.listTerminalUser(device)); + } + if (null == device.getDeptId()) { + device.setDeptId(getLoginUser().getDeptId()); + } + if (Objects.isNull(device.getTenantId())){ + device.setTenantId(getLoginUser().getUserId()); + } + if (null == device.getShowChild()) { + device.setShowChild(false); + } + return getDataTable(deviceService.selectDeviceShortList(device)); + } + + /** + * 查询所有设备简短列表 + */ + @PreAuthorize("@ss.hasPermi('iot:device:list')") + @GetMapping("/all") + @ApiOperation("查询所有设备简短列表") + public TableDataInfo allShortList() + { + Device device = new Device(); + if (null == device.getDeptId()) { + device.setDeptId(getLoginUser().getDeptId()); + } + return getDataTable(deviceService.selectAllDeviceShortList(device)); + } + + /** + * 导出设备列表 + */ + @PreAuthorize("@ss.hasPermi('iot:device:export')") + @Log(title = "设备", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @ApiOperation("导出设备") + public void export(HttpServletResponse response, Device device) + { + List list = deviceService.selectDeviceList(device); + ExcelUtil util = new ExcelUtil(Device.class); + util.exportExcel(response, list, "设备数据"); + } + + /** + * 获取设备详细信息 + */ + @PreAuthorize("@ss.hasPermi('iot:device:query')") + @GetMapping(value = "/{deviceId}") + @ApiOperation("获取设备详情") + public AjaxResult getInfo(@PathVariable("deviceId") Long deviceId) + { + Device device = deviceService.selectDeviceByDeviceId(deviceId); + // 判断当前用户是否有设备分享权限 (设备所属机构管理员和设备所属用户有权限) + LoginUser loginUser = getLoginUser(); + List roles = loginUser.getUser().getRoles(); + //判断当前用户是否为设备所属机构管理员 + if(roles.stream().anyMatch(a-> "admin".equals(a.getRoleKey()))){ + device.setIsOwner(1); + } else { + //判断当前用户是否是设备所属用户 + if (Objects.equals(device.getTenantId(), loginUser.getUserId())){ + device.setIsOwner(1); + }else { + device.setIsOwner(0); + } + } + return AjaxResult.success(device); + } + + /** + * 设备数据同步 + */ + @PreAuthorize("@ss.hasPermi('iot:device:query')") + @GetMapping(value = "/synchronization/{serialNumber}") + @ApiOperation("设备数据同步") + public AjaxResult deviceSynchronization(@PathVariable("serialNumber") String serialNumber) + { + return AjaxResult.success(messagePublish.deviceSynchronization(serialNumber)); + } + + /** + * 根据设备编号详细信息 + */ + @PreAuthorize("@ss.hasPermi('iot:device:query')") + @GetMapping(value = "/getDeviceBySerialNumber/{serialNumber}") + @ApiOperation("根据设备编号获取设备详情") + public AjaxResult getInfoBySerialNumber(@PathVariable("serialNumber") String serialNumber) + { + return AjaxResult.success(deviceService.selectDeviceBySerialNumber(serialNumber)); + } + + /** + * 获取设备统计信息 + */ + @PreAuthorize("@ss.hasPermi('iot:device:query')") + @GetMapping(value = "/statistic") + @ApiOperation("获取设备统计信息") + public AjaxResult getDeviceStatistic() + { + return AjaxResult.success(deviceService.selectDeviceStatistic()); + } + + /** + * 获取设备详细信息 + */ + @PreAuthorize("@ss.hasPermi('iot:device:query')") + @GetMapping(value = "/runningStatus") + @ApiOperation("获取设备详情和运行状态") + public AjaxResult getRunningStatusInfo(Long deviceId, Integer slaveId) + { + return AjaxResult.success(deviceService.selectDeviceRunningStatusByDeviceId(deviceId,slaveId)); + } + + /** + * 新增设备 + */ + @PreAuthorize("@ss.hasPermi('iot:device:add')") + @Log(title = "添加设备", businessType = BusinessType.INSERT) + @PostMapping + @ApiOperation("添加设备") + public AjaxResult add(@RequestBody Device device) + { + return AjaxResult.success(deviceService.insertDevice(device)); + } + + /** + * TODO --APP + * 终端用户绑定设备 + */ + @PreAuthorize("@ss.hasPermi('iot:device:add')") + @Log(title = "设备关联用户", businessType = BusinessType.UPDATE) + @PostMapping("/relateUser") + @ApiOperation("终端-设备关联用户") + public AjaxResult relateUser(@RequestBody DeviceRelateUserInput deviceRelateUserInput) + { + if(deviceRelateUserInput.getUserId()==0 || deviceRelateUserInput.getUserId()==null){ + return AjaxResult.error("用户ID不能为空"); + } + if(deviceRelateUserInput.getDeviceNumberAndProductIds()==null || deviceRelateUserInput.getDeviceNumberAndProductIds().size()==0){ + return AjaxResult.error("设备编号和产品ID不能为空"); + } + return deviceService.deviceRelateUser(deviceRelateUserInput); + } + + /** + * 修改设备 + */ + @PreAuthorize("@ss.hasPermi('iot:device:edit')") + @Log(title = "修改设备", businessType = BusinessType.UPDATE) + @PutMapping + @ApiOperation("修改设备") + public AjaxResult edit(@RequestBody Device device) + { + return deviceService.updateDevice(device); + } + + /** + * 重置设备状态 + */ + @PreAuthorize("@ss.hasPermi('iot:device:edit')") + @Log(title = "重置设备状态", businessType = BusinessType.UPDATE) + @PutMapping("/reset/{serialNumber}") + @ApiOperation("重置设备状态") + public AjaxResult resetDeviceStatus(@PathVariable String serialNumber) + { + Device device=new Device(); + device.setSerialNumber(serialNumber); + return toAjax(deviceService.resetDeviceStatus(device.getSerialNumber())); + } + + /** + * 删除设备 + */ + @PreAuthorize("@ss.hasPermi('iot:device:remove')") + @Log(title = "删除设备", businessType = BusinessType.DELETE) + @DeleteMapping("/{deviceIds}") + @ApiOperation("批量删除设备") + public AjaxResult remove(@PathVariable Long[] deviceIds) throws SchedulerException { + return deviceService.deleteDeviceByDeviceId(deviceIds[0]); + } + + /** + * 生成设备编号 + */ + @PreAuthorize("@ss.hasPermi('iot:device:add')") + @GetMapping("/generator") + @ApiOperation("生成设备编号") + public AjaxResult generatorDeviceNum(Integer type){ + return AjaxResult.success("操作成功",deviceService.generationDeviceNum(type)); + } + + @PreAuthorize("@ss.hasPermi('iot:device:query')") + @GetMapping("/gwDevCount") + @ApiOperation("子设备数量") + public AjaxResult getGwDevCount(String gwDevCode){ + return AjaxResult.success(deviceService.getSubDeviceCount(gwDevCode)); + } + + /** + * 获取设备MQTT连接参数 + * @param deviceId 设备主键id + * @return + */ + @PreAuthorize("@ss.hasPermi('iot:device:query')") + @GetMapping("/getMqttConnectData") + @ApiOperation("获取设备MQTT连接参数") + public AjaxResult getMqttConnectData(Long deviceId){ + return AjaxResult.success(deviceService.getMqttConnectData(deviceId)); + } + + @PreAuthorize("@ss.hasPermi('iot:device:add')") + @ApiOperation("下载设备导入模板") + @PostMapping("/uploadTemplate") + public void uploadTemplate(HttpServletResponse response, @RequestParam(name = "type") Integer type) + { + // 1-设备导入;2-设备分配 + if (1 == type) { + ExcelUtil util = new ExcelUtil<>(DeviceImportVO.class); + util.importTemplateExcel(response, "设备导入"); + } else if (2 == type) { + ExcelUtil util = new ExcelUtil<>(DeviceAssignmentVO.class); + util.importTemplateExcel(response, "设备分配"); + } + } + + @PreAuthorize("@ss.hasPermi('iot:device:add')") + @ApiOperation("批量导入设备") + @Log(title = "用户管理", businessType = BusinessType.IMPORT) + @PostMapping("/importData") + public AjaxResult importData(@RequestParam("file") MultipartFile file, @RequestParam("productId") Long productId) throws Exception + { + if (null == file) { + return error("导入失败,请先上传文件!"); + } + ExcelUtil util = new ExcelUtil<>(DeviceImportVO.class); + List deviceImportVOList = util.importExcel(file.getInputStream()); + if (CollectionUtils.isEmpty(deviceImportVOList)) { + return error("导入失败,模板数据不能为空!"); + } + DeviceImportVO deviceImportVO = deviceImportVOList.stream().filter(d -> StringUtils.isEmpty(d.getDeviceName())).findAny().orElse(null); + if (null != deviceImportVO) { + return error("导入失败,模板里设备名称不能为空!"); + } + String message = deviceService.importDevice(deviceImportVOList, productId); + return StringUtils.isEmpty(message) ? success("导入成功") : error(message); + } + + @PreAuthorize("@ss.hasPermi('iot:device:assignment')") + @ApiOperation("批量导入分配设备") + @Log(title = "用户管理", businessType = BusinessType.IMPORT) + @PostMapping("/importAssignmentData") + public AjaxResult importAssignmentData(@RequestParam("file") MultipartFile file, + @RequestParam("productId") Long productId, + @RequestParam("deptId") Long deptId) throws Exception + { + if (null == file) { + return error("导入失败,请先上传文件!"); + } + ExcelUtil util = new ExcelUtil<>(DeviceAssignmentVO.class); + List deviceAssignmentVOS = util.importExcel(file.getInputStream()); + if (CollectionUtils.isEmpty(deviceAssignmentVOS)) { + return error("导入失败,模板数据不能为空!"); + } + DeviceAssignmentVO deviceAssignmentVO = deviceAssignmentVOS.stream().filter(d -> StringUtils.isEmpty(d.getDeviceName())).findAny().orElse(null); + if (null != deviceAssignmentVO) { + return error("导入失败,模板里设备名称不能为空!"); + } + String message = deviceService.importAssignmentDevice(deviceAssignmentVOS, productId, deptId); + return StringUtils.isEmpty(message) ? success("导入成功") : error(message); + } + + /** + * 分配设备 + * @param deptId 机构id + * @param: deviceIds 设备id字符串 + * @return com.bnhz.common.core.domain.AjaxResult + */ + @PreAuthorize("@ss.hasPermi('iot:device:assignment')") + @ApiOperation("分配设备") + @PostMapping("/assignment") + public AjaxResult assignment(@RequestParam("deptId") Long deptId, + @RequestParam("deviceIds") String deviceIds) { + if (null == deptId) { + return error("请选择分配机构"); + } + if (StringUtils.isEmpty(deviceIds)) { + return error("请选择设备"); + } + return deviceService.assignment(deptId, deviceIds); + } + + /** + * 回收设备 + * @param: deviceIds 设备id字符串 + * @return com.bnhz.common.core.domain.AjaxResult + */ + @PreAuthorize("@ss.hasPermi('iot:device:recovery')") + @ApiOperation("回收设备") + @PostMapping("/recovery") + public AjaxResult recovery(@RequestParam("deviceIds") String deviceIds) { + if (StringUtils.isEmpty(deviceIds)) { + return error("请选择设备"); + } + return deviceService.recovery(deviceIds); + } + +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceJobController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceJobController.java new file mode 100644 index 0000000..b98ab48 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceJobController.java @@ -0,0 +1,147 @@ +package com.bnhz.data.controller; + +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.exception.job.TaskException; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.iot.domain.DeviceJob; +import com.bnhz.iot.service.IDeviceJobService; +import com.bnhz.quartz.util.CronUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 调度任务信息操作处理 + * + * @author kerwincui + */ +@Api(tags = "调度任务信息操作处理模块") +@RestController +@RequestMapping("/iot/job") +public class DeviceJobController extends BaseController +{ + @Autowired + private IDeviceJobService jobService; + + /** + * 查询定时任务列表 + */ + @ApiOperation("查询定时任务列表") + @PreAuthorize("@ss.hasPermi('iot:device:timer:list')") + @GetMapping("/list") + public TableDataInfo list(DeviceJob deviceJob) + { + startPage(); + List list = jobService.selectJobList(deviceJob); + return getDataTable(list); + } + + /** + * 导出定时任务列表 + */ + @ApiOperation("导出定时任务列表") + @PreAuthorize("@ss.hasPermi('iot:device:timer:export')") + @Log(title = "定时任务", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, DeviceJob deviceJob) + { + List list = jobService.selectJobList(deviceJob); + ExcelUtil util = new ExcelUtil(DeviceJob.class); + util.exportExcel(response, list, "定时任务"); + } + + /** + * 获取定时任务详细信息 + */ + @ApiOperation("获取定时任务详细信息") + @PreAuthorize("@ss.hasPermi('iot:device:timer:query')") + @GetMapping(value = "/{jobId}") + public AjaxResult getInfo(@PathVariable("jobId") Long jobId) + { + return AjaxResult.success(jobService.selectJobById(jobId)); + } + + /** + * 新增定时任务 + */ + @ApiOperation("新增定时任务") + @PreAuthorize("@ss.hasPermi('iot:device:timer:add')") + @Log(title = "定时任务", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody DeviceJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + job.setCreateBy(getUsername()); + return toAjax(jobService.insertJob(job)); + } + + /** + * 修改定时任务 + */ + @ApiOperation("修改定时任务") + @PreAuthorize("@ss.hasPermi('iot:device:timer:edit')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody DeviceJob job) throws SchedulerException, TaskException + { + if (!CronUtils.isValid(job.getCronExpression())) + { + return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } + job.setUpdateBy(getUsername()); + return toAjax(jobService.updateJob(job)); + } + + /** + * 定时任务状态修改 + */ + @ApiOperation("定时任务状态修改") + @PreAuthorize("@ss.hasPermi('iot:device:timer:edit')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public AjaxResult changeStatus(@RequestBody DeviceJob job) throws SchedulerException + { + DeviceJob newJob = jobService.selectJobById(job.getJobId()); + newJob.setStatus(job.getStatus()); + return toAjax(jobService.changeStatus(newJob)); + } + + /** + * 定时任务立即执行一次 + */ + @ApiOperation("定时任务立即执行一次") + @PreAuthorize("@ss.hasPermi('iot:device:timer:execute')") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/run") + public AjaxResult run(@RequestBody DeviceJob job) throws SchedulerException + { + jobService.run(job); + return AjaxResult.success(); + } + + /** + * 删除定时任务 + */ + @ApiOperation("删除定时任务") + @PreAuthorize("@ss.hasPermi('iot:device:timer:remove')") + @Log(title = "定时任务", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobIds}") + public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException, TaskException + { + jobService.deleteJobByIds(jobIds); + return AjaxResult.success(); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceLogController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceLogController.java new file mode 100644 index 0000000..22775c5 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceLogController.java @@ -0,0 +1,179 @@ +package com.bnhz.data.controller; + +import com.alibaba.fastjson2.JSONObject; +import com.bnhz.common.annotation.ApiAdd; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.DateUtils; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.data.dto.query.DeviceLogQuery; +import com.bnhz.iot.domain.DeviceLog; +import com.bnhz.iot.model.HistoryModel; +import com.bnhz.iot.model.MonitorModel; +import com.bnhz.iot.service.IDeviceLogService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.ObjectUtils; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 设备日志Controller + * + * @author kerwincui + * @date 2022-01-13 + */ +@Api(tags = "设备日志模块") +@RestController +@RequestMapping("/iot/deviceLog") +public class DeviceLogController extends BaseController +{ + @Autowired + private IDeviceLogService deviceLogService; + + /** + * 查询设备日志列表 + */ + @ApiOperation("查询设备日志列表") + @PreAuthorize("@ss.hasPermi('iot:device:list')") + @GetMapping("/list") + public TableDataInfo list(DeviceLog deviceLog) + { + startPage(); + List list = deviceLogService.selectDeviceLogList(deviceLog); + return getDataTable(list); + } + + /** + * 查询设备的监测数据 + */ + @ApiOperation("查询设备的监测数据") + @PreAuthorize("@ss.hasPermi('iot:device:list')") + @GetMapping("/monitor") + public TableDataInfo monitorList(DeviceLog deviceLog) + { + List list = deviceLogService.selectMonitorList(deviceLog); + return getDataTable(list); + } + + /** + * 导出设备日志列表 + */ + @ApiOperation("导出设备日志列表") + @PreAuthorize("@ss.hasPermi('iot:device:export')") + @Log(title = "设备日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, DeviceLog deviceLog) + { + List list = deviceLogService.selectDeviceLogList(deviceLog); + ExcelUtil util = new ExcelUtil(DeviceLog.class); + util.exportExcel(response, list, "设备日志数据"); + } + + /** + * 获取设备日志详细信息 + */ + @ApiOperation("获取设备日志详细信息") + @PreAuthorize("@ss.hasPermi('iot:device:query')") + @GetMapping(value = "/{logId}") + public AjaxResult getInfo(@PathVariable("logId") Long logId) + { + return AjaxResult.success(deviceLogService.selectDeviceLogByLogId(logId)); + } + + /** + * 新增设备日志 + */ + @ApiOperation("新增设备日志") + @PreAuthorize("@ss.hasPermi('iot:device:add')") + @Log(title = "设备日志", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody DeviceLog deviceLog) + { + return toAjax(deviceLogService.insertDeviceLog(deviceLog)); + } + + /** + * 修改设备日志 + */ + @ApiOperation("修改设备日志") + @PreAuthorize("@ss.hasPermi('iot:device:edit')") + @Log(title = "设备日志", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody DeviceLog deviceLog) + { + return toAjax(deviceLogService.updateDeviceLog(deviceLog)); + } + + /** + * 删除设备日志 + */ + @ApiOperation("删除设备日志") + @PreAuthorize("@ss.hasPermi('iot:device:remove')") + @Log(title = "设备日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{logIds}") + public AjaxResult remove(@PathVariable Long[] logIds) + { + return toAjax(deviceLogService.deleteDeviceLogByLogIds(logIds)); + } + + /** + * 查询设备的历史数据 + */ + @ApiOperation("查询设备的历史数据") + @PreAuthorize("@ss.hasPermi('iot:device:list')") + @GetMapping("/history") + public AjaxResult historyList(DeviceLog deviceLog) + { + String endTime = deviceLog.getEndTime(); + if (!StringUtils.isEmpty(endTime)) { + endTime = endTime.replace("23:59", "23:59:59"); + deviceLog.setEndTime(endTime); + } + Map> resultMap = deviceLogService.selectHistoryList(deviceLog); + return AjaxResult.success(resultMap); + } + + + @ApiAdd + @ApiOperation("[Add]查询设备单个模型历史数据") + @GetMapping("/history/model") + public TableDataInfo historyModel(DeviceLogQuery deviceLogQuery) { + DeviceLog deviceLog = new DeviceLog(); + deviceLog.setSerialNumber(deviceLogQuery.getSerialNumber().toUpperCase()); + deviceLog.setIdentityList(Collections.singletonList(deviceLogQuery.getModelCode())); + if (!ObjectUtils.isEmpty(deviceLogQuery.getBeginTime())) { + deviceLog.setBeginTime(DateUtils.toDateTimeStr(deviceLogQuery.getBeginTime())); + } + if (!ObjectUtils.isEmpty(deviceLogQuery.getEndTime())) { + deviceLog.setEndTime(DateUtils.toDateTimeStr(deviceLogQuery.getEndTime())); + } + startPage(); + List historyModels = deviceLogService.listHistory(deviceLog); + return getDataTable(historyModels); + } + + /** + * 查询设备的历史数据 + */ + @ApiOperation("查询设备的历史数据") + @PreAuthorize("@ss.hasPermi('iot:device:list')") + @GetMapping("/historyGroupByCreateTime") + public TableDataInfo historyGroupByCreateTime(DeviceLog deviceLog) + { + startPage(); + List list = deviceLogService.listhistoryGroupByCreateTime(deviceLog); + return getDataTable(list); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceShareController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceShareController.java new file mode 100644 index 0000000..958f2fe --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceShareController.java @@ -0,0 +1,120 @@ +package com.bnhz.data.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.iot.domain.DeviceShare; +import com.bnhz.iot.service.IDeviceShareService; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.common.core.page.TableDataInfo; + +/** + * 设备分享Controller + * + * @author kerwincui + * @date 2024-04-03 + */ +@RestController +@RequestMapping("/iot/share") +public class DeviceShareController extends BaseController +{ + @Autowired + private IDeviceShareService deviceShareService; + + /** + * 查询设备分享列表 + */ + @PreAuthorize("@ss.hasPermi('iot:share:list')") + @GetMapping("/list") + public TableDataInfo list(DeviceShare deviceShare) + { + startPage(); + List list = deviceShareService.selectDeviceShareList(deviceShare); + return getDataTable(list); + } + + /** + * 导出设备分享列表 + */ + @PreAuthorize("@ss.hasPermi('iot:share:export')") + @Log(title = "设备分享", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, DeviceShare deviceShare) + { + List list = deviceShareService.selectDeviceShareList(deviceShare); + ExcelUtil util = new ExcelUtil(DeviceShare.class); + util.exportExcel(response, list, "设备分享数据"); + } + + /** + * 获取设备分享详细信息 + */ + @PreAuthorize("@ss.hasPermi('iot:share:query')") + @GetMapping(value = "/detail") + public AjaxResult getInfo(Long deviceId,Long userId) + { + return success(deviceShareService.selectDeviceShareByDeviceIdAndUserId(deviceId,userId)); + } + + /** + * 新增设备分享 + */ + @PreAuthorize("@ss.hasPermi('iot:share:add')") + @Log(title = "设备分享", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody DeviceShare deviceShare) + { + return toAjax(deviceShareService.insertDeviceShare(deviceShare)); + } + + /** + * 修改设备分享 + */ + @PreAuthorize("@ss.hasPermi('iot:share:edit')") + @Log(title = "设备分享", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody DeviceShare deviceShare) + { + return toAjax(deviceShareService.updateDeviceShare(deviceShare)); + } + + /** + * 删除设备分享 + */ + @PreAuthorize("@ss.hasPermi('iot:share:remove')") + @Log(title = "设备分享", businessType = BusinessType.DELETE) + @DeleteMapping("/{deviceIds}") + public AjaxResult remove(@PathVariable Long[] deviceIds) + { + return toAjax(deviceShareService.deleteDeviceShareByDeviceIds(deviceIds)); + } + + /** + * 删除设备分享 + */ + @PreAuthorize("@ss.hasPermi('iot:share:remove')") + @Log(title = "设备分享", businessType = BusinessType.DELETE) + @DeleteMapping() + public AjaxResult delete(@RequestBody DeviceShare deviceShare) + { + return toAjax(deviceShareService.deleteDeviceShareByDeviceIdAndUserId(deviceShare)); + } + + + /** + * 获取设备分享用户信息 + */ + @GetMapping("/shareUser") + @PreAuthorize("@ss.hasPermi('iot:share:user')") + public AjaxResult userList(DeviceShare share) + { + return AjaxResult.success(deviceShareService.selectShareUser(share)); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceUserController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceUserController.java new file mode 100644 index 0000000..d157718 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/DeviceUserController.java @@ -0,0 +1,137 @@ +package com.bnhz.data.controller; + +import java.util.List; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.iot.domain.DeviceUser; +import com.bnhz.iot.service.IDeviceUserService; +import com.bnhz.common.core.page.TableDataInfo; + +/** + * 设备用户Controller + * + * @author kerwincui + * @date 2021-12-16 + */ +@Api(tags = "设备用户") +@RestController +@RequestMapping("/iot/deviceUser") +public class DeviceUserController extends BaseController +{ + @Autowired + private IDeviceUserService deviceUserService; + + /** + * 查询设备用户列表 + */ + @PreAuthorize("@ss.hasPermi('iot:device:user:list')") + @GetMapping("/list") + @ApiOperation("设备用户分页列表") + public TableDataInfo list(DeviceUser deviceUser) + { + startPage(); + List list = deviceUserService.selectDeviceUserList(deviceUser); + return getDataTable(list); + } + + /** + * 获取设备分享用户信息 + */ + @PreAuthorize("@ss.hasPermi('iot:device:user:query')") + @GetMapping("/shareUser") + public AjaxResult userList(DeviceUser user) + { + return AjaxResult.success(deviceUserService.selectShareUser(user)); + } + + /** + * 获取设备用户详细信息 根据deviceId 查询的话可能会查出多个 + */ + @PreAuthorize("@ss.hasPermi('iot:device:user:query')") + @GetMapping(value = "/{deviceId}") + @ApiOperation("获取设备用户详情") + public AjaxResult getInfo(@PathVariable("deviceId") Long deviceId) + { + return AjaxResult.success(deviceUserService.selectDeviceUserByDeviceId(deviceId)); + } + + /** + * 获取设备用户详细信息 双主键 device_id 和 user_id + */ + @PreAuthorize("@ss.hasPermi('iot:device:user:query')") + @GetMapping(value = "/{deviceId}/{userId}") + @ApiOperation("获取设备用户详情,根据用户id 和 设备id") + public AjaxResult getInfo(@PathVariable("deviceId") Long deviceId, @PathVariable("userId") Long userId) + { + return AjaxResult.success(deviceUserService.selectDeviceUserByDeviceIdAndUserId(deviceId, userId)); + } + + /** + * 新增设备用户 + */ + @PreAuthorize("@ss.hasPermi('iot:device:user:share')") + @Log(title = "设备用户", businessType = BusinessType.INSERT) + @PostMapping + @ApiOperation("添加设备用户") + public AjaxResult add(@RequestBody DeviceUser deviceUser) + { + return toAjax(deviceUserService.insertDeviceUser(deviceUser)); + } + + /** + * 新增多个设备用户 + */ + @PreAuthorize("@ss.hasPermi('iot:device:user:share')") + @Log(title = "设备用户", businessType = BusinessType.INSERT) + @PostMapping("/addDeviceUsers") + @ApiOperation("批量添加设备用户") + public AjaxResult addDeviceUsers(@RequestBody List deviceUsers) + { + return toAjax(deviceUserService.insertDeviceUserList(deviceUsers)); + } + + /** + * 修改设备用户 + */ + @ApiOperation("修改设备用户") + @PreAuthorize("@ss.hasPermi('iot:device:user:edit')") + @Log(title = "设备用户", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody DeviceUser deviceUser) + { + return toAjax(deviceUserService.updateDeviceUser(deviceUser)); + } + + + /** + * 删除设备用户 + */ + @ApiOperation("删除设备用户") + @PreAuthorize("@ss.hasPermi('iot:device:user:remove')") + @Log(title = "设备用户", businessType = BusinessType.DELETE) + @DeleteMapping + public AjaxResult remove(@RequestBody DeviceUser deviceUser) + { + int count=deviceUserService.deleteDeviceUser(deviceUser); + if(count==0){ + return AjaxResult.error("设备所有者不能删除"); + }else{ + return AjaxResult.success(); + } + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/EventLogController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/EventLogController.java new file mode 100644 index 0000000..9c24ff5 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/EventLogController.java @@ -0,0 +1,122 @@ +package com.bnhz.data.controller; + +import com.bnhz.common.annotation.ApiAdd; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.iot.domain.EventLog; +import com.bnhz.iot.model.ThingsModels.ThingsModelQuery; +import com.bnhz.iot.service.IEventLogService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; + +/** + * 事件日志Controller + * + * @author kerwincui + * @date 2023-03-28 + */ +@Api(tags = "事件日志") +@RestController +@RequestMapping("/iot/event") +public class EventLogController extends BaseController +{ + @Autowired + private IEventLogService eventLogService; + + /** + * 查询事件日志列表 + */ + @ApiOperation("查询事件日志列表") + @PreAuthorize("@ss.hasPermi('iot:event:list')") + @GetMapping("/list") + public TableDataInfo list(EventLog eventLog) + { + startPage(); + List list = eventLogService.selectEventLogList(eventLog); + return getDataTable(list); + } + + /** + * 导出事件日志列表 + */ + @ApiOperation("导出事件日志列表") + @PreAuthorize("@ss.hasPermi('iot:event:export')") + @Log(title = "事件日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, EventLog eventLog) + { + List list = eventLogService.selectEventLogList(eventLog); + ExcelUtil util = new ExcelUtil(EventLog.class); + util.exportExcel(response, list, "事件日志数据"); + } + + /** + * 获取事件日志详细信息 + */ + @ApiOperation("获取事件日志详细信息") + @PreAuthorize("@ss.hasPermi('iot:event:query')") + @GetMapping(value = "/{logId}") + public AjaxResult getInfo(@PathVariable("logId") Long logId) + { + return success(eventLogService.selectEventLogByLogId(logId)); + } + + /** + * 新增事件日志 + */ + @ApiOperation("新增事件日志") + @PreAuthorize("@ss.hasPermi('iot:event:add')") + @Log(title = "事件日志", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody EventLog eventLog) + { + return toAjax(eventLogService.insertEventLog(eventLog)); + } + + /** + * 修改事件日志 + */ + @ApiOperation("修改事件日志") + @PreAuthorize("@ss.hasPermi('iot:event:edit')") + @Log(title = "事件日志", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody EventLog eventLog) + { + return toAjax(eventLogService.updateEventLog(eventLog)); + } + + /** + * 删除事件日志 + */ + @ApiOperation("删除事件日志") + @PreAuthorize("@ss.hasPermi('iot:event:remove')") + @Log(title = "事件日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{logIds}") + public AjaxResult remove(@PathVariable Long[] logIds) + { + return toAjax(eventLogService.deleteEventLogByLogIds(logIds)); + } + + + @ApiAdd + @ApiOperation("[Add]获取事件列式数据") + @GetMapping("/column/list") + public TableDataInfo listColumnEvent(ThingsModelQuery thingsModelQuery) { + startPage(); + List> maps = eventLogService.listColumnEvent(thingsModelQuery); + return getDataTable(maps); + } + + +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/FunctionLogController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/FunctionLogController.java new file mode 100644 index 0000000..3ce1f91 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/FunctionLogController.java @@ -0,0 +1,114 @@ +package com.bnhz.data.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.iot.domain.FunctionLog; +import com.bnhz.iot.service.IFunctionLogService; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.common.core.page.TableDataInfo; + +/** + * 设备服务下发日志Controller + * + * @author kerwincui + * @date 2022-10-22 + */ +@Api(tags = "设备服务下发日志") +@RestController +@RequestMapping("/iot/log") +public class FunctionLogController extends BaseController +{ + @Autowired + private IFunctionLogService functionLogService; + + /** + * 查询设备服务下发日志列表 + */ + @ApiOperation("查询设备服务下发日志列表") + @PreAuthorize("@ss.hasPermi('iot:log:list')") + @GetMapping("/list") + public TableDataInfo list(FunctionLog functionLog) + { + startPage(); + List list = functionLogService.selectFunctionLogList(functionLog); + return getDataTable(list); + } + + /** + * 导出设备服务下发日志列表 + */ + @ApiOperation("导出设备服务下发日志列表") + @PreAuthorize("@ss.hasPermi('iot:log:export')") + @Log(title = "设备服务下发日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, FunctionLog functionLog) + { + List list = functionLogService.selectFunctionLogList(functionLog); + ExcelUtil util = new ExcelUtil(FunctionLog.class); + util.exportExcel(response, list, "设备服务下发日志数据"); + } + + /** + * 获取设备服务下发日志详细信息 + */ + @ApiOperation("获取设备服务下发日志详细信息") + @PreAuthorize("@ss.hasPermi('iot:log:query')") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") Long id) + { + return AjaxResult.success(functionLogService.selectFunctionLogById(id)); + } + + /** + * 新增设备服务下发日志 + */ + @ApiOperation("新增设备服务下发日志") + @PreAuthorize("@ss.hasPermi('iot:log:add')") + @Log(title = "设备服务下发日志", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody FunctionLog functionLog) + { + return toAjax(functionLogService.insertFunctionLog(functionLog)); + } + + /** + * 修改设备服务下发日志 + */ + @ApiOperation("修改设备服务下发日志") + @PreAuthorize("@ss.hasPermi('iot:log:edit')") + @Log(title = "设备服务下发日志", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody FunctionLog functionLog) + { + return toAjax(functionLogService.updateFunctionLog(functionLog)); + } + + /** + * 删除设备服务下发日志 + */ + @ApiOperation("删除设备服务下发日志") + @PreAuthorize("@ss.hasPermi('iot:log:remove')") + @Log(title = "设备服务下发日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) + { + return toAjax(functionLogService.deleteFunctionLogByIds(ids)); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/GoviewProjectController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/GoviewProjectController.java new file mode 100644 index 0000000..9bbebd5 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/GoviewProjectController.java @@ -0,0 +1,184 @@ +package com.bnhz.data.controller; + +import com.bnhz.common.annotation.Log; +import com.bnhz.common.config.DaQiConfig; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.bean.BeanUtils; +import com.bnhz.iot.domain.GoviewProject; +import com.bnhz.iot.domain.GoviewProjectData; +import com.bnhz.iot.model.goview.GoviewProjectVo; +import com.bnhz.iot.service.IGoviewProjectDataService; +import com.bnhz.iot.service.IGoviewProjectService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 项目Controller + * + * @author kami + * @date 2022-10-27 + */ +@Api(tags = "项目模块") +@RestController +@RequestMapping("/goview/project") +public class GoviewProjectController extends BaseController { + + @Autowired + private IGoviewProjectService goviewProjectService; + + @Autowired + private IGoviewProjectDataService goviewProjectDataService; + + /** + * 查询项目列表 + */ + @ApiOperation("查询项目列表") + @GetMapping("/list") + public TableDataInfo list(GoviewProject goviewProject) { + startPage(); + SysUser user = getLoginUser().getUser(); + if (null != user.getDeptId()) { + goviewProject.setTenantId(user.getDept().getDeptUserId()); + } else { + goviewProject.setTenantId(user.getUserId()); + } + List list = goviewProjectService.selectGoviewProjectList(goviewProject); + return getDataTable(list); + } + + /** + * 获取项目详细信息 + */ + @ApiOperation("获取项目详细信息") + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable("id") String id) { + return AjaxResult.success(goviewProjectService.selectGoviewProjectById(id)); + } + + /** + * 新增项目 + */ + @ApiOperation("新增项目") + @Log(title = "项目", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody GoviewProject goviewProject) { + SysUser user = getLoginUser().getUser(); + if (null != user.getDeptId()) { + goviewProject.setTenantId(user.getDept().getDeptUserId()); + goviewProject.setTenantName(user.getDept().getDeptUserName()); + } else { + goviewProject.setTenantId(user.getUserId()); + goviewProject.setTenantName(user.getUserName()); + } + String projectId = goviewProjectService.insertGoviewProject(goviewProject); + if(StringUtils.isNotEmpty(projectId)){ + return AjaxResult.success("创建成功",goviewProject); + }else { + return AjaxResult.error("创建失败"); + } + } + + /** + * 修改项目 + */ + @ApiOperation("修改项目") + @Log(title = "项目", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody GoviewProject goviewProject) { + return toAjax(goviewProjectService.updateGoviewProject(goviewProject)); + } + + /** + * 删除项目 + */ + @ApiOperation("删除项目") + @Log(title = "项目", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable String[] ids) { + return toAjax(goviewProjectService.deleteGoviewProjectByIds(ids)); + } + + + /** + * 获取项目存储数据 + * @param projectId 项目id + * @return + */ + @ApiOperation("获取项目存储数据") + @GetMapping("/getData") + public AjaxResult getData(String projectId) { + GoviewProject goviewProject = goviewProjectService.selectGoviewProjectById(projectId); + GoviewProjectData projectData = goviewProjectDataService.selectGoviewProjectDataByProjectId(projectId); + GoviewProjectVo goviewProjectVo = new GoviewProjectVo(); + BeanUtils.copyBeanProp(goviewProjectVo,goviewProject); + if(projectData != null) { + goviewProjectVo.setContent(projectData.getDataToStr()); + } + return AjaxResult.success(goviewProjectVo); + } + + + /** + * 保存大屏内部数据(字节) + * @param data + * @return + */ + @ApiOperation("保存大屏内部数据") + @PostMapping("/save/data") + public AjaxResult saveData(GoviewProjectData data) { + GoviewProject goviewProject= goviewProjectService.selectGoviewProjectById(data.getProjectId()); + if(goviewProject == null) { + return AjaxResult.error("没有该项目ID"); + } + int i = goviewProjectDataService.insertOrUpdateGoviewProjectData(data); + if(i > 0) { + return AjaxResult.success("数据保存成功"); + } + return AjaxResult.error("保存失败"); + } + + + /** + * goview文件上传(同一个大屏覆盖保存) + */ + @PostMapping("/upload") + @ApiOperation("文件上传") + public AjaxResult uploadFile(@RequestBody MultipartFile object) throws Exception { + try { + String filePath = DaQiConfig.getProfile(); + // 获取文件名和文件类型 + String fileName = object.getOriginalFilename(); + fileName = "/goview/" + getLoginUser().getUserId().toString() + "/" + fileName; + //创建目录 + File desc = new File(filePath + File.separator + fileName); + if (!desc.exists()) { + if (!desc.getParentFile().exists()) { + desc.getParentFile().mkdirs(); + } + } + // 存储文件-覆盖存储(一个文件一个图,防止过多) + object.transferTo(desc); + String url = "/profile" + fileName; + Map map=new HashMap(2); + map.put("fileName", url); + map.put("url", url); + return AjaxResult.success("上传成功",map); + } catch (Exception e) { + return AjaxResult.error(e.getMessage()); + } + } + +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/GoviewProjectDataController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/GoviewProjectDataController.java new file mode 100644 index 0000000..f668bfb --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/GoviewProjectDataController.java @@ -0,0 +1,42 @@ +package com.bnhz.data.controller; + +import com.bnhz.common.annotation.Anonymous; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.iot.service.IGoviewProjectDataService; +import io.swagger.annotations.Api; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * goview 获取数据接口 + * 可自己根据需求在此添加获取数据接口 + * @author fastb + * @date 2023-11-06 15:07 + */ +@Anonymous +@Api(tags = "大屏管理获取数据") +@RestController +@RequestMapping("/goview/projectData") +public class GoviewProjectDataController { + + @Resource + private IGoviewProjectDataService goviewProjectDataService; + + /** + * 根据sql获取组件数据接口 + * @param sql sql + * @return 组件数据 + */ + @PostMapping("/executeSql") + public AjaxResult executeSql(@RequestParam String sql) { + if (StringUtils.isEmpty(sql)) { + return AjaxResult.error("请编写sql语句"); + } + return goviewProjectDataService.executeSql(sql); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/GroupController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/GroupController.java new file mode 100644 index 0000000..299dcae --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/GroupController.java @@ -0,0 +1,138 @@ +package com.bnhz.data.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.bnhz.iot.model.DeviceGroupInput; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.iot.domain.Group; +import com.bnhz.iot.service.IGroupService; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.common.core.page.TableDataInfo; + +/** + * 设备分组Controller + * + * @author kerwincui + * @date 2021-12-16 + */ +@Api(tags = "设备分组") +@RestController +@RequestMapping("/iot/group") +public class GroupController extends BaseController +{ + @Autowired + private IGroupService groupService; + + /** + * 查询设备分组列表 + */ + @PreAuthorize("@ss.hasPermi('iot:group:list')") + @GetMapping("/list") + @ApiOperation("分组分页列表") + public TableDataInfo list(Group group) + { + startPage(); + return getDataTable(groupService.selectGroupList(group)); + } + + /** + * 导出设备分组列表 + */ + @PreAuthorize("@ss.hasPermi('iot:group:export')") + @Log(title = "分组", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @ApiOperation("导出分组") + public void export(HttpServletResponse response, Group group) + { + List list = groupService.selectGroupList(group); + ExcelUtil util = new ExcelUtil(Group.class); + util.exportExcel(response, list, "设备分组数据"); + } + + /** + * 获取设备分组详细信息 + */ + @PreAuthorize("@ss.hasPermi('iot:group:query')") + @GetMapping(value = "/{groupId}") + @ApiOperation("获取分组详情") + public AjaxResult getInfo(@PathVariable("groupId") Long groupId) + { + return AjaxResult.success(groupService.selectGroupByGroupId(groupId)); + } + + /** + * 获取分组下的所有关联设备ID数组 + */ + @PreAuthorize("@ss.hasPermi('iot:group:query')") + @GetMapping(value = "/getDeviceIds/{groupId}") + @ApiOperation("获取分组下的所有关联设备ID数组") + public AjaxResult getDeviceIds(@PathVariable("groupId") Long groupId) + { + return AjaxResult.success(groupService.selectDeviceIdsByGroupId(groupId)); + } + + /** + * 新增设备分组 + */ + @PreAuthorize("@ss.hasPermi('iot:group:add')") + @Log(title = "分组", businessType = BusinessType.INSERT) + @PostMapping + @ApiOperation("添加分组") + public AjaxResult add(@RequestBody Group group) + { + return toAjax(groupService.insertGroup(group)); + } + + /** + * 更新分组下的关联设备 + * @param input + * @return + */ + @PreAuthorize("@ss.hasPermi('iot:group:edit')") + @Log(title = "设备分组", businessType = BusinessType.UPDATE) + @PutMapping("/updateDeviceGroups") + @ApiOperation("更新分组下的关联设备") + public AjaxResult updateDeviceGroups(@RequestBody DeviceGroupInput input){ + return toAjax(groupService.updateDeviceGroups(input)); + } + + /** + * 修改设备分组 + */ + @PreAuthorize("@ss.hasPermi('iot:group:edit')") + @Log(title = "分组", businessType = BusinessType.UPDATE) + @PutMapping + @ApiOperation("修改分组") + public AjaxResult edit(@RequestBody Group group) + { + return toAjax(groupService.updateGroup(group)); + } + + /** + * 删除设备分组 + */ + @PreAuthorize("@ss.hasPermi('iot:group:remove')") + @Log(title = "分组", businessType = BusinessType.DELETE) + @DeleteMapping("/{groupIds}") + @ApiOperation("批量删除设备分组") + public AjaxResult remove(@PathVariable Long[] groupIds) + { + return toAjax(groupService.deleteGroupByGroupIds(groupIds)); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/NewsCategoryController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/NewsCategoryController.java new file mode 100644 index 0000000..c37d0ac --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/NewsCategoryController.java @@ -0,0 +1,126 @@ +package com.bnhz.data.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.bnhz.iot.model.IdAndName; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.iot.domain.NewsCategory; +import com.bnhz.iot.service.INewsCategoryService; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.common.core.page.TableDataInfo; + +/** + * 新闻分类Controller + * + * @author kerwincui + * @date 2022-04-09 + */ +@Api(tags = "新闻分类") +@RestController +@RequestMapping("/iot/newsCategory") +public class NewsCategoryController extends BaseController +{ + @Autowired + private INewsCategoryService newsCategoryService; + + /** + * 查询新闻分类列表 + */ + @PreAuthorize("@ss.hasPermi('iot:newsCategory:list')") + @GetMapping("/list") + @ApiOperation("新闻分类分页列表") + public TableDataInfo list(NewsCategory newsCategory) + { + startPage(); + List list = newsCategoryService.selectNewsCategoryList(newsCategory); + return getDataTable(list); + } + + /** + * 查询新闻分类简短列表 + */ + @PreAuthorize("@ss.hasPermi('iot:news:list')") + @GetMapping("/newsCategoryShortList") + @ApiOperation("分类简短列表") + public AjaxResult newsCategoryShortList() + { + List list = newsCategoryService.selectNewsCategoryShortList(); + return AjaxResult.success(list); + } + + /** + * 导出新闻分类列表 + */ + @PreAuthorize("@ss.hasPermi('iot:newsCategory:export')") + @Log(title = "新闻分类", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, NewsCategory newsCategory) + { + List list = newsCategoryService.selectNewsCategoryList(newsCategory); + ExcelUtil util = new ExcelUtil(NewsCategory.class); + util.exportExcel(response, list, "新闻分类数据"); + } + + /** + * 获取新闻分类详细信息 + */ + @PreAuthorize("@ss.hasPermi('iot:newsCategory:query')") + @GetMapping(value = "/{categoryId}") + @ApiOperation("新闻分类详情") + public AjaxResult getInfo(@PathVariable("categoryId") Long categoryId) + { + return AjaxResult.success(newsCategoryService.selectNewsCategoryByCategoryId(categoryId)); + } + + /** + * 新增新闻分类 + */ + @PreAuthorize("@ss.hasPermi('iot:newsCategory:add')") + @Log(title = "新闻分类", businessType = BusinessType.INSERT) + @PostMapping + @ApiOperation("添加新闻分类") + public AjaxResult add(@RequestBody NewsCategory newsCategory) + { + return toAjax(newsCategoryService.insertNewsCategory(newsCategory)); + } + + /** + * 修改新闻分类 + */ + @PreAuthorize("@ss.hasPermi('iot:newsCategory:edit')") + @Log(title = "新闻分类", businessType = BusinessType.UPDATE) + @PutMapping + @ApiOperation("修改新闻分类") + public AjaxResult edit(@RequestBody NewsCategory newsCategory) + { + return toAjax(newsCategoryService.updateNewsCategory(newsCategory)); + } + + /** + * 删除新闻分类 + */ + @PreAuthorize("@ss.hasPermi('iot:newsCategory:remove')") + @Log(title = "新闻分类", businessType = BusinessType.DELETE) + @DeleteMapping("/{categoryIds}") + @ApiOperation("删除新闻分类") + public AjaxResult remove(@PathVariable Long[] categoryIds) + { + return newsCategoryService.deleteNewsCategoryByCategoryIds(categoryIds); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/NewsController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/NewsController.java new file mode 100644 index 0000000..18d447d --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/NewsController.java @@ -0,0 +1,141 @@ +package com.bnhz.data.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.bnhz.iot.model.CategoryNews; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.iot.domain.News; +import com.bnhz.iot.service.INewsService; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.common.core.page.TableDataInfo; + +/** + * 新闻资讯Controller + * + * @author kerwincui + * @date 2022-04-09 + */ +@Api(tags = "新闻资讯") +@RestController +@RequestMapping("/iot/news") +public class NewsController extends BaseController +{ + @Autowired + private INewsService newsService; + + /** + * 查询新闻资讯列表 + */ + @PreAuthorize("@ss.hasPermi('iot:news:list')") + @GetMapping("/list") + @ApiOperation("新闻分页列表") + public TableDataInfo list(News news) + { + startPage(); + List list = newsService.selectNewsList(news); + return getDataTable(list); + } + + /** + * 查询轮播的新闻资讯 + */ + @PreAuthorize("@ss.hasPermi('iot:news:list')") + @GetMapping("/bannerList") + @ApiOperation("轮播新闻列表") + public AjaxResult bannerList() + { + News news=new News(); + news.setIsBanner(1); + news.setStatus(1); + List list = newsService.selectNewsList(news); + return AjaxResult.success(list); + } + + /** + * 查询置顶的新闻资讯 + */ + @PreAuthorize("@ss.hasPermi('iot:news:list')") + @GetMapping("/topList") + @ApiOperation("置顶新闻列表") + public AjaxResult topList() + { + List list = newsService.selectTopNewsList(); + return AjaxResult.success(list); + } + + /** + * 导出新闻资讯列表 + */ + @PreAuthorize("@ss.hasPermi('iot:news:export')") + @Log(title = "新闻资讯", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, News news) + { + List list = newsService.selectNewsList(news); + ExcelUtil util = new ExcelUtil(News.class); + util.exportExcel(response, list, "新闻资讯数据"); + } + + /** + * 获取新闻资讯详细信息 + */ + @PreAuthorize("@ss.hasPermi('iot:news:query')") + @GetMapping(value = "/{newsId}") + @ApiOperation("新闻详情") + public AjaxResult getInfo(@PathVariable("newsId") Long newsId) + { + return AjaxResult.success(newsService.selectNewsByNewsId(newsId)); + } + + /** + * 新增新闻资讯 + */ + @PreAuthorize("@ss.hasPermi('iot:news:add')") + @Log(title = "新闻资讯", businessType = BusinessType.INSERT) + @PostMapping + @ApiOperation("添加新闻资讯") + public AjaxResult add(@RequestBody News news) + { + return toAjax(newsService.insertNews(news)); + } + + /** + * 修改新闻资讯 + */ + @PreAuthorize("@ss.hasPermi('iot:news:edit')") + @Log(title = "新闻资讯", businessType = BusinessType.UPDATE) + @PutMapping + @ApiOperation("修改新闻资讯") + public AjaxResult edit(@RequestBody News news) + { + return toAjax(newsService.updateNews(news)); + } + + /** + * 删除新闻资讯 + */ + @PreAuthorize("@ss.hasPermi('iot:news:remove')") + @Log(title = "新闻资讯", businessType = BusinessType.DELETE) + @DeleteMapping("/{newsIds}") + @ApiOperation("删除新闻资讯") + public AjaxResult remove(@PathVariable Long[] newsIds) + { + return toAjax(newsService.deleteNewsByNewsIds(newsIds)); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/ProductAuthorizeController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/ProductAuthorizeController.java new file mode 100644 index 0000000..a372367 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/ProductAuthorizeController.java @@ -0,0 +1,120 @@ +package com.bnhz.data.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.bnhz.iot.model.ProductAuthorizeVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.iot.domain.ProductAuthorize; +import com.bnhz.iot.service.IProductAuthorizeService; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.common.core.page.TableDataInfo; + +/** + * 产品授权码Controller + * + * @author kami + * @date 2022-04-11 + */ +@Api(tags = "产品授权码") +@RestController +@RequestMapping("/iot/authorize") +public class ProductAuthorizeController extends BaseController +{ + @Autowired + private IProductAuthorizeService productAuthorizeService; + + /** + * 查询产品授权码列表 + */ + @ApiOperation("查询产品授权码列表") + @PreAuthorize("@ss.hasPermi('iot:authorize:query')") + @GetMapping("/list") + public TableDataInfo list(ProductAuthorize productAuthorize) + { + startPage(); + List list = productAuthorizeService.selectProductAuthorizeList(productAuthorize); + return getDataTable(list); + } + + /** + * 导出产品授权码列表 + */ + @ApiOperation("导出产品授权码列表") + @PreAuthorize("@ss.hasPermi('iot:authorize:export')") + @Log(title = "产品授权码", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, ProductAuthorize productAuthorize) + { + List list = productAuthorizeService.selectProductAuthorizeList(productAuthorize); + ExcelUtil util = new ExcelUtil(ProductAuthorize.class); + util.exportExcel(response, list, "产品授权码数据"); + } + + /** + * 获取产品授权码详细信息 + */ + @ApiOperation("获取产品授权码详细信息") + @PreAuthorize("@ss.hasPermi('iot:authorize:query')") + @GetMapping(value = "/{authorizeId}") + public AjaxResult getInfo(@PathVariable("authorizeId") Long authorizeId) + { + return AjaxResult.success(productAuthorizeService.selectProductAuthorizeByAuthorizeId(authorizeId)); + } + + /** + * 新增产品授权码 + */ + @ApiOperation("新增产品授权码") + @PreAuthorize("@ss.hasPermi('iot:authorize:add')") + @Log(title = "产品授权码", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody ProductAuthorize productAuthorize) + { + return toAjax(productAuthorizeService.insertProductAuthorize(productAuthorize)); + } + + /** + * 根据数量批量新增产品授权码 + */ + @ApiOperation("根据数量批量新增产品授权码") + @PreAuthorize("@ss.hasPermi('iot:authorize:add')") + @Log(title = "根据数量批量新增产品授权码", businessType = BusinessType.INSERT) + @PostMapping("addProductAuthorizeByNum") + public AjaxResult addProductAuthorizeByNum(@RequestBody ProductAuthorizeVO productAuthorizeVO) + { + return toAjax(productAuthorizeService.addProductAuthorizeByNum(productAuthorizeVO)); + } + + /** + * 修改产品授权码 + */ + @ApiOperation("修改产品授权码") + @PreAuthorize("@ss.hasPermi('iot:authorize:edit')") + @Log(title = "产品授权码", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody ProductAuthorize productAuthorize) + { + return toAjax(productAuthorizeService.updateProductAuthorize(productAuthorize)); + } + + /** + * 删除产品授权码 + */ + @ApiOperation("删除产品授权码") + @PreAuthorize("@ss.hasPermi('iot:authorize:remove')") + @Log(title = "产品授权码", businessType = BusinessType.DELETE) + @DeleteMapping("/{authorizeIds}") + public AjaxResult remove(@PathVariable Long[] authorizeIds) + { + return toAjax(productAuthorizeService.deleteProductAuthorizeByAuthorizeIds(authorizeIds)); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/ProductController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/ProductController.java new file mode 100644 index 0000000..3cf24b6 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/ProductController.java @@ -0,0 +1,208 @@ +package com.bnhz.data.controller; + +import com.bnhz.common.annotation.ApiAdd; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.SecurityUtils; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.iot.domain.Product; +import com.bnhz.iot.model.ChangeProductStatusModel; +import com.bnhz.iot.model.IdAndName; +import com.bnhz.iot.service.IProductService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Objects; + +/** + * 产品Controller + * + * @author kerwincui + * @date 2021-12-16 + */ +@Api(tags = "产品管理") +@RestController +@RequestMapping("/iot/product") +public class ProductController extends BaseController +{ + @Autowired + private IProductService productService; + + /** + * 查询产品列表 + */ + @GetMapping("/list") + @ApiOperation("产品分页列表") + public TableDataInfo list(Product product) + { + startPage(); + SysUser user = getLoginUser().getUser(); + if (null == user.getDeptId()) { + product.setTenantId(user.getUserId()); + return getDataTable(productService.selectTerminalUserProduct(product)); + } + Boolean showSenior = product.getShowSenior(); + if (Objects.isNull(showSenior)){ + //默认展示上级产品 + product.setShowSenior(true); + } + Long deptUserId = getLoginUser().getUser().getDept().getDeptUserId(); + product.setAdmin(SecurityUtils.isAdmin(deptUserId)); + product.setDeptId(getLoginUser().getDeptId()); + product.setTenantId(deptUserId); + return getDataTable(productService.selectProductList(product)); + } + + /** + * 查询产品简短列表 + */ + @PreAuthorize("@ss.hasPermi('iot:product:list')") + @GetMapping("/shortList") + @ApiOperation("产品简短列表") + public AjaxResult shortList(Product product) + { + Boolean showSenior = product.getShowSenior(); + if (Objects.isNull(showSenior)){ + //默认展示上级产品 + product.setShowSenior(true); + } + Long deptUserId = getLoginUser().getUser().getDept().getDeptUserId(); + product.setAdmin(SecurityUtils.isAdmin(deptUserId)); + product.setDeptId(getLoginUser().getDeptId()); + product.setTenantId(deptUserId); + startPage(); + List list = productService.selectProductShortList(product); + return AjaxResult.success(list); + } + + /** + * 导出产品列表 + */ + @PreAuthorize("@ss.hasPermi('iot:product:export')") + @Log(title = "产品", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @ApiOperation("导出产品") + public void export(HttpServletResponse response, Product product) + { + List list = productService.selectProductList(product); + ExcelUtil util = new ExcelUtil(Product.class); + util.exportExcel(response, list, "产品数据"); + } + + /** + * 获取产品详细信息 + */ + @PreAuthorize("@ss.hasPermi('iot:product:query')") + @GetMapping(value = "/{productId}") + @ApiOperation("获取产品详情") + public AjaxResult getInfo(@PathVariable("productId") Long productId) + { + Product product = productService.selectProductByProductId(productId); + Long deptUserId = getLoginUser().getUser().getDept().getDeptUserId(); + if (!Objects.equals(product.getTenantId(), deptUserId)){ + product.setIsOwner(0); + }else { + product.setIsOwner(1); + } + return AjaxResult.success(product); + } + + /** + * 新增产品 + */ + @PreAuthorize("@ss.hasPermi('iot:product:add')") + @Log(title = "添加产品", businessType = BusinessType.INSERT) + @PostMapping + @ApiOperation("添加产品") + public AjaxResult add(@RequestBody Product product) + { + LoginUser loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUser() == null) { + return error("请登录后重试!"); + } + // 查询归属机构 + if (null != loginUser.getDeptId()) { + product.setTenantId(loginUser.getUser().getDept().getDeptUserId()); + product.setTenantName(loginUser.getUser().getDept().getDeptUserName()); + } else { + product.setTenantId(loginUser.getUser().getUserId()); + product.setTenantName(loginUser.getUser().getUserName()); + } + return AjaxResult.success(productService.insertProduct(product)); + } + + /** + * 修改产品 + */ + @PreAuthorize("@ss.hasPermi('iot:product:edit')") + @Log(title = "修改产品", businessType = BusinessType.UPDATE) + @PutMapping + @ApiOperation("修改产品") + public AjaxResult edit(@RequestBody Product product) + { + return toAjax(productService.updateProduct(product)); + } + + /** + * 获取产品下面的设备数量 + */ + @PreAuthorize("@ss.hasPermi('iot:product:query')") + @GetMapping("/deviceCount/{productId}") + @ApiOperation("获取产品下面的设备数量") + public AjaxResult deviceCount(@PathVariable Long productId) + { + return AjaxResult.success(productService.selectDeviceCountByProductId(productId)); + } + + /** + * 发布产品 + */ + @PreAuthorize("@ss.hasPermi('iot:product:add')") + @Log(title = "更新产品状态", businessType = BusinessType.UPDATE) + @PutMapping("/status") + @ApiOperation("更新产品状态") + public AjaxResult changeProductStatus(@RequestBody ChangeProductStatusModel model) + { + return productService.changeProductStatus(model); + } + + /** + * 删除产品 + */ + @PreAuthorize("@ss.hasPermi('iot:product:remove')") + @Log(title = "产品", businessType = BusinessType.DELETE) + @DeleteMapping("/{productIds}") + @ApiOperation("批量删除产品") + public AjaxResult remove(@PathVariable Long[] productIds) + { + return productService.deleteProductByProductIds(productIds); + } + + + /** + * 查询采集点模板关联的所有产品 + */ + @PreAuthorize("@ss.hasPermi('iot:product:list')") + @GetMapping("/queryByTemplateId") + @ApiOperation("查询采集点模板id关联的所有产品") + public AjaxResult queryByTemplateId(@RequestParam Long templateId){ + return AjaxResult.success(productService.selectByTempleId(templateId)); + } + + @PostMapping("/copy/{productId}") + @ApiOperation("拷贝产品") + @ApiAdd + public AjaxResult copyProduct(@PathVariable("productId") Long productId) { + return AjaxResult.success(productService.copyProduct(productId)); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/SceneController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/SceneController.java new file mode 100644 index 0000000..d14bf9e --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/SceneController.java @@ -0,0 +1,141 @@ +package com.bnhz.data.controller; + +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.model.LoginUser; +import com.bnhz.common.core.page.TableDataInfo; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.iot.domain.Scene; +import com.bnhz.iot.service.ISceneService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 场景联动Controller + * + * @author kerwincui + * @date 2022-01-13 + */ +@Api(tags = "场景联动") +@RestController +@RequestMapping("/iot/scene") +public class SceneController extends BaseController +{ + @Autowired + private ISceneService sceneService; + + /** + * 查询场景联动列表 + */ + @ApiOperation("查询场景联动列表") + @PreAuthorize("@ss.hasPermi('iot:scene:list')") + @GetMapping("/list") + public TableDataInfo list(Scene scene) + { + startPage(); + List list = sceneService.selectSceneList(scene); + return getDataTable(list); + } + + /** + * 导出场景联动列表 + */ + @ApiOperation("导出场景联动列表") + @PreAuthorize("@ss.hasPermi('iot:scene:export')") + @Log(title = "场景联动", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(HttpServletResponse response, Scene scene) + { + List list = sceneService.selectSceneList(scene); + ExcelUtil util = new ExcelUtil(Scene.class); + util.exportExcel(response, list, "场景联动数据"); + } + + /** + * 获取场景联动详细信息 + */ + @ApiOperation("获取场景联动详细信息") + @PreAuthorize("@ss.hasPermi('iot:scene:query')") + @GetMapping(value = "/{sceneId}") + public AjaxResult getInfo(@PathVariable("sceneId") Long sceneId) + { + return AjaxResult.success(sceneService.selectSceneBySceneId(sceneId)); + } + + /** + * 新增场景联动 + */ + @ApiOperation("新增场景联动") + @PreAuthorize("@ss.hasPermi('iot:scene:add')") + @Log(title = "场景联动", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody Scene scene) + { + LoginUser loginUser = getLoginUser(); + if (loginUser == null || loginUser.getUser() == null) { + return error("请登录后重试!"); + } + // 查询归属机构 + if (null != loginUser.getDeptId()) { + scene.setUserId(loginUser.getUser().getDept().getDeptUserId()); + scene.setUserName(loginUser.getUser().getDept().getDeptUserName()); + scene.setTerminalUser(0); + } else { + scene.setUserId(loginUser.getUser().getUserId()); + scene.setUserName(loginUser.getUser().getUserName()); + scene.setTerminalUser(1); + } + return toAjax(sceneService.insertScene(scene)); + } + + /** + * 修改场景联动 + */ + @ApiOperation("修改场景联动") + @PreAuthorize("@ss.hasPermi('iot:scene:edit')") + @Log(title = "场景联动", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody Scene scene) + { + return toAjax(sceneService.updateScene(scene)); + } + + /** + * 删除场景联动 + */ + @ApiOperation("删除场景联动") + @PreAuthorize("@ss.hasPermi('iot:scene:remove')") + @Log(title = "场景联动", businessType = BusinessType.DELETE) + @DeleteMapping("/{sceneIds}") + public AjaxResult remove(@PathVariable Long[] sceneIds) + { + return toAjax(sceneService.deleteSceneBySceneIds(sceneIds)); + } + + /** + * 修改场景联动状态 + */ + @ApiOperation("修改场景联动状态") + @PreAuthorize("@ss.hasPermi('iot:scene:edit')") + @Log(title = "场景联动", businessType = BusinessType.UPDATE) + @PutMapping("/updateStatus") + public AjaxResult updateStatus(@RequestBody Scene scene) + { + return toAjax(sceneService.updateStatus(scene)); + } +} diff --git a/bnhz-open-api/src/main/java/com/bnhz/data/controller/ScriptController.java b/bnhz-open-api/src/main/java/com/bnhz/data/controller/ScriptController.java new file mode 100644 index 0000000..54d1383 --- /dev/null +++ b/bnhz-open-api/src/main/java/com/bnhz/data/controller/ScriptController.java @@ -0,0 +1,114 @@ +package com.bnhz.data.controller; + +import java.util.List; +import javax.servlet.http.HttpServletResponse; + +import com.bnhz.iot.domain.Script; +import com.bnhz.iot.service.IScriptService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RestController; +import com.bnhz.common.annotation.Log; +import com.bnhz.common.core.controller.BaseController; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.enums.BusinessType; +import com.bnhz.common.utils.poi.ExcelUtil; +import com.bnhz.common.core.page.TableDataInfo; + +/** + * 规则引擎脚本Controller + * + * @author lizhuangpeng + * @date 2023-07-01 + */ +@RestController +@RequestMapping("/iot/script") +public class ScriptController extends BaseController +{ + @Autowired + private IScriptService scriptService; + + /** + * 查询规则引擎脚本列表 + */ + @PreAuthorize("@ss.hasPermi('iot:script:list')") + @GetMapping("/list") + public TableDataInfo list(Script ruleScript) + { + startPage(); + List diff --git a/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/index.vue.vm b/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/index.vue.vm new file mode 100644 index 0000000..6296014 --- /dev/null +++ b/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/index.vue.vm @@ -0,0 +1,602 @@ + + + diff --git a/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm b/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm new file mode 100644 index 0000000..7bbd2fc --- /dev/null +++ b/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm @@ -0,0 +1,474 @@ + + + diff --git a/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/v3/index.vue.vm b/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/v3/index.vue.vm new file mode 100644 index 0000000..8b25665 --- /dev/null +++ b/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/v3/index.vue.vm @@ -0,0 +1,590 @@ + + + diff --git a/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/v3/readme.txt b/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/v3/readme.txt new file mode 100644 index 0000000..99239bb --- /dev/null +++ b/bnhz-plugs/bnhz-generator/src/main/resources/vm/vue/v3/readme.txt @@ -0,0 +1 @@ +ʹõRuoYi-Vue3ǰˣôҪһ´Ŀ¼ģindex.vue.vmindex-tree.vue.vmļϼvueĿ¼ \ No newline at end of file diff --git a/bnhz-plugs/bnhz-generator/src/main/resources/vm/xml/mapper.xml.vm b/bnhz-plugs/bnhz-generator/src/main/resources/vm/xml/mapper.xml.vm new file mode 100644 index 0000000..0ceb3d8 --- /dev/null +++ b/bnhz-plugs/bnhz-generator/src/main/resources/vm/xml/mapper.xml.vm @@ -0,0 +1,135 @@ + + + + + +#foreach ($column in $columns) + +#end + +#if($table.sub) + + + + + + +#foreach ($column in $subTable.columns) + +#end + +#end + + + select#foreach($column in $columns) $column.columnName#if($foreach.count != $columns.size()),#end#end from ${tableName} + + + + + + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + $column.columnName, +#end +#end + + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment) + #{$column.javaField}, +#end +#end + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.columnName != $pkColumn.columnName) + $column.columnName = #{$column.javaField}, +#end +#end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + +#if($table.sub) + + + delete from ${subTableName} where ${subTableFkName} in + + #{${subTableFkclassName}} + + + + + delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}} + + + + insert into ${subTableName}(#foreach($column in $subTable.columns) $column.columnName#if($foreach.count != $subTable.columns.size()),#end#end) values + + (#foreach($column in $subTable.columns) #{item.$column.javaField}#if($foreach.count != $subTable.columns.size()),#end#end) + + +#end + \ No newline at end of file diff --git a/bnhz-plugs/bnhz-oauth/pom.xml b/bnhz-plugs/bnhz-oauth/pom.xml new file mode 100644 index 0000000..63dadb3 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + bnhz-plugs + com.bnhz + 3.8.5 + + + bnhz-oauth + + + 8 + 8 + UTF-8 + + + + + + org.springframework.security.oauth + spring-security-oauth2 + 2.5.1.RELEASE + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + com.bnhz + bnhz-framework + + + com.bnhz + bnhz-common + + + + diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/config/AuthorizationServerConfig.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/config/AuthorizationServerConfig.java new file mode 100644 index 0000000..26df042 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/config/AuthorizationServerConfig.java @@ -0,0 +1,138 @@ +package com.bnhz.oauth.config; + + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.OAuth2RequestFactory; +import org.springframework.security.oauth2.provider.approval.ApprovalStore; +import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore; +import org.springframework.security.oauth2.provider.approval.UserApprovalHandler; +import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; +import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices; +import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; +import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; + +import javax.annotation.Resource; +import javax.sql.DataSource; + +/** + * 授权服务器配置,配置客户端id,密钥和令牌的过期时间 + */ +@Configuration +@EnableAuthorizationServer +public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { + @Resource + private DataSource dataSource; + + @Resource + private AuthenticationManager authenticationManager; + + @Resource + private UserDetailsService userDetailsService; + + /** + * 用来配置令牌端点(Token Endpoint)的安全约束 + * @param security + * @throws Exception + */ + @Override + public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { + security.allowFormAuthenticationForClients() + .authenticationEntryPoint(new OAuth2AuthenticationEntryPoint()); +// security.allowFormAuthenticationForClients() +// .tokenKeyAccess("permitAll()") +// // 允许 /oauth/token_check端点的访问 +// .checkTokenAccess("permitAll()") +// .passwordEncoder(new PasswordEncoder() { +// @Override +// public String encode(CharSequence charSequence) { +// // 密码加密 +// return null; +// } +// +// @Override +// public boolean matches(CharSequence charSequence, String s) { +// // 密码校验 +// // return false; +// return true; +// } +// }) +// .allowFormAuthenticationForClients(); + } + + /** + * 用来配置客户端详情服务 + * @param clients + * @throws Exception + */ + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { + + clients.withClientDetails(getClientDetailsService()); + } + + /** + * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。 + * @param endpoints + * @throws Exception + */ + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + // 查询用户、授权、分组,可以被重写 + endpoints.userDetailsService(userDetailsService) + // 审批客户端的授权 + .userApprovalHandler(userApprovalHandler()) + // 授权审批 + .approvalStore(approvalStore()) + // 获取授权码 + .authorizationCodeServices(new JdbcAuthorizationCodeServices(dataSource)) + // 验证token + .authenticationManager(authenticationManager) + // 查询、保存、刷新token + .tokenStore(this.getJdbcTokenStore()); + } + + @Bean + public ApprovalStore approvalStore() { + return new JdbcApprovalStore(dataSource); + } + + @Bean + public UserApprovalHandler userApprovalHandler() { + return new SpeakerApprovalHandler(getClientDetailsService(), approvalStore(), oAuth2RequestFactory()); + } + + @Bean + public JdbcClientDetailsService getClientDetailsService() { + JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource); + jdbcClientDetailsService.setPasswordEncoder(passwordEncoder()); + return jdbcClientDetailsService; + } + + @Bean + public OAuth2RequestFactory oAuth2RequestFactory() { + return new DefaultOAuth2RequestFactory(getClientDetailsService()); + } + @Bean + public TokenStore getJdbcTokenStore(){ + TokenStore tokenStore = new JdbcTokenStore(dataSource); + return tokenStore; + } + + + + public BCryptPasswordEncoder passwordEncoder(){ + return new BCryptPasswordEncoder(); + } + +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/config/ResourceServerConfig.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/config/ResourceServerConfig.java new file mode 100644 index 0000000..2969049 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/config/ResourceServerConfig.java @@ -0,0 +1,48 @@ +package com.bnhz.oauth.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; + +import javax.sql.DataSource; + +@Configuration +@EnableResourceServer +public class ResourceServerConfig extends ResourceServerConfigurerAdapter { + + @Autowired + private DataSource dataSource; + + @Override + public void configure(ResourceServerSecurityConfigurer resources) throws Exception { + TokenStore tokenStore = jdbcTokenStore(); + OAuth2AuthenticationManager auth2AuthenticationManager= new OAuth2AuthenticationManager(); + resources.authenticationManager(auth2AuthenticationManager); + resources.resourceId("speaker-service").tokenStore(tokenStore).stateless(true); + } + + @Override + public void configure(HttpSecurity http) throws Exception { + // 限制资源服务器只接管匹配的资源 + http.requestMatchers().antMatchers("dueros") + .and() + //授权的请求 + .authorizeRequests() + .anyRequest().authenticated() + //关闭跨站请求防护 + .and() + .csrf().disable(); + } + + public TokenStore jdbcTokenStore(){ + TokenStore tokenStore = new JdbcTokenStore(dataSource); + return tokenStore; + } + +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/config/SpeakerApprovalHandler.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/config/SpeakerApprovalHandler.java new file mode 100644 index 0000000..86b1cf7 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/config/SpeakerApprovalHandler.java @@ -0,0 +1,83 @@ +package com.bnhz.oauth.config; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.common.util.OAuth2Utils; +import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.OAuth2RequestFactory; +import org.springframework.security.oauth2.provider.approval.Approval; +import org.springframework.security.oauth2.provider.approval.ApprovalStore; +import org.springframework.security.oauth2.provider.approval.ApprovalStoreUserApprovalHandler; +import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; + +import java.util.*; + +/** + * kerwincui + */ +public class SpeakerApprovalHandler extends ApprovalStoreUserApprovalHandler { + + private int approvalExpirySeconds = -1; + + @Autowired + private ApprovalStore approvalStore; + + public SpeakerApprovalHandler(JdbcClientDetailsService clientDetailsService, ApprovalStore approvalStore, OAuth2RequestFactory oAuth2RequestFactory) { + this.setApprovalStore(approvalStore); + this.setClientDetailsService(clientDetailsService); + this.setRequestFactory(oAuth2RequestFactory); + } + + @Override + public AuthorizationRequest updateAfterApproval(AuthorizationRequest authorizationRequest, Authentication userAuthentication) { + // 获取授权过的范围 + Set requestedScopes = authorizationRequest.getScope(); + Set approvedScopes = new HashSet(); + Set approvals = new HashSet(); + Date expiry = computeExpiry(); + + // 存储授权或拒绝的范围 + Map approvalParameters = authorizationRequest.getApprovalParameters(); + for (String requestedScope : requestedScopes) { + String approvalParameter = OAuth2Utils.SCOPE_PREFIX + requestedScope; + String value = approvalParameters.get(approvalParameter); + value = value == null ? "" : value.toLowerCase(); + if ("true".equals(value) || value.startsWith("approve")||value.equals("on")) { + approvedScopes.add(requestedScope); + approvals.add(new Approval(userAuthentication.getName(), authorizationRequest.getClientId(), + requestedScope, expiry, Approval.ApprovalStatus.APPROVED)); + } + else { + approvals.add(new Approval(userAuthentication.getName(), authorizationRequest.getClientId(), + requestedScope, expiry, Approval.ApprovalStatus.DENIED)); + } + } + approvalStore.addApprovals(approvals); + + boolean approved; + authorizationRequest.setScope(approvedScopes); + if (approvedScopes.isEmpty() && !requestedScopes.isEmpty()) { + approved = false; + } + else { + approved = true; + } + authorizationRequest.setApproved(approved); + return authorizationRequest; + } + + private Date computeExpiry() { + Calendar expiresAt = Calendar.getInstance(); + // 默认一个月 + if (approvalExpirySeconds == -1) { + expiresAt.add(Calendar.MONTH, 1); + } + else { + expiresAt.add(Calendar.SECOND, approvalExpirySeconds); + } + return expiresAt.getTime(); + } + +} + diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/ConfirmAccessController.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/ConfirmAccessController.java new file mode 100644 index 0000000..5b09fce --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/ConfirmAccessController.java @@ -0,0 +1,49 @@ +package com.bnhz.oauth.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.common.util.OAuth2Utils; +import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.approval.Approval; +import org.springframework.security.oauth2.provider.approval.ApprovalStore; +import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.SessionAttributes; + +import java.security.Principal; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * kerwincui + */ +@Controller +@SessionAttributes("authorizationRequest") +public class ConfirmAccessController { + @Autowired + private JdbcClientDetailsService clientDetailsService; + @Autowired + private ApprovalStore approvalStore; + + @RequestMapping("/oauth/confirm_access") + public String getAccessConfirmation(Map model, Principal principal ) { + AuthorizationRequest clientAuth = (AuthorizationRequest) model.remove("authorizationRequest"); + ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId()); + + Map scopes = new LinkedHashMap(); + for (String scope : clientAuth.getScope()) { + scopes.put(OAuth2Utils.SCOPE_PREFIX + scope, "false"); + } + for (Approval approval : approvalStore.getApprovals(principal.getName(), client.getClientId())) { + if (clientAuth.getScope().contains(approval.getScope())) { + scopes.put(OAuth2Utils.SCOPE_PREFIX + approval.getScope(), + approval.getStatus() == Approval.ApprovalStatus.APPROVED ? "true" : "false"); + } + } + model.put("auth_request", clientAuth); + model.put("client", client); + model.put("scopes", scopes); + return "oauth/access_confirmation"; + } +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/LoginController.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/LoginController.java new file mode 100644 index 0000000..2b9ac1e --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/LoginController.java @@ -0,0 +1,51 @@ +package com.bnhz.oauth.controller; + +import com.bnhz.framework.web.service.SysLoginService; +import com.bnhz.framework.web.service.TokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class LoginController { + + + @Autowired + private TokenStore tokenStore; + + @Autowired + private SysLoginService loginService; + + @Autowired + private TokenService tokenService; + + @RequestMapping("/oauth/login") + public String login() { + return "oauth/login"; + } + + @RequestMapping("/oauth/index") + public String index() { + return "oauth/index"; + } + + @GetMapping("/oauth/logout") + @ResponseBody + public String logout(@RequestHeader String Authorization) { + if (!Authorization.isEmpty()){ + String token=Authorization.split(" ")[1]; + OAuth2AccessToken auth2AccessToken = tokenStore.readAccessToken(token); + tokenStore.removeAccessToken(auth2AccessToken); + return "SUCCESS"; + }else{ + return "FAIL"; + } + + } + +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/OauthClientDetailsController.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/OauthClientDetailsController.java new file mode 100644 index 0000000..7ea51c5 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/OauthClientDetailsController.java @@ -0,0 +1,107 @@ +//package com.bnhz.oauth.controller; +// +//import com.bnhz.common.annotation.Log; +//import com.bnhz.common.core.controller.BaseController; +//import com.bnhz.common.core.domain.AjaxResult; +//import com.bnhz.common.core.page.TableDataInfo; +//import com.bnhz.common.enums.BusinessType; +//import com.bnhz.common.utils.poi.ExcelUtil; +//import com.bnhz.oauth.domain.OauthClientDetails; +//import com.bnhz.oauth.service.IOauthClientDetailsService; +//import io.swagger.annotations.Api; +//import io.swagger.annotations.ApiOperation; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.security.access.prepost.PreAuthorize; +//import org.springframework.web.bind.annotation.*; +// +//import javax.servlet.http.HttpServletResponse; +//import java.util.List; +// +///** +// * 云云对接Controller +// * +// * @author kerwincui +// * @date 2022-02-07 +// */ +//@Api(tags = "云云对接") +//@RestController +//@RequestMapping("/iot/clientDetails") +//public class OauthClientDetailsController extends BaseController +//{ +// @Autowired +// private IOauthClientDetailsService oauthClientDetailsService; +// +// /** +// * 查询云云对接列表 +// */ +// @ApiOperation("查询云云对接列表") +// @PreAuthorize("@ss.hasPermi('iot:clientDetails:list')") +// @GetMapping("/list") +// public TableDataInfo list(OauthClientDetails oauthClientDetails) +// { +// startPage(); +// List list = oauthClientDetailsService.selectOauthClientDetailsList(oauthClientDetails); +// return getDataTable(list); +// } +// +// /** +// * 导出云云对接列表 +// */ +// @ApiOperation("导出云云对接列表") +// @PreAuthorize("@ss.hasPermi('iot:clientDetails:export')") +// @Log(title = "云云对接", businessType = BusinessType.EXPORT) +// @PostMapping("/export") +// public void export(HttpServletResponse response, OauthClientDetails oauthClientDetails) +// { +// List list = oauthClientDetailsService.selectOauthClientDetailsList(oauthClientDetails); +// ExcelUtil util = new ExcelUtil(OauthClientDetails.class); +// util.exportExcel(response, list, "云云对接数据"); +// } +// +// /** +// * 获取云云对接详细信息 +// */ +// @ApiOperation("获取云云对接详细信息") +// @PreAuthorize("@ss.hasPermi('iot:clientDetails:query')") +// @GetMapping(value = "/{id}") +// public AjaxResult getInfo(@PathVariable("id") Long id) +// { +// return AjaxResult.success(oauthClientDetailsService.selectOauthClientDetailsById(id)); +// } +// +// /** +// * 新增云云对接 +// */ +// @ApiOperation("新增云云对接") +// @PreAuthorize("@ss.hasPermi('iot:clientDetails:add')") +// @Log(title = "云云对接", businessType = BusinessType.INSERT) +// @PostMapping +// public AjaxResult add(@RequestBody OauthClientDetails oauthClientDetails) +// { +// return oauthClientDetailsService.insertOauthClientDetails(oauthClientDetails); +// } +// +// /** +// * 修改云云对接 +// */ +// @ApiOperation("修改云云对接") +// @PreAuthorize("@ss.hasPermi('iot:clientDetails:edit')") +// @Log(title = "云云对接", businessType = BusinessType.UPDATE) +// @PutMapping +// public AjaxResult edit(@RequestBody OauthClientDetails oauthClientDetails) +// { +// return oauthClientDetailsService.updateOauthClientDetails(oauthClientDetails); +// } +// +// /** +// * 修改云云对接 +// */ +// @ApiOperation("删除云云对接") +// @PreAuthorize("@ss.hasPermi('iot:clientDetails:remove')") +// @Log(title = "云云对接", businessType = BusinessType.DELETE) +// @DeleteMapping("/{ids}") +// public AjaxResult remove(@PathVariable Long[] ids) +// { +// return toAjax(oauthClientDetailsService.deleteOauthClientDetailsByIds(ids)); +// } +//} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/OauthController.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/OauthController.java new file mode 100644 index 0000000..32ef265 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/controller/OauthController.java @@ -0,0 +1,253 @@ +package com.bnhz.oauth.controller; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.text.KeyValue; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.json.JsonUtils; +import com.bnhz.oauth.domain.OauthAccessToken; +import com.bnhz.oauth.domain.OauthApprovals; +import com.bnhz.oauth.domain.OauthClientDetails; +import com.bnhz.oauth.domain.OauthCode; +import com.bnhz.oauth.enums.OAuth2GrantTypeEnum; +import com.bnhz.oauth.service.IOauthApprovalsService; +import com.bnhz.oauth.service.IOauthClientDetailsService; +import com.bnhz.oauth.service.IOauthCodeService; +import com.bnhz.oauth.service.OauthAccessTokenService; +import com.bnhz.oauth.utils.HttpUtils; +import com.bnhz.oauth.utils.OAuth2Utils; +import com.bnhz.oauth.vo.OAuth2OpenAccessTokenRespVO; +import com.bnhz.oauth.vo.OAuth2OpenAuthorizeInfoRespVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.bnhz.common.core.domain.AjaxResult.success; +import static com.bnhz.common.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static com.bnhz.common.exception.ServiceExceptionUtil.exception0; +import static com.bnhz.common.utils.SecurityUtils.getUserId; +import static com.bnhz.common.utils.collection.CollectionUtils.convertList; + +/** + * @author fastb + * @version 1.0 + * @description: OAuth2.0 授权接口 + * @date 2024-03-20 11:29 + */ +@RestController +@RequestMapping("/oauth2") +@Slf4j +public class OauthController { + + @Resource + private IOauthClientDetailsService oauthClientDetailsService; + @Resource + private IOauthApprovalsService oAuthApproveService; + @Resource + private OauthAccessTokenService oauthAccessTokenService; + @Resource + private IOauthCodeService oauthCodeService; + + @GetMapping("/authorize") + public AjaxResult authorize(@RequestParam("clientId") String clientId) { + // 0. 校验用户已经登录。通过 Spring Security 实现 + + // 1. 获得 Client 客户端的信息 + OauthClientDetails oauthClientDetails = oauthClientDetailsService.validOAuthClientFromCache(clientId); + // 2. 获得用户已经授权的信息 + List approves = oAuthApproveService.getApproveList(getUserId(), clientId); + // 拼接返回 + return success(this.convert(oauthClientDetails, approves)); + } + + private OAuth2OpenAuthorizeInfoRespVO convert(OauthClientDetails oauthClientDetails, List approves) { + // 构建 scopes + List strings = StringUtils.str2List(oauthClientDetails.getScope(), ",", true, true); + List> scopes = new ArrayList<>(strings.size()); + Map approveMap = approves.stream().collect(Collectors.toMap(OauthApprovals::getScope, Function.identity())); + for (String scope : strings) { + OauthApprovals oauthApprovals = approveMap.get(scope); + scopes.add(new KeyValue<>(scope, oauthApprovals != null ? "true".equals(oauthApprovals.getStatus()) : false)); + } + // 拼接返回 + return new OAuth2OpenAuthorizeInfoRespVO( + new OAuth2OpenAuthorizeInfoRespVO.Client(oauthClientDetails.getClientId(), oauthClientDetails.getIcon()), scopes); + } + + @PostMapping("/authorize") + public AjaxResult authorize(@RequestParam("response_type") String responseType, + @RequestParam("client_id") String clientId, + @RequestParam(value = "scope", required = false) String scope, + @RequestParam("redirect_uri") String redirectUri, + @RequestParam(value = "auto_approve") Boolean autoApprove, + @RequestParam(value = "state", required = false) String state) throws IOException { + log.warn("oauth2.0认证"); + Map scopes = JsonUtils.parseObject(scope, Map.class); + scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap()); + // 0. 校验用户已经登录。通过 Spring Security 实现 + + // 1.1 校验 responseType 是否满足 code 或者 token 值 + OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType); + // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 + OauthClientDetails client = oauthClientDetailsService.validOAuthClientFromCache(clientId, null, + grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); + + // 2.1 假设 approved 为 null,说明是场景一 + if (Boolean.TRUE.equals(autoApprove)) { + // 如果无法自动授权通过,则返回空 url,前端不进行跳转 + if (!oAuthApproveService.checkForPreApproval(getUserId(), clientId, scopes.keySet())) { + return success(null); + } + } else { // 2.2 假设 approved 非 null,说明是场景二 + // 如果计算后不通过,则跳转一个错误链接 + if (!oAuthApproveService.updateAfterApproval(getUserId(), clientId, scopes)) { + return success(OAuth2Utils.buildUnsuccessfulRedirect(redirectUri, responseType, state, + "access_denied", "User denied access")); + } + } + + // 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向 + List approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue); + if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) { + String redirect = getAuthorizationCodeRedirect(getUserId(), client, approveScopes, redirectUri, state); + return success("授权成功", redirect); + } + return success(); + // 3.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向 +// return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state)); + } + + private String getAuthorizationCodeRedirect(Long userId, OauthClientDetails client, + List scopes, String redirectUri, String state) { + // 1. 创建 code 授权码 + String authorizationCode = generateCode(); + OauthCode oauthCode = new OauthCode(); + oauthCode.setCode(authorizationCode); + oauthCode.setUserId(userId); + oauthCodeService.insertOauthCode(oauthCode); +// String authorizationCode = oauthCodeService.grantAuthorizationCodeForCode(userId, client.getClientId(), scopes, +// redirectUri, state); + // 2. 拼接重定向的 URL + return OAuth2Utils.buildAuthorizationCodeRedirectUri(redirectUri, authorizationCode, state); + } + + private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) { + if (StrUtil.equals(responseType, "code")) { + return OAuth2GrantTypeEnum.AUTHORIZATION_CODE; + } + if (StrUtil.equalsAny(responseType, "token")) { + return OAuth2GrantTypeEnum.IMPLICIT; + } + throw exception0(BAD_REQUEST.getCode(), "response_type 参数值只允许 code 和 token"); + } + + @PostMapping("/token") + public ResponseEntity postAccessToken(HttpServletRequest request, + @RequestParam("grant_type") String grantType, + @RequestParam(value = "code", required = false) String code, // 授权码模式 + @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式 + @RequestParam(value = "state", required = false) String state, // 授权码模式 + @RequestParam(value = "username", required = false) String username, // 密码模式 + @RequestParam(value = "password", required = false) String password, // 密码模式 + @RequestParam(value = "scope", required = false) String scope, // 密码模式 + @RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式 +// log.error("小度请求token,入参:{},{},{},{},{},{},{},{}", grantType, code, redirectUri, state, username, password, scope, refreshToken); + List scopes = OAuth2Utils.buildScopes(scope); + // todo 小度传过来的参数重复了,这里先暂时处理一下 + if (grantType.contains(",")) { + grantType = grantType.substring(grantType.indexOf(",") + 1); + } + if (code.contains(",")) { + code = code.substring(code.indexOf(",") + 1); + } + if (redirectUri.contains(",")) { + redirectUri = redirectUri.substring(redirectUri.indexOf(",") + 1); + } + // 1.1 校验授权类型 + OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType); + if (grantTypeEnum == null) { + throw new ServiceException("未知授权类型:" + grantType + ";" + code + ";" + redirectUri); + } + if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) { + throw new ServiceException("Token 接口不支持 implicit 授权模式"); + } + + // 1.2 校验客户端 + String[] clientIdAndSecret = obtainBasicAuthorization(request); + OauthClientDetails client = oauthClientDetailsService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], + grantType, scopes, redirectUri); + + // 2. 根据授权模式,获取访问令牌 + OauthAccessToken oauthAccessToken; + switch (grantTypeEnum) { + case AUTHORIZATION_CODE: + oauthAccessToken = oauthAccessTokenService.grantAuthorizationCodeForAccessToken(client, code, redirectUri, state); + break; +// case PASSWORD: +// accessTokenDO = oauth2GrantService.grantPassword(username, password, client.getClientId(), scopes); +// break; +// case CLIENT_CREDENTIALS: +// accessTokenDO = oauth2GrantService.grantClientCredentials(client.getClientId(), scopes); +// break; +// case REFRESH_TOKEN: +// accessTokenDO = oauth2GrantService.grantRefreshToken(refreshToken, client.getClientId()); +// break; + default: + throw new IllegalArgumentException("未知授权类型:" + grantType); + } + Assert.notNull(oauthAccessToken, "访问令牌不能为空"); // 防御性检查 + OAuth2OpenAccessTokenRespVO oAuth2OpenAccessTokenRespVO = this.convertAccessToken(oauthAccessToken); + ResponseEntity response = getResponse(oAuth2OpenAccessTokenRespVO); +// log.error("小度请求token成功:{}", JSON.toJSONString(response)); + return response; + } + + private ResponseEntity getResponse(OAuth2OpenAccessTokenRespVO accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Cache-Control", "no-store"); + headers.set("Pragma", "no-cache"); + headers.set("Content-Type", "application/json;charset=UTF-8"); + return new ResponseEntity<>(accessToken, headers, HttpStatus.OK); + } + + private OAuth2OpenAccessTokenRespVO convertAccessToken(OauthAccessToken oauthAccessToken) { + OAuth2OpenAccessTokenRespVO respVO = new OAuth2OpenAccessTokenRespVO(); + respVO.setAccessToken(oauthAccessToken.getTokenId()); + respVO.setRefreshToken(oauthAccessToken.getRefreshToken()); + respVO.setTokenType("bearer"); + respVO.setExpiresIn(OAuth2Utils.getExpiresIn(oauthAccessToken.getExpiresTime())); +// respVO.setScope(OAuth2Utils.buildScopeStr(bean.getScopes())); + return respVO; + } + + private String[] obtainBasicAuthorization(HttpServletRequest request) { + String[] clientIdAndSecret = HttpUtils.obtainBasicAuthorization(request); + if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) { + throw exception0(BAD_REQUEST.getCode(), "client_id 或 client_secret 未正确传递"); + } + return clientIdAndSecret; + } + + private static String generateCode() { + return IdUtil.fastSimpleUUID(); + } + + +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthAccessToken.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthAccessToken.java new file mode 100644 index 0000000..8d74b89 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthAccessToken.java @@ -0,0 +1,39 @@ +package com.bnhz.oauth.domain; + +import lombok.*; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * @author fastb + * @date 2023-09-01 17:00 + */ +@Data +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class OauthAccessToken { + + private String tokenId; + + private String token; + + private String authenticationId; + + private String userName; + + private String clientId; + + private String authentication; + + private String refreshToken; + + private String openId; + + private Long userId; + + private LocalDateTime expiresTime; +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthApprovals.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthApprovals.java new file mode 100644 index 0000000..1080d2f --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthApprovals.java @@ -0,0 +1,42 @@ +package com.bnhz.oauth.domain; + +import com.bnhz.common.annotation.Excel; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 【请填写功能名称】对象 oauth_approvals + * + * @author kerwincui + * @date 2024-03-20 + */ +@Data +public class OauthApprovals +{ + private static final long serialVersionUID = 1L; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private String userid; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private String clientid; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private String scope; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private String status; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private LocalDateTime expiresat; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private LocalDateTime lastmodifiedat; +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthClientDetails.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthClientDetails.java new file mode 100644 index 0000000..7b53b3d --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthClientDetails.java @@ -0,0 +1,285 @@ +package com.bnhz.oauth.domain; + +import com.bnhz.common.annotation.Excel; +import com.bnhz.common.core.domain.BaseEntity; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 云云对接对象 oauth_client_details + * + * @author kerwincui + * @date 2022-02-07 + */ +@ApiModel(value = "OauthClientDetails", description = "云云对接对象 oauth_client_details") +public class OauthClientDetails extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** + * 主键编号 + */ + @ApiModelProperty("编号") + @Excel(name = "编号") + private Long id; + + /** 客户端ID */ + @ApiModelProperty("客户端ID") + @Excel(name = "客户端ID") + private String clientId; + + /** 资源 */ + @ApiModelProperty("资源") + @Excel(name = "资源") + private String resourceIds; + + /** 客户端秘钥 */ + @ApiModelProperty("客户端秘钥") + private String clientSecret; + + /** 权限范围 */ + @ApiModelProperty("权限范围") + @Excel(name = "权限范围") + private String scope; + + /** 授权模式 */ + @ApiModelProperty("授权模式") + @Excel(name = "授权模式") + private String authorizedGrantTypes; + + /** 回调地址 */ + @ApiModelProperty("回调地址") + @Excel(name = "回调地址") + private String webServerRedirectUri; + + /** 权限 */ + @ApiModelProperty("权限") + @Excel(name = "权限") + private String authorities; + + /** access token有效时间 */ + @ApiModelProperty("access token有效时间") + @Excel(name = "access token有效时间") + private Long accessTokenValidity; + + /** refresh token有效时间 */ + @ApiModelProperty("refresh token有效时间") + @Excel(name = "refresh token有效时间") + private Long refreshTokenValidity; + + /** 预留的字段 */ + @ApiModelProperty("预留的字段") + @Excel(name = "预留的字段") + private String additionalInformation; + + /** 自动授权 */ + @ApiModelProperty("自动授权") + @Excel(name = "自动授权") + private String autoapprove; + + /** 平台 */ + @ApiModelProperty("平台") + @Excel(name = "平台") + private Integer type; + + /** + * 启用状态 + */ + @ApiModelProperty("启用状态") + @Excel(name = "启用状态") + private Integer status; + + /** + * 图标 + */ + @ApiModelProperty("图标") + private String icon; + + /** + * 云技能id + */ + private String cloudSkillId; + + /** 租户id */ + private Long tenantId; + + /** 租户名称 */ + private String tenantName; + + public Long getTenantId() { + return tenantId; + } + + public void setTenantId(Long tenantId) { + this.tenantId = tenantId; + } + + public String getTenantName() { + return tenantName; + } + + public void setTenantName(String tenantName) { + this.tenantName = tenantName; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCloudSkillId() { + return cloudSkillId; + } + + public void setCloudSkillId(String cloudSkillId) { + this.cloudSkillId = cloudSkillId; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + public String getClientId() + { + return clientId; + } + public void setResourceIds(String resourceIds) + { + this.resourceIds = resourceIds; + } + + public String getResourceIds() + { + return resourceIds; + } + public void setClientSecret(String clientSecret) + { + this.clientSecret = clientSecret; + } + + public String getClientSecret() + { + return clientSecret; + } + public void setScope(String scope) + { + this.scope = scope; + } + + public String getScope() + { + return scope; + } + public void setAuthorizedGrantTypes(String authorizedGrantTypes) + { + this.authorizedGrantTypes = authorizedGrantTypes; + } + + public String getAuthorizedGrantTypes() + { + return authorizedGrantTypes; + } + public void setWebServerRedirectUri(String webServerRedirectUri) + { + this.webServerRedirectUri = webServerRedirectUri; + } + + public String getWebServerRedirectUri() + { + return webServerRedirectUri; + } + public void setAuthorities(String authorities) + { + this.authorities = authorities; + } + + public String getAuthorities() + { + return authorities; + } + public void setAccessTokenValidity(Long accessTokenValidity) + { + this.accessTokenValidity = accessTokenValidity; + } + + public Long getAccessTokenValidity() + { + return accessTokenValidity; + } + public void setRefreshTokenValidity(Long refreshTokenValidity) + { + this.refreshTokenValidity = refreshTokenValidity; + } + + public Long getRefreshTokenValidity() + { + return refreshTokenValidity; + } + public void setAdditionalInformation(String additionalInformation) + { + this.additionalInformation = additionalInformation; + } + + public String getAdditionalInformation() + { + return additionalInformation; + } + public void setAutoapprove(String autoapprove) + { + this.autoapprove = autoapprove; + } + + public String getAutoapprove() + { + return autoapprove; + } + public void setType(Integer type) + { + this.type = type; + } + + public Integer getType() + { + return type; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("clientId", getClientId()) + .append("resourceIds", getResourceIds()) + .append("clientSecret", getClientSecret()) + .append("scope", getScope()) + .append("authorizedGrantTypes", getAuthorizedGrantTypes()) + .append("webServerRedirectUri", getWebServerRedirectUri()) + .append("authorities", getAuthorities()) + .append("accessTokenValidity", getAccessTokenValidity()) + .append("refreshTokenValidity", getRefreshTokenValidity()) + .append("additionalInformation", getAdditionalInformation()) + .append("autoapprove", getAutoapprove()) + .append("type", getType()) + .toString(); + } +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthCode.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthCode.java new file mode 100644 index 0000000..b35304a --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/domain/OauthCode.java @@ -0,0 +1,66 @@ +package com.bnhz.oauth.domain; + +import com.bnhz.common.annotation.Excel; +import com.bnhz.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * 【请填写功能名称】对象 oauth_code + * + * @author kerwincui + * @date 2024-03-20 + */ +public class OauthCode extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private String code; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private String authentication; + + /** $column.columnComment */ + @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()") + private Long userId; + + public void setCode(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } + public void setAuthentication(String authentication) + { + this.authentication = authentication; + } + + public String getAuthentication() + { + return authentication; + } + public void setUserId(Long userId) + { + this.userId = userId; + } + + public Long getUserId() + { + return userId; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("code", getCode()) + .append("authentication", getAuthentication()) + .append("userId", getUserId()) + .toString(); + } +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/enums/OAuth2GrantTypeEnum.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/enums/OAuth2GrantTypeEnum.java new file mode 100644 index 0000000..a651e7f --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/enums/OAuth2GrantTypeEnum.java @@ -0,0 +1,29 @@ +package com.bnhz.oauth.enums; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * OAuth2 授权类型(模式)的枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum OAuth2GrantTypeEnum { + + PASSWORD("password"), // 密码模式 + AUTHORIZATION_CODE("authorization_code"), // 授权码模式 + IMPLICIT("implicit"), // 简化模式 + CLIENT_CREDENTIALS("client_credentials"), // 客户端模式 + REFRESH_TOKEN("refresh_token"), // 刷新模式 + ; + + private final String grantType; + + public static OAuth2GrantTypeEnum getByGranType(String grantType) { + return ArrayUtil.firstMatch(o -> o.getGrantType().equals(grantType), values()); + } + +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthAccessTokenMapper.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthAccessTokenMapper.java new file mode 100644 index 0000000..28c92ff --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthAccessTokenMapper.java @@ -0,0 +1,22 @@ +package com.bnhz.oauth.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bnhz.oauth.domain.OauthAccessToken; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface OauthAccessTokenMapper extends BaseMapper { + + String selectUserNameByTokenId(String tokenId); + + OauthAccessToken selectByTokenId(String tokenId); + + void updateOpenIdByTokenId(@Param("tokenId") String tokenId,@Param("openUid") String openUid); + + OauthAccessToken selectByUserName(String userName); + + void insertOauthAccessToken(OauthAccessToken oauthAccessToken); + + void deleteByUserId(Long userId); +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthApprovalsMapper.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthApprovalsMapper.java new file mode 100644 index 0000000..31e219f --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthApprovalsMapper.java @@ -0,0 +1,67 @@ +package com.bnhz.oauth.mapper; + +import com.bnhz.oauth.domain.OauthApprovals; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 【请填写功能名称】Mapper接口 + * + * @author kerwincui + * @date 2024-03-20 + */ +public interface OauthApprovalsMapper +{ + /** + * 查询【请填写功能名称】 + * + * @param userid 【请填写功能名称】主键 + * @return 【请填写功能名称】 + */ + public OauthApprovals selectOauthApprovalsByUserid(String userid); + + /** + * 查询【请填写功能名称】列表 + * + * @param oauthApprovals 【请填写功能名称】 + * @return 【请填写功能名称】集合 + */ + public List selectOauthApprovalsList(OauthApprovals oauthApprovals); + + /** + * 新增【请填写功能名称】 + * + * @param oauthApprovals 【请填写功能名称】 + * @return 结果 + */ + public int insertOauthApprovals(OauthApprovals oauthApprovals); + + /** + * 修改【请填写功能名称】 + * + * @param oauthApprovals 【请填写功能名称】 + * @return 结果 + */ + public int updateOauthApprovals(OauthApprovals oauthApprovals); + + /** + * 删除【请填写功能名称】 + * + * @param userid 【请填写功能名称】主键 + * @return 结果 + */ + public int deleteOauthApprovalsByUserid(String userid); + + /** + * 批量删除【请填写功能名称】 + * + * @param userids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteOauthApprovalsByUserids(String[] userids); + + int update(OauthApprovals oauthApprovals); + + List selectListByUserIdAndClientId(@Param("userId") Long userId, @Param("clientId") String clientId); +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthClientDetailsMapper.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthClientDetailsMapper.java new file mode 100644 index 0000000..451e45b --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthClientDetailsMapper.java @@ -0,0 +1,74 @@ +package com.bnhz.oauth.mapper; + +import com.bnhz.oauth.domain.OauthClientDetails; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 云云对接Mapper接口 + * + * @author kerwincui + * @date 2022-02-07 + */ +@Repository +public interface OauthClientDetailsMapper +{ + /** + * 查询云云对接 + * + * @param id 云云对接主键 + * @return 云云对接 + */ + public OauthClientDetails selectOauthClientDetailsById(Long id); + + /** + * 查询云云对接列表 + * + * @param oauthClientDetails 云云对接 + * @return 云云对接集合 + */ + public List selectOauthClientDetailsList(OauthClientDetails oauthClientDetails); + + /** + * 新增云云对接 + * + * @param oauthClientDetails 云云对接 + * @return 结果 + */ + public int insertOauthClientDetails(OauthClientDetails oauthClientDetails); + + /** + * 修改云云对接 + * + * @param oauthClientDetails 云云对接 + * @return 结果 + */ + public int updateOauthClientDetails(OauthClientDetails oauthClientDetails); + + /** + * 删除云云对接 + * + * @param clientId 云云对接主键 + * @return 结果 + */ + public int deleteOauthClientDetailsByClientId(String clientId); + + /** + * 批量删除云云对接 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteOauthClientDetailsByIds(Long[] ids); + + /** + * 通过授权平台查询配置 + * @param type 授权平台类型 + * @return + */ + OauthClientDetails selectOauthClientDetailsByType(@Param("type") Integer type, @Param("tenantId") Long tenantId); + + OauthClientDetails selectOauthClientDetailsByClientId(String clientId); +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthCodeMapper.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthCodeMapper.java new file mode 100644 index 0000000..2440456 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/mapper/OauthCodeMapper.java @@ -0,0 +1,62 @@ +package com.bnhz.oauth.mapper; + +import com.bnhz.oauth.domain.OauthCode; + +import java.util.List; + +/** + * 【请填写功能名称】Mapper接口 + * + * @author kerwincui + * @date 2024-03-20 + */ +public interface OauthCodeMapper +{ + /** + * 查询【请填写功能名称】 + * + * @param code 【请填写功能名称】主键 + * @return 【请填写功能名称】 + */ + public OauthCode selectOauthCodeByCode(String code); + + /** + * 查询【请填写功能名称】列表 + * + * @param oauthCode 【请填写功能名称】 + * @return 【请填写功能名称】集合 + */ + public List selectOauthCodeList(OauthCode oauthCode); + + /** + * 新增【请填写功能名称】 + * + * @param oauthCode 【请填写功能名称】 + * @return 结果 + */ + public int insertOauthCode(OauthCode oauthCode); + + /** + * 修改【请填写功能名称】 + * + * @param oauthCode 【请填写功能名称】 + * @return 结果 + */ + public int updateOauthCode(OauthCode oauthCode); + + /** + * 删除【请填写功能名称】 + * + * @param code 【请填写功能名称】主键 + * @return 结果 + */ + public int deleteOauthCodeByCode(String code); + + /** + * 批量删除【请填写功能名称】 + * + * @param codes 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteOauthCodeByCodes(String[] codes); +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/IOauthApprovalsService.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/IOauthApprovalsService.java new file mode 100644 index 0000000..be78f88 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/IOauthApprovalsService.java @@ -0,0 +1,70 @@ +package com.bnhz.oauth.service; + +import com.bnhz.oauth.domain.OauthApprovals; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 【请填写功能名称】Service接口 + * + * @author kerwincui + * @date 2024-03-20 + */ +public interface IOauthApprovalsService +{ + /** + * 查询【请填写功能名称】 + * + * @param userid 【请填写功能名称】主键 + * @return 【请填写功能名称】 + */ + public OauthApprovals selectOauthApprovalsByUserid(String userid); + + /** + * 查询【请填写功能名称】列表 + * + * @param oauthApprovals 【请填写功能名称】 + * @return 【请填写功能名称】集合 + */ + public List selectOauthApprovalsList(OauthApprovals oauthApprovals); + + /** + * 新增【请填写功能名称】 + * + * @param oauthApprovals 【请填写功能名称】 + * @return 结果 + */ + public int insertOauthApprovals(OauthApprovals oauthApprovals); + + /** + * 修改【请填写功能名称】 + * + * @param oauthApprovals 【请填写功能名称】 + * @return 结果 + */ + public int updateOauthApprovals(OauthApprovals oauthApprovals); + + /** + * 批量删除【请填写功能名称】 + * + * @param userids 需要删除的【请填写功能名称】主键集合 + * @return 结果 + */ + public int deleteOauthApprovalsByUserids(String[] userids); + + /** + * 删除【请填写功能名称】信息 + * + * @param userid 【请填写功能名称】主键 + * @return 结果 + */ + public int deleteOauthApprovalsByUserid(String userid); + + boolean checkForPreApproval(Long userId, String clientId, Set requestedScopes); + + boolean updateAfterApproval(Long userId, String clientId, Map scopes); + + List getApproveList(Long userId, String clientId); +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/IOauthClientDetailsService.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/IOauthClientDetailsService.java new file mode 100644 index 0000000..b07e446 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/IOauthClientDetailsService.java @@ -0,0 +1,70 @@ +package com.bnhz.oauth.service; + +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.oauth.domain.OauthClientDetails; + +import java.util.Collection; +import java.util.List; + +/** + * 云云对接Service接口 + * + * @author kerwincui + * @date 2022-02-07 + */ +public interface IOauthClientDetailsService +{ + /** + * 查询云云对接 + * + * @param id 云云对接主键 + * @return 云云对接 + */ + public OauthClientDetails selectOauthClientDetailsById(Long id); + + /** + * 查询云云对接列表 + * + * @param oauthClientDetails 云云对接 + * @return 云云对接集合 + */ + public List selectOauthClientDetailsList(OauthClientDetails oauthClientDetails); + + /** + * 新增云云对接 + * + * @param oauthClientDetails 云云对接 + * @return 结果 + */ + public AjaxResult insertOauthClientDetails(OauthClientDetails oauthClientDetails); + + /** + * 修改云云对接 + * + * @param oauthClientDetails 云云对接 + * @return 结果 + */ + public AjaxResult updateOauthClientDetails(OauthClientDetails oauthClientDetails); + + /** + * 批量删除云云对接 + * + * @param ids 需要删除的云云对接主键集合 + * @return 结果 + */ + public int deleteOauthClientDetailsByIds(Long[] ids); + + /** + * 删除云云对接信息 + * + * @param clientId 云云对接主键 + * @return 结果 + */ + public int deleteOauthClientDetailsByClientId(String clientId); + + default OauthClientDetails validOAuthClientFromCache(String clientId) { + return validOAuthClientFromCache(clientId, null, null, null, null); + } + + OauthClientDetails validOAuthClientFromCache(String clientId, String clientSecret, String grantType, Collection strings, String redirectUri); +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/IOauthCodeService.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/IOauthCodeService.java new file mode 100644 index 0000000..faa9a2b --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/IOauthCodeService.java @@ -0,0 +1,64 @@ +package com.bnhz.oauth.service; + +import com.bnhz.oauth.domain.OauthCode; + +import java.util.List; + +/** + * 【请填写功能名称】Service接口 + * + * @author kerwincui + * @date 2024-03-20 + */ +public interface IOauthCodeService +{ + /** + * 查询【请填写功能名称】 + * + * @param code 【请填写功能名称】主键 + * @return 【请填写功能名称】 + */ + public OauthCode selectOauthCodeByCode(String code); + + /** + * 查询【请填写功能名称】列表 + * + * @param oauthCode 【请填写功能名称】 + * @return 【请填写功能名称】集合 + */ + public List selectOauthCodeList(OauthCode oauthCode); + + /** + * 新增【请填写功能名称】 + * + * @param oauthCode 【请填写功能名称】 + * @return 结果 + */ + public int insertOauthCode(OauthCode oauthCode); + + /** + * 修改【请填写功能名称】 + * + * @param oauthCode 【请填写功能名称】 + * @return 结果 + */ + public int updateOauthCode(OauthCode oauthCode); + + /** + * 批量删除【请填写功能名称】 + * + * @param codes 需要删除的【请填写功能名称】主键集合 + * @return 结果 + */ + public int deleteOauthCodeByCodes(String[] codes); + + /** + * 删除【请填写功能名称】信息 + * + * @param code 【请填写功能名称】主键 + * @return 结果 + */ + public int deleteOauthCodeByCode(String code); + + OauthCode consumeAuthorizationCode(String code); +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/OauthAccessTokenService.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/OauthAccessTokenService.java new file mode 100644 index 0000000..3348443 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/OauthAccessTokenService.java @@ -0,0 +1,21 @@ +package com.bnhz.oauth.service; + +import com.bnhz.oauth.domain.OauthAccessToken; +import com.bnhz.oauth.domain.OauthClientDetails; + +/** + * @author fastb + * @date 2023-09-01 17:20 + */ +public interface OauthAccessTokenService { + + String selectUserNameByTokenId(String token); + + OauthAccessToken selectByTokenId(String tokenId); + + void updateOpenIdByTokenId(String tokenId, String openUid); + + OauthAccessToken selectByUserName(String userName); + + OauthAccessToken grantAuthorizationCodeForAccessToken(OauthClientDetails client, String code, String redirectUri, String state); +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthAccessTokenServiceImpl.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthAccessTokenServiceImpl.java new file mode 100644 index 0000000..b8c1e96 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthAccessTokenServiceImpl.java @@ -0,0 +1,68 @@ +package com.bnhz.oauth.service.impl; + +import cn.hutool.core.util.IdUtil; +import com.bnhz.oauth.domain.OauthAccessToken; +import com.bnhz.oauth.domain.OauthClientDetails; +import com.bnhz.oauth.domain.OauthCode; +import com.bnhz.oauth.mapper.OauthAccessTokenMapper; +import com.bnhz.oauth.service.IOauthClientDetailsService; +import com.bnhz.oauth.service.IOauthCodeService; +import com.bnhz.oauth.service.OauthAccessTokenService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +/** + * @author fastb + * @date 2023-09-01 17:20 + */ +@Service +public class OauthAccessTokenServiceImpl implements OauthAccessTokenService { + + @Resource + private OauthAccessTokenMapper oauthAccessTokenMapper; + @Resource + private IOauthCodeService oauthCodeService; + @Resource + private IOauthClientDetailsService oauthClientDetailsService; + + @Override + public String selectUserNameByTokenId(String tokenId) { + return oauthAccessTokenMapper.selectUserNameByTokenId(tokenId); + } + + @Override + public OauthAccessToken selectByTokenId(String tokenId) { + return oauthAccessTokenMapper.selectByTokenId(tokenId); + } + + @Override + public void updateOpenIdByTokenId(String tokenId, String openUid) { + oauthAccessTokenMapper.updateOpenIdByTokenId(tokenId, openUid); + } + + @Override + public OauthAccessToken selectByUserName(String userName) { + return oauthAccessTokenMapper.selectByUserName(userName); + } + + @Override + public OauthAccessToken grantAuthorizationCodeForAccessToken(OauthClientDetails client, String code, String redirectUri, String state) { + OauthCode oauthCode = oauthCodeService.consumeAuthorizationCode(code); + oauthAccessTokenMapper.deleteByUserId(oauthCode.getUserId()); + OauthAccessToken oauthAccessToken = new OauthAccessToken(); + oauthAccessToken.setTokenId(generateRefreshToken()); + oauthAccessToken.setClientId(client.getClientId()); + oauthAccessToken.setUserId(oauthCode.getUserId()); + oauthAccessToken.setRefreshToken(generateRefreshToken()); + oauthAccessToken.setExpiresTime(LocalDateTime.now().plusSeconds(client.getAccessTokenValidity())); + oauthAccessTokenMapper.insertOauthAccessToken(oauthAccessToken); + return oauthAccessToken; + } + + private static String generateRefreshToken() { + return IdUtil.fastSimpleUUID(); + } + +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthApprovalsServiceImpl.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthApprovalsServiceImpl.java new file mode 100644 index 0000000..d25b7bd --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthApprovalsServiceImpl.java @@ -0,0 +1,171 @@ +package com.bnhz.oauth.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.bnhz.common.utils.StringUtils; +import com.bnhz.common.utils.date.DateUtils; +import com.bnhz.oauth.domain.OauthApprovals; +import com.bnhz.oauth.domain.OauthClientDetails; +import com.bnhz.oauth.mapper.OauthApprovalsMapper; +import com.bnhz.oauth.service.IOauthApprovalsService; +import com.bnhz.oauth.service.IOauthClientDetailsService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 【请填写功能名称】Service业务层处理 + * + * @author kerwincui + * @date 2024-03-20 + */ +@Service +public class OauthApprovalsServiceImpl implements IOauthApprovalsService +{ + /** + * 批准的过期时间,默认 30 天 + */ + private static final Integer TIMEOUT = 30 * 24 * 60 * 60; // 单位:秒 + @Resource + private OauthApprovalsMapper oauthApprovalsMapper; + @Resource + private IOauthClientDetailsService oauthClientDetailsService; + + /** + * 查询【请填写功能名称】 + * + * @param userid 【请填写功能名称】主键 + * @return 【请填写功能名称】 + */ + @Override + public OauthApprovals selectOauthApprovalsByUserid(String userid) + { + return oauthApprovalsMapper.selectOauthApprovalsByUserid(userid); + } + + /** + * 查询【请填写功能名称】列表 + * + * @param oauthApprovals 【请填写功能名称】 + * @return 【请填写功能名称】 + */ + @Override + public List selectOauthApprovalsList(OauthApprovals oauthApprovals) + { + return oauthApprovalsMapper.selectOauthApprovalsList(oauthApprovals); + } + + /** + * 新增【请填写功能名称】 + * + * @param oauthApprovals 【请填写功能名称】 + * @return 结果 + */ + @Override + public int insertOauthApprovals(OauthApprovals oauthApprovals) + { + return oauthApprovalsMapper.insertOauthApprovals(oauthApprovals); + } + + /** + * 修改【请填写功能名称】 + * + * @param oauthApprovals 【请填写功能名称】 + * @return 结果 + */ + @Override + public int updateOauthApprovals(OauthApprovals oauthApprovals) + { + return oauthApprovalsMapper.updateOauthApprovals(oauthApprovals); + } + + /** + * 批量删除【请填写功能名称】 + * + * @param userids 需要删除的【请填写功能名称】主键 + * @return 结果 + */ + @Override + public int deleteOauthApprovalsByUserids(String[] userids) + { + return oauthApprovalsMapper.deleteOauthApprovalsByUserids(userids); + } + + /** + * 删除【请填写功能名称】信息 + * + * @param userid 【请填写功能名称】主键 + * @return 结果 + */ + @Override + public int deleteOauthApprovalsByUserid(String userid) + { + return oauthApprovalsMapper.deleteOauthApprovalsByUserid(userid); + } + + @Override + public boolean checkForPreApproval(Long userId, String clientId, Set requestedScopes) { + OauthClientDetails oauthClientDetails = oauthClientDetailsService.validOAuthClientFromCache(clientId); + Assert.notNull(oauthClientDetails, "客户端不能为空"); // 防御性编程 + List strings = StringUtils.str2List(oauthClientDetails.getScope(), ",", true, true); + if (CollUtil.containsAll(strings, requestedScopes)) { + // gh-877 - if all scopes are auto approved, approvals still need to be added to the approval store. + LocalDateTime expireTime = LocalDateTime.now().plusSeconds(TIMEOUT); + for (String scope : requestedScopes) { + saveApprove(userId, clientId, scope, true, expireTime); + } + return true; + } + + // 第二步,算上用户已经批准的授权。如果 scopes 都包含,则返回 true + List approvalsList = this.getApproveList(userId, clientId); + Set scopes = approvalsList.stream().filter(a -> "true".equals(a.getStatus())).map(OauthApprovals::getScope).collect(Collectors.toSet()); + return CollUtil.containsAll(scopes, requestedScopes); + } + + @Override + public boolean updateAfterApproval(Long userId, String clientId, Map requestedScopes) { + // 如果 requestedScopes 为空,说明没有要求,则返回 true 通过 + if (CollUtil.isEmpty(requestedScopes)) { + return true; + } + + // 更新批准的信息 + boolean success = false; // 需要至少有一个同意 + LocalDateTime expireTime = LocalDateTime.now().plusSeconds(TIMEOUT); + for (Map.Entry entry : requestedScopes.entrySet()) { + if (entry.getValue()) { + success = true; + } + saveApprove(userId, clientId, entry.getKey(), entry.getValue(), expireTime); + } + return success; + } + + public List getApproveList(Long userId, String clientId) { + List approvalsList = oauthApprovalsMapper.selectListByUserIdAndClientId( + userId, clientId); + approvalsList.removeIf(o -> DateUtils.isExpired(o.getExpiresat())); + return approvalsList; + } + + private void saveApprove(Long userId, String clientId, String scope, boolean b, LocalDateTime expireTime) { + // 先更新 + OauthApprovals oauthApprovals = new OauthApprovals(); + oauthApprovals.setUserid(userId.toString()); + oauthApprovals.setClientid(clientId); + oauthApprovals.setScope(scope); + oauthApprovals.setStatus(String.valueOf(b)); + oauthApprovals.setExpiresat(expireTime); + if (oauthApprovalsMapper.update(oauthApprovals) == 1) { + return; + } + // 失败,则说明不存在,进行更新 + oauthApprovalsMapper.insertOauthApprovals(oauthApprovals); + } +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthClientDetailsServiceImpl.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthClientDetailsServiceImpl.java new file mode 100644 index 0000000..27c34b3 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthClientDetailsServiceImpl.java @@ -0,0 +1,158 @@ +package com.bnhz.oauth.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.bnhz.common.core.domain.AjaxResult; +import com.bnhz.common.core.domain.entity.SysUser; +import com.bnhz.common.exception.ServiceException; +import com.bnhz.oauth.domain.OauthClientDetails; +import com.bnhz.oauth.mapper.OauthClientDetailsMapper; +import com.bnhz.oauth.service.IOauthClientDetailsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static com.bnhz.common.utils.SecurityUtils.getLoginUser; + +/** + * 云云对接Service业务层处理 + * + * @author kerwincui + * @date 2022-02-07 + */ +@Service +public class OauthClientDetailsServiceImpl implements IOauthClientDetailsService +{ + @Autowired + private OauthClientDetailsMapper oauthClientDetailsMapper; + + /** + * 查询云云对接 + * + * @param id 云云对接主键 + * @return 云云对接 + */ + @Override + public OauthClientDetails selectOauthClientDetailsById(Long id) + { + return oauthClientDetailsMapper.selectOauthClientDetailsById(id); + } + + /** + * 查询云云对接列表 + * + * @param oauthClientDetails 云云对接 + * @return 云云对接 + */ + @Override + public List selectOauthClientDetailsList(OauthClientDetails oauthClientDetails) + { + // 查询所属机构 + SysUser user = getLoginUser().getUser(); + oauthClientDetails.setTenantId(user.getDept().getDeptUserId()); + return oauthClientDetailsMapper.selectOauthClientDetailsList(oauthClientDetails); + } + + /** + * 新增云云对接 + * + * @param oauthClientDetails 云云对接 + * @return 结果 + */ + @Override + public AjaxResult insertOauthClientDetails(OauthClientDetails oauthClientDetails) + { + SysUser user = getLoginUser().getUser(); + if (null == user.getDept() || null == user.getDept().getDeptUserId()) { + throw new ServiceException("只允许租户配置"); + } + oauthClientDetails.setTenantId(user.getDept().getDeptUserId()); + oauthClientDetails.setTenantName(user.getDept().getDeptUserName()); + OauthClientDetails oauthClientDetails1 = oauthClientDetailsMapper.selectOauthClientDetailsByType(oauthClientDetails.getType(), oauthClientDetails.getTenantId()); + if (oauthClientDetails1 != null) { + return AjaxResult.error("同一个授权平台只能配置一条信息,请勿重复配置"); + } + OauthClientDetails oauthClientDetails2 = oauthClientDetailsMapper.selectOauthClientDetailsByClientId(oauthClientDetails.getClientId()); + if (oauthClientDetails2 != null) { + return AjaxResult.error("客户端id:" + oauthClientDetails.getClientId() + "已存在"); + } + return oauthClientDetailsMapper.insertOauthClientDetails(oauthClientDetails) > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 修改云云对接 + * + * @param oauthClientDetails 云云对接 + * @return 结果 + */ + @Override + public AjaxResult updateOauthClientDetails(OauthClientDetails oauthClientDetails) + { + OauthClientDetails oauthClientDetails1 = oauthClientDetailsMapper.selectOauthClientDetailsByClientId(oauthClientDetails.getClientId()); + if (oauthClientDetails1 != null && !Objects.equals(oauthClientDetails1.getId(), oauthClientDetails.getId())) { + return AjaxResult.error("客户端id:" + oauthClientDetails.getClientId() + "已存在"); + } + return oauthClientDetailsMapper.updateOauthClientDetails(oauthClientDetails) > 0 ? AjaxResult.success() : AjaxResult.error(); + } + + /** + * 批量删除云云对接 + * + * @param ids 需要删除的云云对接主键 + * @return 结果 + */ + @Override + public int deleteOauthClientDetailsByIds(Long[] ids) + { + return oauthClientDetailsMapper.deleteOauthClientDetailsByIds(ids); + } + + /** + * 删除云云对接信息 + * + * @param clientId 云云对接主键 + * @return 结果 + */ + @Override + public int deleteOauthClientDetailsByClientId(String clientId) + { + return oauthClientDetailsMapper.deleteOauthClientDetailsByClientId(clientId); + } + + @Override + public OauthClientDetails validOAuthClientFromCache(String clientId, String clientSecret, String grantType, Collection scopes, String redirectUri) { + // 校验客户端存在、且开启 + OauthClientDetails client = this.getOAuth2ClientFromCache(clientId); + if (client == null) { + throw new ServiceException("OAuth2 客户端不存在"); + } + if (0 != client.getStatus()) { + throw new ServiceException("OAuth2 客户端已禁用"); + } + + // 校验客户端密钥 + if (StrUtil.isNotEmpty(clientSecret) && ObjectUtil.notEqual(client.getClientSecret(), clientSecret)) { + throw new ServiceException("无效 client_secret"); + } + // 校验授权方式 +// if (StrUtil.isNotEmpty(grantType) && !StringUtils.contains(client.getAuthorizedGrantTypes(), grantType)) { +// throw new ServiceException("不支持该授权类型"); +// } + // 校验授权范围 +// if (CollUtil.isNotEmpty(scopes) && !CollUtil.containsAll(client.getScope(), scopes)) { +// throw new ServiceException("授权范围过大"); +// } + // 校验回调地址 + if (StrUtil.isNotEmpty(redirectUri) && !redirectUri.equals(client.getWebServerRedirectUri())) { + throw new ServiceException("无效 redirect_uri:" + redirectUri); + } + return client; + } + + private OauthClientDetails getOAuth2ClientFromCache(String clientId) { + return oauthClientDetailsMapper.selectOauthClientDetailsByClientId(clientId); + } +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthCodeServiceImpl.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthCodeServiceImpl.java new file mode 100644 index 0000000..ae561d5 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/service/impl/OauthCodeServiceImpl.java @@ -0,0 +1,108 @@ +package com.bnhz.oauth.service.impl; + +import com.bnhz.common.exception.ServiceException; +import com.bnhz.oauth.domain.OauthCode; +import com.bnhz.oauth.mapper.OauthCodeMapper; +import com.bnhz.oauth.service.IOauthCodeService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 【请填写功能名称】Service业务层处理 + * + * @author kerwincui + * @date 2024-03-20 + */ +@Service +public class OauthCodeServiceImpl implements IOauthCodeService +{ + @Resource + private OauthCodeMapper oauthCodeMapper; + + /** + * 查询【请填写功能名称】 + * + * @param code 【请填写功能名称】主键 + * @return 【请填写功能名称】 + */ + @Override + public OauthCode selectOauthCodeByCode(String code) + { + return oauthCodeMapper.selectOauthCodeByCode(code); + } + + /** + * 查询【请填写功能名称】列表 + * + * @param oauthCode 【请填写功能名称】 + * @return 【请填写功能名称】 + */ + @Override + public List selectOauthCodeList(OauthCode oauthCode) + { + return oauthCodeMapper.selectOauthCodeList(oauthCode); + } + + /** + * 新增【请填写功能名称】 + * + * @param oauthCode 【请填写功能名称】 + * @return 结果 + */ + @Override + public int insertOauthCode(OauthCode oauthCode) + { + return oauthCodeMapper.insertOauthCode(oauthCode); + } + + /** + * 修改【请填写功能名称】 + * + * @param oauthCode 【请填写功能名称】 + * @return 结果 + */ + @Override + public int updateOauthCode(OauthCode oauthCode) + { + return oauthCodeMapper.updateOauthCode(oauthCode); + } + + /** + * 批量删除【请填写功能名称】 + * + * @param codes 需要删除的【请填写功能名称】主键 + * @return 结果 + */ + @Override + public int deleteOauthCodeByCodes(String[] codes) + { + return oauthCodeMapper.deleteOauthCodeByCodes(codes); + } + + /** + * 删除【请填写功能名称】信息 + * + * @param code 【请填写功能名称】主键 + * @return 结果 + */ + @Override + public int deleteOauthCodeByCode(String code) + { + return oauthCodeMapper.deleteOauthCodeByCode(code); + } + + @Override + public OauthCode consumeAuthorizationCode(String code) { + OauthCode oauthCode = this.selectOauthCodeByCode(code); + if (oauthCode == null) { + throw new ServiceException("code 不存在"); + } +// if (DateUtils.isExpired(codeDO.getExpiresTime())) { +// throw exception(OAUTH2_CODE_EXPIRE); +// } + this.deleteOauthCodeByCode(code); + return oauthCode; + } +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/utils/HttpUtils.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/utils/HttpUtils.java new file mode 100644 index 0000000..ce1ecd3 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/utils/HttpUtils.java @@ -0,0 +1,126 @@ +package com.bnhz.oauth.utils; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.map.TableMap; +import cn.hutool.core.net.url.UrlBuilder; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.http.HttpServletRequest; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.Map; + +/** + * HTTP 工具类 + * + * @author 芋道源码 + */ +public class HttpUtils { + + @SuppressWarnings("unchecked") + public static String replaceUrlQuery(String url, String key, String value) { + UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); + // 先移除 + TableMap query = (TableMap) + ReflectUtil.getFieldValue(builder.getQuery(), "query"); + query.remove(key); + // 后添加 + builder.addQuery(key, value); + return builder.build(); + } + + private String append(String base, Map query, boolean fragment) { + return append(base, query, null, fragment); + } + + /** + * 拼接 URL + * + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法 + * + * @param base 基础 URL + * @param query 查询参数 + * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射 + * @param fragment URL 的 fragment,即拼接到 # 中 + * @return 拼接后的 URL + */ + public static String append(String base, Map query, Map keys, boolean fragment) { + UriComponentsBuilder template = UriComponentsBuilder.newInstance(); + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base); + URI redirectUri; + try { + // assume it's encoded to start with (if it came in over the wire) + redirectUri = builder.build(true).toUri(); + } catch (Exception e) { + // ... but allow client registrations to contain hard-coded non-encoded values + redirectUri = builder.build().toUri(); + builder = UriComponentsBuilder.fromUri(redirectUri); + } + template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost()) + .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath()); + + if (fragment) { + StringBuilder values = new StringBuilder(); + if (redirectUri.getFragment() != null) { + String append = redirectUri.getFragment(); + values.append(append); + } + for (String key : query.keySet()) { + if (values.length() > 0) { + values.append("&"); + } + String name = key; + if (keys != null && keys.containsKey(key)) { + name = keys.get(key); + } + values.append(name).append("={").append(key).append("}"); + } + if (values.length() > 0) { + template.fragment(values.toString()); + } + UriComponents encoded = template.build().expand(query).encode(); + builder.fragment(encoded.getFragment()); + } else { + for (String key : query.keySet()) { + String name = key; + if (keys != null && keys.containsKey(key)) { + name = keys.get(key); + } + template.queryParam(name, "{" + key + "}"); + } + template.fragment(redirectUri.getFragment()); + UriComponents encoded = template.build().expand(query).encode(); + builder.query(encoded.getQuery()); + } + return builder.build().toUriString(); + } + + public static String[] obtainBasicAuthorization(HttpServletRequest request) { + String clientId; + String clientSecret; + // 先从 Header 中获取 + String authorization = request.getHeader("Authorization"); + authorization = StrUtil.subAfter(authorization, "Basic ", true); + if (StringUtils.hasText(authorization)) { + authorization = Base64.decodeStr(authorization); + clientId = StrUtil.subBefore(authorization, ":", false); + clientSecret = StrUtil.subAfter(authorization, ":", false); + // 再从 Param 中获取 + } else { + clientId = request.getParameter("client_id"); + clientSecret = request.getParameter("client_secret"); + } + + // 如果两者非空,则返回 + if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) { + return new String[]{clientId, clientSecret}; + } + return null; + } + + +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/utils/OAuth2Utils.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/utils/OAuth2Utils.java new file mode 100644 index 0000000..dcb60da --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/utils/OAuth2Utils.java @@ -0,0 +1,101 @@ +package com.bnhz.oauth.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.StrUtil; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.*; + +/** + * OAuth2 相关的工具类 + * + * @author 芋道源码 + */ +public class OAuth2Utils { + + /** + * 构建授权码模式下,重定向的 URI + * + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 getSuccessfulRedirect 方法 + * + * @param redirectUri 重定向 URI + * @param authorizationCode 授权码 + * @param state 状态 + * @return 授权码模式下的重定向 URI + */ + public static String buildAuthorizationCodeRedirectUri(String redirectUri, String authorizationCode, String state) { + Map query = new LinkedHashMap<>(); + query.put("code", authorizationCode); + if (state != null) { + query.put("state", state); + } + return HttpUtils.append(redirectUri, query, null, false); + } + + /** + * 构建简化模式下,重定向的 URI + * + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 appendAccessToken 方法 + * + * @param redirectUri 重定向 URI + * @param accessToken 访问令牌 + * @param state 状态 + * @param expireTime 过期时间 + * @param scopes 授权范围 + * @param additionalInformation 附加信息 + * @return 简化授权模式下的重定向 URI + */ + public static String buildImplicitRedirectUri(String redirectUri, String accessToken, String state, LocalDateTime expireTime, + Collection scopes, Map additionalInformation) { + Map vars = new LinkedHashMap(); + Map keys = new HashMap(); + vars.put("access_token", accessToken); + vars.put("token_type", "bearer"); + if (state != null) { + vars.put("state", state); + } + if (expireTime != null) { + vars.put("expires_in", getExpiresIn(expireTime)); + } + if (CollUtil.isNotEmpty(scopes)) { + vars.put("scope", buildScopeStr(scopes)); + } + if (CollUtil.isNotEmpty(additionalInformation)) { + for (String key : additionalInformation.keySet()) { + Object value = additionalInformation.get(key); + if (value != null) { + keys.put("extra_" + key, key); + vars.put("extra_" + key, value); + } + } + } + // Do not include the refresh token (even if there is one) + return HttpUtils.append(redirectUri, vars, keys, true); + } + + public static String buildUnsuccessfulRedirect(String redirectUri, String responseType, String state, + String error, String description) { + Map query = new LinkedHashMap(); + query.put("error", error); + query.put("error_description", description); + if (state != null) { + query.put("state", state); + } + return HttpUtils.append(redirectUri, query, null, !responseType.contains("code")); + } + + public static long getExpiresIn(LocalDateTime expireTime) { + return LocalDateTimeUtil.between(LocalDateTime.now(), expireTime, ChronoUnit.SECONDS); + } + + public static String buildScopeStr(Collection scopes) { + return CollUtil.join(scopes, " "); + } + + public static List buildScopes(String scope) { + return StrUtil.split(scope, ' '); + } + +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/vo/OAuth2OpenAccessTokenRespVO.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/vo/OAuth2OpenAccessTokenRespVO.java new file mode 100644 index 0000000..6c18a47 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/vo/OAuth2OpenAccessTokenRespVO.java @@ -0,0 +1,27 @@ +package com.bnhz.oauth.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenAccessTokenRespVO { + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("refresh_token") + private String refreshToken; + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("expires_in") + private Long expiresIn; + + private String scope; + +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/vo/OAuth2OpenAuthorizeInfoRespVO.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/vo/OAuth2OpenAuthorizeInfoRespVO.java new file mode 100644 index 0000000..b6aa5c0 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/vo/OAuth2OpenAuthorizeInfoRespVO.java @@ -0,0 +1,33 @@ +package com.bnhz.oauth.vo; + +import com.bnhz.common.core.text.KeyValue; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenAuthorizeInfoRespVO { + + /** + * 客户端 + */ + private Client client; + + private List> scopes; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Client { + + private String name; + + private String logo; + + } + +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/vo/Oauth2AccessTokenReqVO.java b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/vo/Oauth2AccessTokenReqVO.java new file mode 100644 index 0000000..86a964c --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/java/com/bnhz/oauth/vo/Oauth2AccessTokenReqVO.java @@ -0,0 +1,27 @@ +package com.bnhz.oauth.vo; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; + +/** + * @author fastb + * @version 1.0 + * @description: TODO + * @date 2024-03-21 11:13 + */ +@Data +public class Oauth2AccessTokenReqVO { + + @JSONField(name = "grant_type") + private String grantType; + + private String code; + + @JSONField(name = "redirect_uri") + private String redirectUri; + + private String scope; + + private String state; + +} diff --git a/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthAccessTokenMapper.xml b/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthAccessTokenMapper.xml new file mode 100644 index 0000000..b76d69f --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthAccessTokenMapper.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + select token_id, token, authentication_id, user_name, client_id, authentication, refresh_token, open_id, user_id, expires_time from oauth_access_token + + + + update oauth_access_token + set open_id = #{openUid} + where token_id = #{tokenId} + + + + delete + from oauth_access_token + where user_id = #{userId} + + + + + + + + + + insert into oauth_access_token + + token_id, + token, + authentication_id, + user_name, + client_id, + authentication, + refresh_token, + open_id, + user_id, + expires_time, + + + #{tokenId}, + #{token}, + #{authenticationId}, + #{userName}, + #{clientId}, + #{authentication}, + #{refreshToken}, + #{openId}, + #{userId}, + #{expiresTime}, + + + + diff --git a/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthApprovalsMapper.xml b/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthApprovalsMapper.xml new file mode 100644 index 0000000..c2493b9 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthApprovalsMapper.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + select userId, clientId, scope, status, expiresAt, lastModifiedAt from oauth_approvals + + + + + + + + + + insert into oauth_approvals + + userId, + clientId, + scope, + status, + expiresAt, + lastModifiedAt, + + + #{userid}, + #{clientid}, + #{scope}, + #{status}, + #{expiresat}, + #{lastmodifiedat}, + + + + + update oauth_approvals + + clientId = #{clientid}, + scope = #{scope}, + status = #{status}, + expiresAt = #{expiresat}, + lastModifiedAt = #{lastmodifiedat}, + + where userId = #{userid} + + + update oauth_approvals + set status = #{status}, + expiresat = #{expiresat} + where userid = #{userid} + and clientid = #{clientid} + and scope = #{scope} + + + + delete from oauth_approvals where userId = #{userid} + + + + delete from oauth_approvals where userId in + + #{userid} + + + diff --git a/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthClientDetailsMapper.xml b/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthClientDetailsMapper.xml new file mode 100644 index 0000000..78871ae --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthClientDetailsMapper.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + select id, client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove, type, status, icon, cloud_skill_id,tenant_id, tenant_name from oauth_client_details + + + + + + + + + + + + insert into oauth_client_details + + client_id, + resource_ids, + client_secret, + scope, + authorized_grant_types, + web_server_redirect_uri, + authorities, + access_token_validity, + refresh_token_validity, + additional_information, + autoapprove, + type, + status, + icon, + cloud_skill_id, + tenant_id, + tenant_name, + + + #{clientId}, + #{resourceIds}, + #{clientSecret}, + #{scope}, + #{authorizedGrantTypes}, + #{webServerRedirectUri}, + #{authorities}, + #{accessTokenValidity}, + #{refreshTokenValidity}, + #{additionalInformation}, + #{autoapprove}, + #{type}, + #{status}, + #{icon}, + #{cloudSkillId}, + #{tenantId}, + #{tenantName}, + + + + + update oauth_client_details + + client_id = #{clientId}, + resource_ids = #{resourceIds}, + client_secret = #{clientSecret}, + scope = #{scope}, + authorized_grant_types = #{authorizedGrantTypes}, + web_server_redirect_uri = #{webServerRedirectUri}, + authorities = #{authorities}, + + + access_token_validity = #{accessTokenValidity}, + refresh_token_validity = #{refreshTokenValidity}, + additional_information = #{additionalInformation}, + autoapprove = #{autoapprove}, + type = #{type}, + status = #{status}, + icon = #{icon}, + cloud_skill_id = #{cloudSkillId}, + + where id = #{id} + + + + delete from oauth_client_details where client_id = #{clientId} + + + + delete from oauth_client_details where id in + + #{id} + + + diff --git a/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthCodeMapper.xml b/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthCodeMapper.xml new file mode 100644 index 0000000..6c545b7 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/resources/mapper/OauthCodeMapper.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + select code, authentication, user_id from oauth_code + + + + + + + + insert into oauth_code + + code, + authentication, + user_id, + + + #{code}, + #{authentication}, + #{userId}, + + + + + update oauth_code + + authentication = #{authentication}, + user_id = #{userId}, + + where code = #{code} + + + + delete from oauth_code where code = #{code} + + + + delete from oauth_code where code in + + #{code} + + + diff --git a/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.css b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.css new file mode 100644 index 0000000..ea33f76 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.css @@ -0,0 +1,587 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +.btn-default, +.btn-primary, +.btn-success, +.btn-info, +.btn-warning, +.btn-danger { + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); +} +.btn-default:active, +.btn-primary:active, +.btn-success:active, +.btn-info:active, +.btn-warning:active, +.btn-danger:active, +.btn-default.active, +.btn-primary.active, +.btn-success.active, +.btn-info.active, +.btn-warning.active, +.btn-danger.active { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn-default.disabled, +.btn-primary.disabled, +.btn-success.disabled, +.btn-info.disabled, +.btn-warning.disabled, +.btn-danger.disabled, +.btn-default[disabled], +.btn-primary[disabled], +.btn-success[disabled], +.btn-info[disabled], +.btn-warning[disabled], +.btn-danger[disabled], +fieldset[disabled] .btn-default, +fieldset[disabled] .btn-primary, +fieldset[disabled] .btn-success, +fieldset[disabled] .btn-info, +fieldset[disabled] .btn-warning, +fieldset[disabled] .btn-danger { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-default .badge, +.btn-primary .badge, +.btn-success .badge, +.btn-info .badge, +.btn-warning .badge, +.btn-danger .badge { + text-shadow: none; +} +.btn:active, +.btn.active { + background-image: none; +} +.btn-default { + background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); + background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); + background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #dbdbdb; + text-shadow: 0 1px 0 #fff; + border-color: #ccc; +} +.btn-default:hover, +.btn-default:focus { + background-color: #e0e0e0; + background-position: 0 -15px; +} +.btn-default:active, +.btn-default.active { + background-color: #e0e0e0; + border-color: #dbdbdb; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #e0e0e0; + background-image: none; +} +.btn-primary { + background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); + background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #245580; +} +.btn-primary:hover, +.btn-primary:focus { + background-color: #265a88; + background-position: 0 -15px; +} +.btn-primary:active, +.btn-primary.active { + background-color: #265a88; + border-color: #245580; +} +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #265a88; + background-image: none; +} +.btn-success { + background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); + background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); + background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #3e8f3e; +} +.btn-success:hover, +.btn-success:focus { + background-color: #419641; + background-position: 0 -15px; +} +.btn-success:active, +.btn-success.active { + background-color: #419641; + border-color: #3e8f3e; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #419641; + background-image: none; +} +.btn-info { + background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); + background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); + background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #28a4c9; +} +.btn-info:hover, +.btn-info:focus { + background-color: #2aabd2; + background-position: 0 -15px; +} +.btn-info:active, +.btn-info.active { + background-color: #2aabd2; + border-color: #28a4c9; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #2aabd2; + background-image: none; +} +.btn-warning { + background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); + background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); + background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #e38d13; +} +.btn-warning:hover, +.btn-warning:focus { + background-color: #eb9316; + background-position: 0 -15px; +} +.btn-warning:active, +.btn-warning.active { + background-color: #eb9316; + border-color: #e38d13; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #eb9316; + background-image: none; +} +.btn-danger { + background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); + background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); + background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #b92c28; +} +.btn-danger:hover, +.btn-danger:focus { + background-color: #c12e2a; + background-position: 0 -15px; +} +.btn-danger:active, +.btn-danger.active { + background-color: #c12e2a; + border-color: #b92c28; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #c12e2a; + background-image: none; +} +.thumbnail, +.img-thumbnail { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); + background-repeat: repeat-x; + background-color: #e8e8e8; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); + background-repeat: repeat-x; + background-color: #2e6da4; +} +.navbar-default { + background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); + background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#f8f8f8)); + background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075); +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .active > a { + background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); + background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); + background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); + background-repeat: repeat-x; + -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075); +} +.navbar-brand, +.navbar-nav > li > a { + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25); +} +.navbar-inverse { + background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); + background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); + background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + border-radius: 4px; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .active > a { + background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); + background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); + background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); + background-repeat: repeat-x; + -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25); + box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25); +} +.navbar-inverse .navbar-brand, +.navbar-inverse .navbar-nav > li > a { + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.navbar-static-top, +.navbar-fixed-top, +.navbar-fixed-bottom { + border-radius: 0; +} +@media (max-width: 767px) { + .navbar .navbar-nav .open .dropdown-menu > .active > a, + .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); + background-repeat: repeat-x; + } +} +.alert { + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); +} +.alert-success { + background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); + background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); + background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); + background-repeat: repeat-x; + border-color: #b2dba1; +} +.alert-info { + background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); + background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); + background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); + background-repeat: repeat-x; + border-color: #9acfea; +} +.alert-warning { + background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); + background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); + background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); + background-repeat: repeat-x; + border-color: #f5e79e; +} +.alert-danger { + background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); + background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); + background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); + background-repeat: repeat-x; + border-color: #dca7a7; +} +.progress { + background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); + background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); + background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar { + background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); + background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-success { + background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); + background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); + background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-info { + background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); + background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); + background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-warning { + background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); + background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); + background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-danger { + background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); + background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); + background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-striped { + 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); + 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); + 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); +} +.list-group { + border-radius: 4px; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + text-shadow: 0 -1px 0 #286090; + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); + background-repeat: repeat-x; + border-color: #2b669a; +} +.list-group-item.active .badge, +.list-group-item.active:hover .badge, +.list-group-item.active:focus .badge { + text-shadow: none; +} +.panel { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} +.panel-default > .panel-heading { + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); + background-repeat: repeat-x; +} +.panel-primary > .panel-heading { + background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); + background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); + background-repeat: repeat-x; +} +.panel-success > .panel-heading { + background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); + background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); + background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); + background-repeat: repeat-x; +} +.panel-info > .panel-heading { + background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); + background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); + background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); + background-repeat: repeat-x; +} +.panel-warning > .panel-heading { + background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); + background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); + background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); + background-repeat: repeat-x; +} +.panel-danger > .panel-heading { + background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); + background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); + background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); + background-repeat: repeat-x; +} +.well { + background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); + background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); + background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); + background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); + background-repeat: repeat-x; + border-color: #dcdcdc; + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); +} +/*# sourceMappingURL=bootstrap-theme.css.map */ \ No newline at end of file diff --git a/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.css.map b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.css.map new file mode 100644 index 0000000..949d097 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap-theme.css","less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":"AAAA;;;;GAIG;ACiBH;;;;;;EAME,yCAAA;EC2CA,4FAAA;EACQ,oFAAA;CFzDT;ACkBC;;;;;;;;;;;;ECsCA,yDAAA;EACQ,iDAAA;CF1CT;ACQC;;;;;;;;;;;;;;;;;;ECiCA,yBAAA;EACQ,iBAAA;CFrBT;AC7BD;;;;;;EAuBI,kBAAA;CDcH;AC2BC;;EAEE,uBAAA;CDzBH;AC8BD;EEvEI,sEAAA;EACA,iEAAA;EACA,2FAAA;EAAA,oEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;EAyCA,0BAAA;EACA,mBAAA;CDtBD;AClBC;;EAEE,0BAAA;EACA,6BAAA;CDoBH;ACjBC;;EAEE,0BAAA;EACA,sBAAA;CDmBH;ACbG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD2BL;ACPD;EE5EI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;CD4DD;AC1DC;;EAEE,0BAAA;EACA,6BAAA;CD4DH;ACzDC;;EAEE,0BAAA;EACA,sBAAA;CD2DH;ACrDG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDmEL;AC9CD;EE7EI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;CDoGD;AClGC;;EAEE,0BAAA;EACA,6BAAA;CDoGH;ACjGC;;EAEE,0BAAA;EACA,sBAAA;CDmGH;AC7FG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD2GL;ACrFD;EE9EI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;CD4ID;AC1IC;;EAEE,0BAAA;EACA,6BAAA;CD4IH;ACzIC;;EAEE,0BAAA;EACA,sBAAA;CD2IH;ACrIG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDmJL;AC5HD;EE/EI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;CDoLD;AClLC;;EAEE,0BAAA;EACA,6BAAA;CDoLH;ACjLC;;EAEE,0BAAA;EACA,sBAAA;CDmLH;AC7KG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD2LL;ACnKD;EEhFI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EClBF,oEAAA;EH8CA,4BAAA;EACA,sBAAA;CD4ND;AC1NC;;EAEE,0BAAA;EACA,6BAAA;CD4NH;ACzNC;;EAEE,0BAAA;EACA,sBAAA;CD2NH;ACrNG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDmOL;ACpMD;;ECtCE,mDAAA;EACQ,2CAAA;CF8OT;AC/LD;;EEjGI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFgGF,0BAAA;CDqMD;ACnMD;;;EEtGI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFsGF,0BAAA;CDyMD;AChMD;EEnHI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;ECnBF,oEAAA;EHqIA,mBAAA;ECrEA,4FAAA;EACQ,oFAAA;CF4QT;AC3MD;;EEnHI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;ED6CF,yDAAA;EACQ,iDAAA;CFsRT;ACxMD;;EAEE,+CAAA;CD0MD;ACtMD;EEtII,sEAAA;EACA,iEAAA;EACA,2FAAA;EAAA,oEAAA;EACA,uHAAA;EACA,4BAAA;ECnBF,oEAAA;EHwJA,mBAAA;CD4MD;AC/MD;;EEtII,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;ED6CF,wDAAA;EACQ,gDAAA;CF6ST;ACzND;;EAYI,0CAAA;CDiNH;AC5MD;;;EAGE,iBAAA;CD8MD;AC1MD;EAEI;;;IAGE,YAAA;IEnKF,yEAAA;IACA,oEAAA;IACA,8FAAA;IAAA,uEAAA;IACA,uHAAA;IACA,4BAAA;GH+WD;CACF;ACrMD;EACE,8CAAA;EC/HA,2FAAA;EACQ,mFAAA;CFuUT;AC7LD;EE5LI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFoLF,sBAAA;CDyMD;ACpMD;EE7LI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFoLF,sBAAA;CDiND;AC3MD;EE9LI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFoLF,sBAAA;CDyND;AClND;EE/LI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFoLF,sBAAA;CDiOD;AClND;EEvMI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CH4ZH;AC/MD;EEjNI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHmaH;ACrND;EElNI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CH0aH;AC3ND;EEnNI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHibH;ACjOD;EEpNI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHwbH;ACvOD;EErNI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CH+bH;AC1OD;EExLI,8MAAA;EACA,yMAAA;EACA,sMAAA;CHqaH;ACtOD;EACE,mBAAA;EClLA,mDAAA;EACQ,2CAAA;CF2ZT;ACvOD;;;EAGE,8BAAA;EEzOE,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EFuOF,sBAAA;CD6OD;AClPD;;;EAQI,kBAAA;CD+OH;ACrOD;ECvME,kDAAA;EACQ,0CAAA;CF+aT;AC/ND;EElQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHoeH;ACrOD;EEnQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CH2eH;AC3OD;EEpQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHkfH;ACjPD;EErQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHyfH;ACvPD;EEtQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHggBH;AC7PD;EEvQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;CHugBH;AC7PD;EE9QI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,uHAAA;EACA,4BAAA;EF4QF,sBAAA;EC/NA,0FAAA;EACQ,kFAAA;CFmeT","file":"bootstrap-theme.css","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.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.btn-default:active,\n.btn-primary:active,\n.btn-success:active,\n.btn-info:active,\n.btn-warning:active,\n.btn-danger:active,\n.btn-default.active,\n.btn-primary.active,\n.btn-success.active,\n.btn-info.active,\n.btn-warning.active,\n.btn-danger.active {\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-default.disabled,\n.btn-primary.disabled,\n.btn-success.disabled,\n.btn-info.disabled,\n.btn-warning.disabled,\n.btn-danger.disabled,\n.btn-default[disabled],\n.btn-primary[disabled],\n.btn-success[disabled],\n.btn-info[disabled],\n.btn-warning[disabled],\n.btn-danger[disabled],\nfieldset[disabled] .btn-default,\nfieldset[disabled] .btn-primary,\nfieldset[disabled] .btn-success,\nfieldset[disabled] .btn-info,\nfieldset[disabled] .btn-warning,\nfieldset[disabled] .btn-danger {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-default .badge,\n.btn-primary .badge,\n.btn-success .badge,\n.btn-info .badge,\n.btn-warning .badge,\n.btn-danger .badge {\n text-shadow: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n}\n.btn-default {\n background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #dbdbdb;\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus {\n background-color: #e0e0e0;\n background-position: 0 -15px;\n}\n.btn-default:active,\n.btn-default.active {\n background-color: #e0e0e0;\n border-color: #dbdbdb;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\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.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #e0e0e0;\n background-image: none;\n}\n.btn-primary {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #245580;\n}\n.btn-primary:hover,\n.btn-primary:focus {\n background-color: #265a88;\n background-position: 0 -15px;\n}\n.btn-primary:active,\n.btn-primary.active {\n background-color: #265a88;\n border-color: #245580;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\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.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #265a88;\n background-image: none;\n}\n.btn-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #3e8f3e;\n}\n.btn-success:hover,\n.btn-success:focus {\n background-color: #419641;\n background-position: 0 -15px;\n}\n.btn-success:active,\n.btn-success.active {\n background-color: #419641;\n border-color: #3e8f3e;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\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.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #419641;\n background-image: none;\n}\n.btn-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #28a4c9;\n}\n.btn-info:hover,\n.btn-info:focus {\n background-color: #2aabd2;\n background-position: 0 -15px;\n}\n.btn-info:active,\n.btn-info.active {\n background-color: #2aabd2;\n border-color: #28a4c9;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\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.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #2aabd2;\n background-image: none;\n}\n.btn-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #e38d13;\n}\n.btn-warning:hover,\n.btn-warning:focus {\n background-color: #eb9316;\n background-position: 0 -15px;\n}\n.btn-warning:active,\n.btn-warning.active {\n background-color: #eb9316;\n border-color: #e38d13;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\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.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #eb9316;\n background-image: none;\n}\n.btn-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #b92c28;\n}\n.btn-danger:hover,\n.btn-danger:focus {\n background-color: #c12e2a;\n background-position: 0 -15px;\n}\n.btn-danger:active,\n.btn-danger.active {\n background-color: #c12e2a;\n border-color: #b92c28;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\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.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #c12e2a;\n background-image: none;\n}\n.thumbnail,\n.img-thumbnail {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n background-color: #e8e8e8;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n background-color: #2e6da4;\n}\n.navbar-default {\n background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);\n}\n.navbar-inverse {\n background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n}\n.navbar-inverse .navbar-brand,\n.navbar-inverse .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n@media (max-width: 767px) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n }\n}\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.alert-success {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);\n background-repeat: repeat-x;\n border-color: #b2dba1;\n}\n.alert-info {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #9acfea;\n}\n.alert-warning {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #f5e79e;\n}\n.alert-danger {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dca7a7;\n}\n.progress {\n background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);\n background-repeat: repeat-x;\n}\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}\n.list-group {\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 #286090;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);\n background-repeat: repeat-x;\n border-color: #2b669a;\n}\n.list-group-item.active .badge,\n.list-group-item.active:hover .badge,\n.list-group-item.active:focus .badge {\n text-shadow: none;\n}\n.panel {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.panel-default > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-primary > .panel-heading {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-success > .panel-heading {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-info > .panel-heading {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-warning > .panel-heading {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-danger > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);\n background-repeat: repeat-x;\n}\n.well {\n background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dcdcdc;\n -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n}\n/*# sourceMappingURL=bootstrap-theme.css.map */","// stylelint-disable selector-no-qualifying-type, selector-max-compound-selectors\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\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0, 0, 0, .125));\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n .box-shadow(none);\n }\n\n .badge {\n text-shadow: none;\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &.focus,\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n background-image: none;\n }\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default {\n .btn-styles(@btn-default-bg);\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0, 0, 0, .075));\n}\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);\n .box-shadow(@shadow);\n\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0, 0, 0, .075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, .25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257\n border-radius: @navbar-border-radius;\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0, 0, 0, .25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n// Fix active state of dropdown items in collapsed mode\n@media (max-width: @grid-float-breakpoint-max) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: #fff;\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n }\n }\n}\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, .2);\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n// Reset the striped class because our mixins don't do multiple gradients and\n// the above custom styles override the new `.progress-bar-striped` in v3.2.0.\n.progress-bar-striped {\n #gradient > .striped();\n}\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0, 0, 0, .075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n\n .badge {\n text-shadow: none;\n }\n}\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0, 0, 0, .05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);\n .box-shadow(@shadow);\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","// stylelint-disable value-no-vendor-prefix, selector-max-id\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\", argb(@start-color), argb(@end-color))); // IE9 and down\n background-repeat: repeat-x;\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\", argb(@start-color), argb(@end-color))); // IE9 and down\n background-repeat: repeat-x;\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\", argb(@start-color), argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n background-repeat: no-repeat;\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\", argb(@start-color), argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n background-repeat: no-repeat;\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255, 255, 255, .15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n"]} \ No newline at end of file diff --git a/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.min.css b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.min.css new file mode 100644 index 0000000..2a69f48 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.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) + */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x;background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x;background-color:#2e6da4}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{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)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} +/*# sourceMappingURL=bootstrap-theme.min.css.map */ \ No newline at end of file diff --git a/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.min.css.map b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.min.css.map new file mode 100644 index 0000000..5d75106 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap-theme.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap-theme.css","dist/css/bootstrap-theme.css","less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":"AAAA;;;;ACUA,YCWA,aDbA,UAFA,aACA,aAEA,aCkBE,YAAA,EAAA,KAAA,EAAA,eC2CA,mBAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,iBF7CV,mBANA,mBACA,oBCWE,oBDRF,iBANA,iBAIA,oBANA,oBAOA,oBANA,oBAQA,oBANA,oBEmDE,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBFpCV,qBAMA,sBCJE,sBDDF,uBAHA,mBAMA,oBARA,sBAMA,uBALA,sBAMA,uBAJA,sBAMA,uBAOA,+BALA,gCAGA,6BAFA,gCACA,gCAEA,gCEwBE,mBAAA,KACQ,WAAA,KFfV,mBCnCA,oBDiCA,iBAFA,oBACA,oBAEA,oBCXI,YAAA,KDgBJ,YCyBE,YAEE,iBAAA,KAKJ,aEvEI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QAyCA,YAAA,EAAA,IAAA,EAAA,KACA,aAAA,KDnBF,mBCrBE,mBAEE,iBAAA,QACA,oBAAA,EAAA,MDuBJ,oBCpBE,oBAEE,iBAAA,QACA,aAAA,QAMA,sBD8BJ,6BANA,4BAGA,6BANA,4BAHA,4BAFA,uBAeA,8BANA,6BAGA,8BANA,6BAHA,6BAFA,gCAeA,uCANA,sCAGA,uCANA,sCAHA,sCCdM,iBAAA,QACA,iBAAA,KAoBN,aE5EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QDgEF,mBC9DE,mBAEE,iBAAA,QACA,oBAAA,EAAA,MDgEJ,oBC7DE,oBAEE,iBAAA,QACA,aAAA,QAMA,sBDuEJ,6BANA,4BAGA,6BANA,4BAHA,4BAFA,uBAeA,8BANA,6BAGA,8BANA,6BAHA,6BAFA,gCAeA,uCANA,sCAGA,uCANA,sCAHA,sCCvDM,iBAAA,QACA,iBAAA,KAqBN,aE7EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QDyGF,mBCvGE,mBAEE,iBAAA,QACA,oBAAA,EAAA,MDyGJ,oBCtGE,oBAEE,iBAAA,QACA,aAAA,QAMA,sBDgHJ,6BANA,4BAGA,6BANA,4BAHA,4BAFA,uBAeA,8BANA,6BAGA,8BANA,6BAHA,6BAFA,gCAeA,uCANA,sCAGA,uCANA,sCAHA,sCChGM,iBAAA,QACA,iBAAA,KAsBN,UE9EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QDkJF,gBChJE,gBAEE,iBAAA,QACA,oBAAA,EAAA,MDkJJ,iBC/IE,iBAEE,iBAAA,QACA,aAAA,QAMA,mBDyJJ,0BANA,yBAGA,0BANA,yBAHA,yBAFA,oBAeA,2BANA,0BAGA,2BANA,0BAHA,0BAFA,6BAeA,oCANA,mCAGA,oCANA,mCAHA,mCCzIM,iBAAA,QACA,iBAAA,KAuBN,aE/EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QD2LF,mBCzLE,mBAEE,iBAAA,QACA,oBAAA,EAAA,MD2LJ,oBCxLE,oBAEE,iBAAA,QACA,aAAA,QAMA,sBDkMJ,6BANA,4BAGA,6BANA,4BAHA,4BAFA,uBAeA,8BANA,6BAGA,8BANA,6BAHA,6BAFA,gCAeA,uCANA,sCAGA,uCANA,sCAHA,sCClLM,iBAAA,QACA,iBAAA,KAwBN,YEhFI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GClBF,OAAA,0DH8CA,kBAAA,SACA,aAAA,QDoOF,kBClOE,kBAEE,iBAAA,QACA,oBAAA,EAAA,MDoOJ,mBCjOE,mBAEE,iBAAA,QACA,aAAA,QAMA,qBD2OJ,4BANA,2BAGA,4BANA,2BAHA,2BAFA,sBAeA,6BANA,4BAGA,6BANA,4BAHA,4BAFA,+BAeA,sCANA,qCAGA,sCANA,qCAHA,qCC3NM,iBAAA,QACA,iBAAA,KD2ON,eC5MA,WCtCE,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBFsPV,0BCvMA,0BEjGI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFgGF,iBAAA,QAEF,yBD6MA,+BADA,+BGlTI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFsGF,iBAAA,QASF,gBEnHI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,kBAAA,SCnBF,OAAA,0DHqIA,cAAA,ICrEA,mBAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,iBFuRV,sCCtNA,oCEnHI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD6CF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBD8EV,cDoNA,iBClNE,YAAA,EAAA,IAAA,EAAA,sBAIF,gBEtII,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,kBAAA,SCnBF,OAAA,0DHwJA,cAAA,IDyNF,sCC5NA,oCEtII,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD6CF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBDoFV,8BDuOA,iCC3NI,YAAA,EAAA,KAAA,EAAA,gBDgOJ,qBADA,kBC1NA,mBAGE,cAAA,EAIF,yBAEI,mDDwNF,yDADA,yDCpNI,MAAA,KEnKF,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,UF2KJ,OACE,YAAA,EAAA,IAAA,EAAA,qBC/HA,mBAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,qBAAA,CAAA,EAAA,IAAA,IAAA,gBD0IV,eE5LI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoLF,aAAA,QAKF,YE7LI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoLF,aAAA,QAMF,eE9LI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoLF,aAAA,QAOF,cE/LI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoLF,aAAA,QAeF,UEvMI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF6MJ,cEjNI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8MJ,sBElNI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+MJ,mBEnNI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFgNJ,sBEpNI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiNJ,qBErNI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFqNJ,sBExLI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKF+LJ,YACE,cAAA,IClLA,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBDoLV,wBDiQA,8BADA,8BC7PE,YAAA,EAAA,KAAA,EAAA,QEzOE,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFuOF,aAAA,QALF,+BD6QA,qCADA,qCCpQI,YAAA,KAUJ,OCvME,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gBDgNV,8BElQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+PJ,8BEnQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFgQJ,8BEpQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiQJ,2BErQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFkQJ,8BEtQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFmQJ,6BEvQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0QJ,ME9QI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF4QF,aAAA,QC/NA,mBAAA,MAAA,EAAA,IAAA,IAAA,eAAA,CAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,eAAA,CAAA,EAAA,IAAA,EAAA","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.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.btn-default:active,\n.btn-primary:active,\n.btn-success:active,\n.btn-info:active,\n.btn-warning:active,\n.btn-danger:active,\n.btn-default.active,\n.btn-primary.active,\n.btn-success.active,\n.btn-info.active,\n.btn-warning.active,\n.btn-danger.active {\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-default.disabled,\n.btn-primary.disabled,\n.btn-success.disabled,\n.btn-info.disabled,\n.btn-warning.disabled,\n.btn-danger.disabled,\n.btn-default[disabled],\n.btn-primary[disabled],\n.btn-success[disabled],\n.btn-info[disabled],\n.btn-warning[disabled],\n.btn-danger[disabled],\nfieldset[disabled] .btn-default,\nfieldset[disabled] .btn-primary,\nfieldset[disabled] .btn-success,\nfieldset[disabled] .btn-info,\nfieldset[disabled] .btn-warning,\nfieldset[disabled] .btn-danger {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-default .badge,\n.btn-primary .badge,\n.btn-success .badge,\n.btn-info .badge,\n.btn-warning .badge,\n.btn-danger .badge {\n text-shadow: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n}\n.btn-default {\n background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #dbdbdb;\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus {\n background-color: #e0e0e0;\n background-position: 0 -15px;\n}\n.btn-default:active,\n.btn-default.active {\n background-color: #e0e0e0;\n border-color: #dbdbdb;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\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.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #e0e0e0;\n background-image: none;\n}\n.btn-primary {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #245580;\n}\n.btn-primary:hover,\n.btn-primary:focus {\n background-color: #265a88;\n background-position: 0 -15px;\n}\n.btn-primary:active,\n.btn-primary.active {\n background-color: #265a88;\n border-color: #245580;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\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.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #265a88;\n background-image: none;\n}\n.btn-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #3e8f3e;\n}\n.btn-success:hover,\n.btn-success:focus {\n background-color: #419641;\n background-position: 0 -15px;\n}\n.btn-success:active,\n.btn-success.active {\n background-color: #419641;\n border-color: #3e8f3e;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\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.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #419641;\n background-image: none;\n}\n.btn-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #28a4c9;\n}\n.btn-info:hover,\n.btn-info:focus {\n background-color: #2aabd2;\n background-position: 0 -15px;\n}\n.btn-info:active,\n.btn-info.active {\n background-color: #2aabd2;\n border-color: #28a4c9;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\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.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #2aabd2;\n background-image: none;\n}\n.btn-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #e38d13;\n}\n.btn-warning:hover,\n.btn-warning:focus {\n background-color: #eb9316;\n background-position: 0 -15px;\n}\n.btn-warning:active,\n.btn-warning.active {\n background-color: #eb9316;\n border-color: #e38d13;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\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.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #eb9316;\n background-image: none;\n}\n.btn-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #b92c28;\n}\n.btn-danger:hover,\n.btn-danger:focus {\n background-color: #c12e2a;\n background-position: 0 -15px;\n}\n.btn-danger:active,\n.btn-danger.active {\n background-color: #c12e2a;\n border-color: #b92c28;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\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.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #c12e2a;\n background-image: none;\n}\n.thumbnail,\n.img-thumbnail {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n background-color: #e8e8e8;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n background-color: #2e6da4;\n}\n.navbar-default {\n background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);\n}\n.navbar-inverse {\n background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n}\n.navbar-inverse .navbar-brand,\n.navbar-inverse .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n@media (max-width: 767px) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n }\n}\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.alert-success {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);\n background-repeat: repeat-x;\n border-color: #b2dba1;\n}\n.alert-info {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #9acfea;\n}\n.alert-warning {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #f5e79e;\n}\n.alert-danger {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dca7a7;\n}\n.progress {\n background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);\n background-repeat: repeat-x;\n}\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}\n.list-group {\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 #286090;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);\n background-repeat: repeat-x;\n border-color: #2b669a;\n}\n.list-group-item.active .badge,\n.list-group-item.active:hover .badge,\n.list-group-item.active:focus .badge {\n text-shadow: none;\n}\n.panel {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.panel-default > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-primary > .panel-heading {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-success > .panel-heading {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-info > .panel-heading {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-warning > .panel-heading {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-danger > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);\n background-repeat: repeat-x;\n}\n.well {\n background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dcdcdc;\n -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n}\n/*# sourceMappingURL=bootstrap-theme.css.map */","/*!\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.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.btn-default:active,\n.btn-primary:active,\n.btn-success:active,\n.btn-info:active,\n.btn-warning:active,\n.btn-danger:active,\n.btn-default.active,\n.btn-primary.active,\n.btn-success.active,\n.btn-info.active,\n.btn-warning.active,\n.btn-danger.active {\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-default.disabled,\n.btn-primary.disabled,\n.btn-success.disabled,\n.btn-info.disabled,\n.btn-warning.disabled,\n.btn-danger.disabled,\n.btn-default[disabled],\n.btn-primary[disabled],\n.btn-success[disabled],\n.btn-info[disabled],\n.btn-warning[disabled],\n.btn-danger[disabled],\nfieldset[disabled] .btn-default,\nfieldset[disabled] .btn-primary,\nfieldset[disabled] .btn-success,\nfieldset[disabled] .btn-info,\nfieldset[disabled] .btn-warning,\nfieldset[disabled] .btn-danger {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-default .badge,\n.btn-primary .badge,\n.btn-success .badge,\n.btn-info .badge,\n.btn-warning .badge,\n.btn-danger .badge {\n text-shadow: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n}\n.btn-default {\n background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));\n background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #dbdbdb;\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus {\n background-color: #e0e0e0;\n background-position: 0 -15px;\n}\n.btn-default:active,\n.btn-default.active {\n background-color: #e0e0e0;\n border-color: #dbdbdb;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\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.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #e0e0e0;\n background-image: none;\n}\n.btn-primary {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));\n background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #245580;\n}\n.btn-primary:hover,\n.btn-primary:focus {\n background-color: #265a88;\n background-position: 0 -15px;\n}\n.btn-primary:active,\n.btn-primary.active {\n background-color: #265a88;\n border-color: #245580;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\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.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #265a88;\n background-image: none;\n}\n.btn-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));\n background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #3e8f3e;\n}\n.btn-success:hover,\n.btn-success:focus {\n background-color: #419641;\n background-position: 0 -15px;\n}\n.btn-success:active,\n.btn-success.active {\n background-color: #419641;\n border-color: #3e8f3e;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\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.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #419641;\n background-image: none;\n}\n.btn-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));\n background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #28a4c9;\n}\n.btn-info:hover,\n.btn-info:focus {\n background-color: #2aabd2;\n background-position: 0 -15px;\n}\n.btn-info:active,\n.btn-info.active {\n background-color: #2aabd2;\n border-color: #28a4c9;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\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.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #2aabd2;\n background-image: none;\n}\n.btn-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #e38d13;\n}\n.btn-warning:hover,\n.btn-warning:focus {\n background-color: #eb9316;\n background-position: 0 -15px;\n}\n.btn-warning:active,\n.btn-warning.active {\n background-color: #eb9316;\n border-color: #e38d13;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\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.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #eb9316;\n background-image: none;\n}\n.btn-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));\n background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #b92c28;\n}\n.btn-danger:hover,\n.btn-danger:focus {\n background-color: #c12e2a;\n background-position: 0 -15px;\n}\n.btn-danger:active,\n.btn-danger.active {\n background-color: #c12e2a;\n border-color: #b92c28;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\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.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #c12e2a;\n background-image: none;\n}\n.thumbnail,\n.img-thumbnail {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n background-color: #e8e8e8;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n background-color: #2e6da4;\n}\n.navbar-default {\n background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#f8f8f8));\n background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));\n background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);\n}\n.navbar-inverse {\n background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));\n background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));\n background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);\n background-repeat: repeat-x;\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n}\n.navbar-inverse .navbar-brand,\n.navbar-inverse .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n@media (max-width: 767px) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n }\n}\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.alert-success {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));\n background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);\n background-repeat: repeat-x;\n border-color: #b2dba1;\n}\n.alert-info {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));\n background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #9acfea;\n}\n.alert-warning {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);\n background-repeat: repeat-x;\n border-color: #f5e79e;\n}\n.alert-danger {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));\n background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dca7a7;\n}\n.progress {\n background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));\n background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));\n background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));\n background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));\n background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);\n background-repeat: repeat-x;\n}\n.progress-bar-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));\n background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);\n background-repeat: repeat-x;\n}\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}\n.list-group {\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 #286090;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));\n background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);\n background-repeat: repeat-x;\n border-color: #2b669a;\n}\n.list-group-item.active .badge,\n.list-group-item.active:hover .badge,\n.list-group-item.active:focus .badge {\n text-shadow: none;\n}\n.panel {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.panel-default > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-primary > .panel-heading {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-success > .panel-heading {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));\n background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-info > .panel-heading {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));\n background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-warning > .panel-heading {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);\n background-repeat: repeat-x;\n}\n.panel-danger > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));\n background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);\n background-repeat: repeat-x;\n}\n.well {\n background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));\n background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);\n background-repeat: repeat-x;\n border-color: #dcdcdc;\n -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n}\n/*# sourceMappingURL=bootstrap-theme.css.map */","// stylelint-disable selector-no-qualifying-type, selector-max-compound-selectors\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\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0, 0, 0, .125));\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n .box-shadow(none);\n }\n\n .badge {\n text-shadow: none;\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &.focus,\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n background-image: none;\n }\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default {\n .btn-styles(@btn-default-bg);\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0, 0, 0, .075));\n}\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);\n .box-shadow(@shadow);\n\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0, 0, 0, .075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, .25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257\n border-radius: @navbar-border-radius;\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0, 0, 0, .25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n// Fix active state of dropdown items in collapsed mode\n@media (max-width: @grid-float-breakpoint-max) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: #fff;\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n }\n }\n}\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, .2);\n @shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n// Reset the striped class because our mixins don't do multiple gradients and\n// the above custom styles override the new `.progress-bar-striped` in v3.2.0.\n.progress-bar-striped {\n #gradient > .striped();\n}\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0, 0, 0, .075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n\n .badge {\n text-shadow: none;\n }\n}\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0, 0, 0, .05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);\n .box-shadow(@shadow);\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","// stylelint-disable value-no-vendor-prefix, selector-max-id\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\", argb(@start-color), argb(@end-color))); // IE9 and down\n background-repeat: repeat-x;\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\", argb(@start-color), argb(@end-color))); // IE9 and down\n background-repeat: repeat-x;\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\", argb(@start-color), argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n background-repeat: no-repeat;\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\", argb(@start-color), argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n background-repeat: no-repeat;\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255, 255, 255, .15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n"]} \ No newline at end of file diff --git a/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap.css b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap.css new file mode 100644 index 0000000..33c96da --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap.css @@ -0,0 +1,6834 @@ +/*! + * 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: bold; +} +dfn { + font-style: italic; +} +h1 { + font-size: 2em; + margin: 0.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: -0.5em; +} +sub { + bottom: -0.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 #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} +legend { + border: 0; + padding: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +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 { + *, + *:before, + *:after { + color: #000 !important; + text-shadow: none !important; + background: transparent !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: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + 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 th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} +@font-face { + font-family: "Glyphicons Halflings"; + src: url("glyphicons-halflings-regular.eot"); + src: url("glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"), url("glyphicons-halflings-regular.woff2") format("woff2"), url("glyphicons-halflings-regular.woff") format("woff"), url("glyphicons-halflings-regular.ttf") format("truetype"), url("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-euro:before, +.glyphicon-eur: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; +} +*:before, +*:after { + -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: #333333; + background-color: #fff; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #337ab7; + text-decoration: none; +} +a:hover, +a:focus { + color: #23527c; + text-decoration: underline; +} +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > 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 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.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 #eeeeee; +} +.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, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: 400; + line-height: 1; + color: #777777; +} +h1, +.h1, +h2, +.h2, +h3, +.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, +.h4, +h5, +.h5, +h6, +.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: 0.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: #777777; +} +.text-primary { + color: #337ab7; +} +a.text-primary:hover, +a.text-primary:focus { + color: #286090; +} +.text-success { + color: #3c763d; +} +a.text-success:hover, +a.text-success:focus { + color: #2b542c; +} +.text-info { + color: #31708f; +} +a.text-info:hover, +a.text-info:focus { + color: #245269; +} +.text-warning { + color: #8a6d3b; +} +a.text-warning:hover, +a.text-warning:focus { + color: #66512c; +} +.text-danger { + color: #a94442; +} +a.text-danger:hover, +a.text-danger:focus { + color: #843534; +} +.bg-primary { + color: #fff; + background-color: #337ab7; +} +a.bg-primary:hover, +a.bg-primary:focus { + background-color: #286090; +} +.bg-success { + background-color: #dff0d8; +} +a.bg-success:hover, +a.bg-success:focus { + background-color: #c1e2b3; +} +.bg-info { + background-color: #d9edf7; +} +a.bg-info:hover, +a.bg-info:focus { + background-color: #afd9ee; +} +.bg-warning { + background-color: #fcf8e3; +} +a.bg-warning:hover, +a.bg-warning:focus { + background-color: #f7ecb5; +} +.bg-danger { + background-color: #f2dede; +} +a.bg-danger:hover, +a.bg-danger:focus { + background-color: #e4b9b9; +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eeeeee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + 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; +} +dt, +dd { + 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[title], +abbr[data-original-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 #eeeeee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777777; +} +blockquote footer:before, +blockquote small: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 #eeeeee; + border-left: 0; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ""; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small: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, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 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: #333333; + 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-xs-1, +.col-sm-1, +.col-md-1, +.col-lg-1, +.col-xs-2, +.col-sm-2, +.col-md-2, +.col-lg-2, +.col-xs-3, +.col-sm-3, +.col-md-3, +.col-lg-3, +.col-xs-4, +.col-sm-4, +.col-md-4, +.col-lg-4, +.col-xs-5, +.col-sm-5, +.col-md-5, +.col-lg-5, +.col-xs-6, +.col-sm-6, +.col-md-6, +.col-lg-6, +.col-xs-7, +.col-sm-7, +.col-md-7, +.col-lg-7, +.col-xs-8, +.col-sm-8, +.col-md-8, +.col-lg-8, +.col-xs-9, +.col-sm-9, +.col-md-9, +.col-lg-9, +.col-xs-10, +.col-sm-10, +.col-md-10, +.col-lg-10, +.col-xs-11, +.col-sm-11, +.col-md-11, +.col-lg-11, +.col-xs-12, +.col-sm-12, +.col-md-12, +.col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} +.col-xs-1, +.col-xs-2, +.col-xs-3, +.col-xs-4, +.col-xs-5, +.col-xs-6, +.col-xs-7, +.col-xs-8, +.col-xs-9, +.col-xs-10, +.col-xs-11, +.col-xs-12 { + 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-2, + .col-sm-3, + .col-sm-4, + .col-sm-5, + .col-sm-6, + .col-sm-7, + .col-sm-8, + .col-sm-9, + .col-sm-10, + .col-sm-11, + .col-sm-12 { + 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-2, + .col-md-3, + .col-md-4, + .col-md-5, + .col-md-6, + .col-md-7, + .col-md-8, + .col-md-9, + .col-md-10, + .col-md-11, + .col-md-12 { + 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-2, + .col-lg-3, + .col-lg-4, + .col-lg-5, + .col-lg-6, + .col-lg-7, + .col-lg-8, + .col-lg-9, + .col-lg-10, + .col-lg-11, + .col-lg-12 { + 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: #777777; + text-align: left; +} +th { + text-align: left; +} +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + 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 > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} +.table > tbody + tbody { + border-top: 2px solid #ddd; +} +.table .table { + background-color: #fff; +} +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} +.table-bordered { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #d9edf7; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; +} +.table-responsive { + min-height: 0.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 > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + 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: #333333; + 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="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + line-height: normal; +} +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"].disabled, +input[type="checkbox"].disabled, +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"] { + cursor: not-allowed; +} +input[type="file"] { + display: block; +} +input[type="range"] { + display: block; + width: 100%; +} +select[multiple], +select[size] { + height: auto; +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]: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: #555555; +} +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 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, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.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: #eeeeee; + 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="time"].form-control, + input[type="datetime-local"].form-control, + input[type="month"].form-control { + line-height: 34px; + } + input[type="date"].input-sm, + input[type="time"].input-sm, + input[type="datetime-local"].input-sm, + input[type="month"].input-sm, + .input-group-sm input[type="date"], + .input-group-sm input[type="time"], + .input-group-sm input[type="datetime-local"], + .input-group-sm input[type="month"] { + line-height: 30px; + } + input[type="date"].input-lg, + input[type="time"].input-lg, + input[type="datetime-local"].input-lg, + input[type="month"].input-lg, + .input-group-lg input[type="date"], + .input-group-lg input[type="time"], + .input-group-lg input[type="datetime-local"], + .input-group-lg input[type="month"] { + line-height: 46px; + } +} +.form-group { + margin-bottom: 15px; +} +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +.radio.disabled label, +.checkbox.disabled label, +fieldset[disabled] .radio label, +fieldset[disabled] .checkbox label { + cursor: not-allowed; +} +.radio label, +.checkbox label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: 400; + cursor: pointer; +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-top: 4px \9; + margin-left: -20px; +} +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} +.radio-inline, +.checkbox-inline { + position: relative; + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: 400; + vertical-align: middle; + cursor: pointer; +} +.radio-inline.disabled, +.checkbox-inline.disabled, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-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; +} +textarea.input-sm, +select[multiple].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 textarea.form-control, +.form-group-sm select[multiple].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; +} +textarea.input-lg, +select[multiple].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 textarea.form-control, +.form-group-lg select[multiple].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; +} +.input-lg + .form-control-feedback, +.input-group-lg + .form-control-feedback, +.form-group-lg .form-control + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; +} +.input-sm + .form-control-feedback, +.input-group-sm + .form-control-feedback, +.form-group-sm .form-control + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #3c763d; +} +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 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 .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #8a6d3b; +} +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 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 .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #a94442; +} +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 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 .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} +.form-horizontal .radio, +.form-horizontal .checkbox { + 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: normal; + 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:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus, +.btn.focus { + 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, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + cursor: not-allowed; + filter: alpha(opacity=65); + opacity: 0.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:hover, +.btn-default.active:hover, +.open > .dropdown-toggle.btn-default:hover, +.btn-default:active:focus, +.btn-default.active:focus, +.open > .dropdown-toggle.btn-default:focus, +.btn-default:active.focus, +.btn-default.active.focus, +.open > .dropdown-toggle.btn-default.focus { + color: #333; + background-color: #d4d4d4; + border-color: #8c8c8c; +} +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus { + 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:hover, +.btn-primary.active:hover, +.open > .dropdown-toggle.btn-primary:hover, +.btn-primary:active:focus, +.btn-primary.active:focus, +.open > .dropdown-toggle.btn-primary:focus, +.btn-primary:active.focus, +.btn-primary.active.focus, +.open > .dropdown-toggle.btn-primary.focus { + color: #fff; + background-color: #204d74; + border-color: #122b40; +} +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus { + 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:hover, +.btn-success.active:hover, +.open > .dropdown-toggle.btn-success:hover, +.btn-success:active:focus, +.btn-success.active:focus, +.open > .dropdown-toggle.btn-success:focus, +.btn-success:active.focus, +.btn-success.active.focus, +.open > .dropdown-toggle.btn-success.focus { + color: #fff; + background-color: #398439; + border-color: #255625; +} +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus { + 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:hover, +.btn-info.active:hover, +.open > .dropdown-toggle.btn-info:hover, +.btn-info:active:focus, +.btn-info.active:focus, +.open > .dropdown-toggle.btn-info:focus, +.btn-info:active.focus, +.btn-info.active.focus, +.open > .dropdown-toggle.btn-info.focus { + color: #fff; + background-color: #269abc; + border-color: #1b6d85; +} +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus { + 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:hover, +.btn-warning.active:hover, +.open > .dropdown-toggle.btn-warning:hover, +.btn-warning:active:focus, +.btn-warning.active:focus, +.open > .dropdown-toggle.btn-warning:focus, +.btn-warning:active.focus, +.btn-warning.active.focus, +.open > .dropdown-toggle.btn-warning.focus { + color: #fff; + background-color: #d58512; + border-color: #985f0d; +} +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus { + 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:hover, +.btn-danger.active:hover, +.open > .dropdown-toggle.btn-danger:hover, +.btn-danger:active:focus, +.btn-danger.active:focus, +.open > .dropdown-toggle.btn-danger:focus, +.btn-danger:active.focus, +.btn-danger.active.focus, +.open > .dropdown-toggle.btn-danger.focus { + color: #fff; + background-color: #ac2925; + border-color: #761c19; +} +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus { + 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:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} +.btn-link:hover, +.btn-link:focus { + color: #23527c; + text-decoration: underline; + background-color: transparent; +} +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #777777; + text-decoration: none; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-xs, +.btn-group-xs > .btn { + 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="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.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: 0.35s; + -o-transition-duration: 0.35s; + transition-duration: 0.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; +} +.dropup, +.dropdown { + 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, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 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: #333333; + white-space: nowrap; +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #fff; + text-decoration: none; + background-color: #337ab7; + outline: 0; +} +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #777777; +} +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + 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: #777777; + 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 > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + 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, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 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="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + 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; +} +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].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; +} +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { + height: auto; +} +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control: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: #555555; + text-align: center; + background-color: #eeeeee; + 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="radio"], +.input-group-addon input[type="checkbox"] { + 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:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + 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:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + 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:hover, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active { + 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:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} +.nav > li.disabled > a { + color: #777777; +} +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #777777; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eeeeee; + 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: #eeeeee #eeeeee #ddd; +} +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555555; + 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:hover, +.nav-tabs.nav-justified > .active > a:focus { + 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:hover, + .nav-tabs.nav-justified > .active > a:focus { + 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:hover, +.nav-pills > li.active > a:focus { + 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:hover, +.nav-tabs-justified > .active > a:focus { + 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:hover, + .nav-tabs-justified > .active > a:focus { + 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, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.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-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-right: 0; + padding-left: 0; + } +} +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + 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 > .navbar-header, +.container-fluid > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .container > .navbar-header, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-collapse { + 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:hover, +.navbar-brand:focus { + 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 > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + 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, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.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 .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { + width: auto; + } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + 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:hover, +.navbar-default .navbar-brand:focus { + 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:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333; + background-color: transparent; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555; + background-color: #e7e7e7; +} +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #ccc; + background-color: transparent; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + 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:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #ccc; + background-color: transparent; + } +} +.navbar-default .navbar-toggle { + border-color: #ddd; +} +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + 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:hover, +.navbar-default .btn-link:focus { + color: #333; +} +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #ccc; +} +.navbar-inverse { + background-color: #222; + border-color: #080808; +} +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + 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:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #fff; + background-color: #080808; +} +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + 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:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444; + background-color: transparent; + } +} +.navbar-inverse .navbar-toggle { + border-color: #333; +} +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + 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:hover, +.navbar-inverse .btn-link:focus { + color: #fff; +} +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:focus { + 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: #777777; +} +.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:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + z-index: 2; + color: #23527c; + background-color: #eeeeee; + 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 > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 3; + color: #fff; + cursor: default; + background-color: #337ab7; + border-color: #337ab7; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #777777; + 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:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} +.pager .next > a, +.pager .next > span { + float: right; +} +.pager .previous > a, +.pager .previous > span { + float: left; +} +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #777777; + cursor: not-allowed; + background-color: #fff; +} +.label { + display: inline; + padding: 0.2em 0.6em 0.3em; + font-size: 75%; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25em; +} +a.label:hover, +a.label:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.label:empty { + display: none; +} +.btn .label { + position: relative; + top: -1px; +} +.label-default { + background-color: #777777; +} +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #5e5e5e; +} +.label-primary { + background-color: #337ab7; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #286090; +} +.label-success { + background-color: #5cb85c; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} +.label-info { + background-color: #5bc0de; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} +.label-warning { + background-color: #f0ad4e; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} +.label-danger { + background-color: #d9534f; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: middle; + background-color: #777777; + border-radius: 10px; +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.btn-xs .badge, +.btn-group-xs > .btn .badge { + top: 0; + padding: 1px 5px; +} +a.badge:hover, +a.badge:focus { + 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: #eeeeee; +} +.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 0.2s ease-in-out; + -o-transition: border 0.2s ease-in-out; + transition: border 0.2s ease-in-out; +} +.thumbnail > img, +.thumbnail a > img { + margin-right: auto; + margin-left: auto; +} +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #337ab7; +} +.thumbnail .caption { + padding: 9px; + color: #333333; +} +.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: bold; +} +.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, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 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, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} +.progress-striped .progress-bar, +.progress-bar-striped { + 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); + 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); + 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); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .progress-bar, +.progress-bar.active { + -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, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + 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); + 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); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + 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); + 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); + 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); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + 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); + 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); + 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); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + 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); + 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); + 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); +} +.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-left, +.media-right, +.media-body { + 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:hover, +.list-group-item.disabled:focus { + color: #777777; + cursor: not-allowed; + background-color: #eeeeee; +} +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading { + color: inherit; +} +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text { + color: #777777; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > .small { + color: inherit; +} +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .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:hover, +button.list-group-item:hover, +a.list-group-item:focus, +button.list-group-item:focus { + 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:hover, +button.list-group-item-success:hover, +a.list-group-item-success:focus, +button.list-group-item-success:focus { + color: #3c763d; + background-color: #d0e9c6; +} +a.list-group-item-success.active, +button.list-group-item-success.active, +a.list-group-item-success.active:hover, +button.list-group-item-success.active:hover, +a.list-group-item-success.active:focus, +button.list-group-item-success.active:focus { + 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:hover, +button.list-group-item-info:hover, +a.list-group-item-info:focus, +button.list-group-item-info:focus { + color: #31708f; + background-color: #c4e3f3; +} +a.list-group-item-info.active, +button.list-group-item-info.active, +a.list-group-item-info.active:hover, +button.list-group-item-info.active:hover, +a.list-group-item-info.active:focus, +button.list-group-item-info.active:focus { + 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:hover, +button.list-group-item-warning:hover, +a.list-group-item-warning:focus, +button.list-group-item-warning:focus { + color: #8a6d3b; + background-color: #faf2cc; +} +a.list-group-item-warning.active, +button.list-group-item-warning.active, +a.list-group-item-warning.active:hover, +button.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus, +button.list-group-item-warning.active:focus { + 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:hover, +button.list-group-item-danger:hover, +a.list-group-item-danger:focus, +button.list-group-item-danger:focus { + color: #a94442; + background-color: #ebcccc; +} +a.list-group-item-danger.active, +button.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +button.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus, +button.list-group-item-danger.active:focus { + 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, 0.05); + box-shadow: 0 1px 1px rgba(0, 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 > a, +.panel-title > small, +.panel-title > .small, +.panel-title > small > a, +.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 > .table, +.panel > .table-responsive > .table, +.panel > .panel-collapse > .table { + margin-bottom: 0; +} +.panel > .table caption, +.panel > .table-responsive > .table caption, +.panel > .panel-collapse > .table caption { + padding-right: 15px; + padding-left: 15px; +} +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead: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-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .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 td:first-child, +.panel > .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 td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th: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 th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: 3px; +} +.panel > .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 td:last-child, +.panel > .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 td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th: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 th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; +} +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .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 td:first-child, +.panel > .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 td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: 3px; +} +.panel > .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 td:last-child, +.panel > .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 td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .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 th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; +} +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; +} +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; +} +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.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 > .panel-body, +.panel-group .panel-heading + .panel-collapse > .list-group { + 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: #333333; + 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: #333333; +} +.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 iframe, +.embed-responsive embed, +.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, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 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: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: 0.2; +} +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: 0.5; +} +button.close { + padding: 0; + cursor: pointer; + background: transparent; + 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 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + transition: -webkit-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out, -o-transform 0.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, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 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: 0.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, 0.5); + box-shadow: 0 5px 15px rgba(0, 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: 0.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, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 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: #999999; + border-top-color: rgba(0, 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: #999999; + border-right-color: rgba(0, 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: #999999; + border-bottom-color: rgba(0, 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: #999999; + border-left-color: rgba(0, 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: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + line-height: 1; +} +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform 0.6s ease-in-out; + -o-transition: -o-transform 0.6s ease-in-out; + transition: -webkit-transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out, -o-transform 0.6s ease-in-out; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000px; + perspective: 1000px; + } + .carousel-inner > .item.next, + .carousel-inner > .item.active.right { + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + left: 0; + } + .carousel-inner > .item.prev, + .carousel-inner > .item.active.left { + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + left: 0; + } + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right, + .carousel-inner > .item.active { + -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, 0.6); + background-color: rgba(0, 0, 0, 0); + filter: alpha(opacity=50); + opacity: 0.5; +} +.carousel-control.left { + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001))); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 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, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5))); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control:hover, +.carousel-control:focus { + color: #fff; + text-decoration: none; + outline: 0; + filter: alpha(opacity=90); + opacity: 0.9; +} +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + margin-top: -10px; +} +.carousel-control .icon-prev, +.carousel-control .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; +} +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; +} +.carousel-control .icon-prev, +.carousel-control .icon-next { + 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, 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-prev, + .carousel-control .icon-next { + 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; + } +} +.clearfix:before, +.clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, +.container:before, +.container:after, +.container-fluid:before, +.container-fluid:after, +.row:before, +.row:after, +.form-horizontal .form-group:before, +.form-horizontal .form-group:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after, +.nav:before, +.nav:after, +.navbar:before, +.navbar:after, +.navbar-header:before, +.navbar-header:after, +.navbar-collapse:before, +.navbar-collapse:after, +.pager:before, +.pager:after, +.panel-body:before, +.panel-body:after, +.modal-header:before, +.modal-header:after, +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} +.clearfix:after, +.dl-horizontal dd:after, +.container:after, +.container-fluid:after, +.row:after, +.form-horizontal .form-group:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after, +.nav:after, +.navbar:after, +.navbar-header:after, +.navbar-collapse:after, +.pager:after, +.panel-body:after, +.modal-header:after, +.modal-footer: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-xs, +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-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; + } + th.visible-xs, + td.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; + } + th.visible-sm, + td.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; + } + th.visible-md, + td.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; + } + th.visible-lg, + td.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; + } + th.visible-print, + td.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.css.map */ diff --git a/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap.css.map b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap.css.map new file mode 100644 index 0000000..caac3e6 --- /dev/null +++ b/bnhz-plugs/bnhz-oauth/src/main/resources/static/oauth/css/bootstrap.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap.css","less/normalize.less","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;;;;GAIG;AACH,4EAA4E;ACK5E;EACE,wBAAA;EACA,2BAAA;EACA,+BAAA;CDHD;ACUD;EACE,UAAA;CDRD;ACqBD;;;;;;;;;;;;;EAaE,eAAA;CDnBD;AC2BD;;;;EAIE,sBAAA;EACA,yBAAA;CDzBD;ACiCD;EACE,cAAA;EACA,UAAA;CD/BD;ACuCD;;EAEE,cAAA;CDrCD;AC+CD;EACE,8BAAA;CD7CD;ACqDD;;EAEE,WAAA;CDnDD;AC8DD;EACE,oBAAA;EACA,2BAAA;EACA,0CAAA;EAAA,uCAAA;EAAA,kCAAA;CD5DD;ACmED;;EAEE,kBAAA;CDjED;ACwED;EACE,mBAAA;CDtED;AC8ED;EACE,eAAA;EACA,iBAAA;CD5ED;ACmFD;EACE,iBAAA;EACA,YAAA;CDjFD;ACwFD;EACE,eAAA;CDtFD;AC6FD;;EAEE,eAAA;EACA,eAAA;EACA,mBAAA;EACA,yBAAA;CD3FD;AC8FD;EACE,YAAA;CD5FD;AC+FD;EACE,gBAAA;CD7FD;ACuGD;EACE,UAAA;CDrGD;AC4GD;EACE,iBAAA;CD1GD;ACoHD;EACE,iBAAA;CDlHD;ACyHD;EACE,gCAAA;EAAA,6BAAA;EAAA,wBAAA;EACA,UAAA;CDvHD;AC8HD;EACE,eAAA;CD5HD;ACmID;;;;EAIE,kCAAA;EACA,eAAA;CDjID;ACmJD;;;;;EAKE,eAAA;EACA,cAAA;EACA,UAAA;CDjJD;ACwJD;EACE,kBAAA;CDtJD;ACgKD;;EAEE,qBAAA;CD9JD;ACyKD;;;;EAIE,2BAAA;EACA,gBAAA;CDvKD;AC8KD;;EAEE,gBAAA;CD5KD;ACmLD;;EAEE,UAAA;EACA,WAAA;CDjLD;ACyLD;EACE,oBAAA;CDvLD;ACkMD;;EAEE,+BAAA;EAAA,4BAAA;EAAA,uBAAA;EACA,WAAA;CDhMD;ACyMD;;EAEE,aAAA;CDvMD;AC+MD;EACE,8BAAA;EACA,gCAAA;EAAA,6BAAA;EAAA,wBAAA;CD7MD;ACsND;;EAEE,yBAAA;CDpND;AC2ND;EACE,0BAAA;EACA,cAAA;EACA,+BAAA;CDzND;ACiOD;EACE,UAAA;EACA,WAAA;CD/ND;ACsOD;EACE,eAAA;CDpOD;AC4OD;EACE,kBAAA;CD1OD;ACoPD;EACE,0BAAA;EACA,kBAAA;CDlPD;ACqPD;;EAEE,WAAA;CDnPD;AACD,qFAAqF;AEhLrF;EACE;;;IAGE,uBAAA;IACA,6BAAA;IACA,mCAAA;IACA,oCAAA;IAAA,4BAAA;GFkLD;EE/KD;;IAEE,2BAAA;GFiLD;EE9KD;IACE,6BAAA;GFgLD;EE7KD;IACE,8BAAA;GF+KD;EE1KD;;IAEE,YAAA;GF4KD;EEzKD;;IAEE,uBAAA;IACA,yBAAA;GF2KD;EExKD;IACE,4BAAA;GF0KD;EEvKD;;IAEE,yBAAA;GFyKD;EEtKD;IACE,2BAAA;GFwKD;EErKD;;;IAGE,WAAA;IACA,UAAA;GFuKD;EEpKD;;IAEE,wBAAA;GFsKD;EEhKD;IACE,cAAA;GFkKD;EEhKD;;IAGI,kCAAA;GFiKH;EE9JD;IACE,uBAAA;GFgKD;EE7JD;IACE,qCAAA;GF+JD;EEhKD;;IAKI,kCAAA;GF+JH;EE5JD;;IAGI,kCAAA;GF6JH;CACF;AGnPD;EACE,oCAAA;EACA,sDAAA;EACA,gYAAA;CHqPD;AG7OD;EACE,mBAAA;EACA,SAAA;EACA,sBAAA;EACA,oCAAA;EACA,mBAAA;EACA,iBAAA;EACA,eAAA;EACA,oCAAA;EACA,mCAAA;CH+OD;AG3OmC;EAAW,iBAAA;CH8O9C;AG7OmC;EAAW,iBAAA;CHgP9C;AG9OmC;;EAAW,iBAAA;CHkP9C;AGjPmC;EAAW,iBAAA;CHoP9C;AGnPmC;EAAW,iBAAA;CHsP9C;AGrPmC;EAAW,iBAAA;CHwP9C;AGvPmC;EAAW,iBAAA;CH0P9C;AGzPmC;EAAW,iBAAA;CH4P9C;AG3PmC;EAAW,iBAAA;CH8P9C;AG7PmC;EAAW,iBAAA;CHgQ9C;AG/PmC;EAAW,iBAAA;CHkQ9C;AGjQmC;EAAW,iBAAA;CHoQ9C;AGnQmC;EAAW,iBAAA;CHsQ9C;AGrQmC;EAAW,iBAAA;CHwQ9C;AGvQmC;EAAW,iBAAA;CH0Q9C;AGzQmC;EAAW,iBAAA;CH4Q9C;AG3QmC;EAAW,iBAAA;CH8Q9C;AG7QmC;EAAW,iBAAA;CHgR9C;AG/QmC;EAAW,iBAAA;CHkR9C;AGjRmC;EAAW,iBAAA;CHoR9C;AGnRmC;EAAW,iBAAA;CHsR9C;AGrRmC;EAAW,iBAAA;CHwR9C;AGvRmC;EAAW,iBAAA;CH0R9C;AGzRmC;EAAW,iBAAA;CH4R9C;AG3RmC;EAAW,iBAAA;CH8R9C;AG7RmC;EAAW,iBAAA;CHgS9C;AG/RmC;EAAW,iBAAA;CHkS9C;AGjSmC;EAAW,iBAAA;CHoS9C;AGnSmC;EAAW,iBAAA;CHsS9C;AGrSmC;EAAW,iBAAA;CHwS9C;AGvSmC;EAAW,iBAAA;CH0S9C;AGzSmC;EAAW,iBAAA;CH4S9C;AG3SmC;EAAW,iBAAA;CH8S9C;AG7SmC;EAAW,iBAAA;CHgT9C;AG/SmC;EAAW,iBAAA;CHkT9C;AGjTmC;EAAW,iBAAA;CHoT9C;AGnTmC;EAAW,iBAAA;CHsT9C;AGrTmC;EAAW,iBAAA;CHwT9C;AGvTmC;EAAW,iBAAA;CH0T9C;AGzTmC;EAAW,iBAAA;CH4T9C;AG3TmC;EAAW,iBAAA;CH8T9C;AG7TmC;EAAW,iBAAA;CHgU9C;AG/TmC;EAAW,iBAAA;CHkU9C;AGjUmC;EAAW,iBAAA;CHoU9C;AGnUmC;EAAW,iBAAA;CHsU9C;AGrUmC;EAAW,iBAAA;CHwU9C;AGvUmC;EAAW,iBAAA;CH0U9C;AGzUmC;EAAW,iBAAA;CH4U9C;AG3UmC;EAAW,iBAAA;CH8U9C;AG7UmC;EAAW,iBAAA;CHgV9C;AG/UmC;EAAW,iBAAA;CHkV9C;AGjVmC;EAAW,iBAAA;CHoV9C;AGnVmC;EAAW,iBAAA;CHsV9C;AGrVmC;EAAW,iBAAA;CHwV9C;AGvVmC;EAAW,iBAAA;CH0V9C;AGzVmC;EAAW,iBAAA;CH4V9C;AG3VmC;EAAW,iBAAA;CH8V9C;AG7VmC;EAAW,iBAAA;CHgW9C;AG/VmC;EAAW,iBAAA;CHkW9C;AGjWmC;EAAW,iBAAA;CHoW9C;AGnWmC;EAAW,iBAAA;CHsW9C;AGrWmC;EAAW,iBAAA;CHwW9C;AGvWmC;EAAW,iBAAA;CH0W9C;AGzWmC;EAAW,iBAAA;CH4W9C;AG3WmC;EAAW,iBAAA;CH8W9C;AG7WmC;EAAW,iBAAA;CHgX9C;AG/WmC;EAAW,iBAAA;CHkX9C;AGjXmC;EAAW,iBAAA;CHoX9C;AGnXmC;EAAW,iBAAA;CHsX9C;AGrXmC;EAAW,iBAAA;CHwX9C;AGvXmC;EAAW,iBAAA;CH0X9C;AGzXmC;EAAW,iBAAA;CH4X9C;AG3XmC;EAAW,iBAAA;CH8X9C;AG7XmC;EAAW,iBAAA;CHgY9C;AG/XmC;EAAW,iBAAA;CHkY9C;AGjYmC;EAAW,iBAAA;CHoY9C;AGnYmC;EAAW,iBAAA;CHsY9C;AGrYmC;EAAW,iBAAA;CHwY9C;AGvYmC;EAAW,iBAAA;CH0Y9C;AGzYmC;EAAW,iBAAA;CH4Y9C;AG3YmC;EAAW,iBAAA;CH8Y9C;AG7YmC;EAAW,iBAAA;CHgZ9C;AG/YmC;EAAW,iBAAA;CHkZ9C;AGjZmC;EAAW,iBAAA;CHoZ9C;AGnZmC;EAAW,iBAAA;CHsZ9C;AGrZmC;EAAW,iBAAA;CHwZ9C;AGvZmC;EAAW,iBAAA;CH0Z9C;AGzZmC;EAAW,iBAAA;CH4Z9C;AG3ZmC;EAAW,iBAAA;CH8Z9C;AG7ZmC;EAAW,iBAAA;CHga9C;AG/ZmC;EAAW,iBAAA;CHka9C;AGjamC;EAAW,iBAAA;CHoa9C;AGnamC;EAAW,iBAAA;CHsa9C;AGramC;EAAW,iBAAA;CHwa9C;AGvamC;EAAW,iBAAA;CH0a9C;AGzamC;EAAW,iBAAA;CH4a9C;AG3amC;EAAW,iBAAA;CH8a9C;AG7amC;EAAW,iBAAA;CHgb9C;AG/amC;EAAW,iBAAA;CHkb9C;AGjbmC;EAAW,iBAAA;CHob9C;AGnbmC;EAAW,iBAAA;CHsb9C;AGrbmC;EAAW,iBAAA;CHwb9C;AGvbmC;EAAW,iBAAA;CH0b9C;AGzbmC;EAAW,iBAAA;CH4b9C;AG3bmC;EAAW,iBAAA;CH8b9C;AG7bmC;EAAW,iBAAA;CHgc9C;AG/bmC;EAAW,iBAAA;CHkc9C;AGjcmC;EAAW,iBAAA;CHoc9C;AGncmC;EAAW,iBAAA;CHsc9C;AGrcmC;EAAW,iBAAA;CHwc9C;AGvcmC;EAAW,iBAAA;CH0c9C;AGzcmC;EAAW,iBAAA;CH4c9C;AG3cmC;EAAW,iBAAA;CH8c9C;AG7cmC;EAAW,iBAAA;CHgd9C;AG/cmC;EAAW,iBAAA;CHkd9C;AGjdmC;EAAW,iBAAA;CHod9C;AGndmC;EAAW,iBAAA;CHsd9C;AGrdmC;EAAW,iBAAA;CHwd9C;AGvdmC;EAAW,iBAAA;CH0d9C;AGzdmC;EAAW,iBAAA;CH4d9C;AG3dmC;EAAW,iBAAA;CH8d9C;AG7dmC;EAAW,iBAAA;CHge9C;AG/dmC;EAAW,iBAAA;CHke9C;AGjemC;EAAW,iBAAA;CHoe9C;AGnemC;EAAW,iBAAA;CHse9C;AGremC;EAAW,iBAAA;CHwe9C;AGvemC;EAAW,iBAAA;CH0e9C;AGzemC;EAAW,iBAAA;CH4e9C;AG3emC;EAAW,iBAAA;CH8e9C;AG7emC;EAAW,iBAAA;CHgf9C;AG/emC;EAAW,iBAAA;CHkf9C;AGjfmC;EAAW,iBAAA;CHof9C;AGnfmC;EAAW,iBAAA;CHsf9C;AGrfmC;EAAW,iBAAA;CHwf9C;AGvfmC;EAAW,iBAAA;CH0f9C;AGzfmC;EAAW,iBAAA;CH4f9C;AG3fmC;EAAW,iBAAA;CH8f9C;AG7fmC;EAAW,iBAAA;CHggB9C;AG/fmC;EAAW,iBAAA;CHkgB9C;AGjgBmC;EAAW,iBAAA;CHogB9C;AGngBmC;EAAW,iBAAA;CHsgB9C;AGrgBmC;EAAW,iBAAA;CHwgB9C;AGvgBmC;EAAW,iBAAA;CH0gB9C;AGzgBmC;EAAW,iBAAA;CH4gB9C;AG3gBmC;EAAW,iBAAA;CH8gB9C;AG7gBmC;EAAW,iBAAA;CHghB9C;AG/gBmC;EAAW,iBAAA;CHkhB9C;AGjhBmC;EAAW,iBAAA;CHohB9C;AGnhBmC;EAAW,iBAAA;CHshB9C;AGrhBmC;EAAW,iBAAA;CHwhB9C;AGvhBmC;EAAW,iBAAA;CH0hB9C;AGzhBmC;EAAW,iBAAA;CH4hB9C;AG3hBmC;EAAW,iBAAA;CH8hB9C;AG7hBmC;EAAW,iBAAA;CHgiB9C;AG/hBmC;EAAW,iBAAA;CHkiB9C;AGjiBmC;EAAW,iBAAA;CHoiB9C;AGniBmC;EAAW,iBAAA;CHsiB9C;AGriBmC;EAAW,iBAAA;CHwiB9C;AGviBmC;EAAW,iBAAA;CH0iB9C;AGziBmC;EAAW,iBAAA;CH4iB9C;AG3iBmC;EAAW,iBAAA;CH8iB9C;AG7iBmC;EAAW,iBAAA;CHgjB9C;AG/iBmC;EAAW,iBAAA;CHkjB9C;AGjjBmC;EAAW,iBAAA;CHojB9C;AGnjBmC;EAAW,iBAAA;CHsjB9C;AGrjBmC;EAAW,iBAAA;CHwjB9C;AGvjBmC;EAAW,iBAAA;CH0jB9C;AGzjBmC;EAAW,iBAAA;CH4jB9C;AG3jBmC;EAAW,iBAAA;CH8jB9C;AG7jBmC;EAAW,iBAAA;CHgkB9C;AG/jBmC;EAAW,iBAAA;CHkkB9C;AGjkBmC;EAAW,iBAAA;CHokB9C;AGnkBmC;EAAW,iBAAA;CHskB9C;AGrkBmC;EAAW,iBAAA;CHwkB9C;AGvkBmC;EAAW,iBAAA;CH0kB9C;AGzkBmC;EAAW,iBAAA;CH4kB9C;AG3kBmC;EAAW,iBAAA;CH8kB9C;AG7kBmC;EAAW,iBAAA;CHglB9C;AG/kBmC;EAAW,iBAAA;CHklB9C;AGjlBmC;EAAW,iBAAA;CHolB9C;AGnlBmC;EAAW,iBAAA;CHslB9C;AGrlBmC;EAAW,iBAAA;CHwlB9C;AGvlBmC;EAAW,iBAAA;CH0lB9C;AGzlBmC;EAAW,iBAAA;CH4lB9C;AG3lBmC;EAAW,iBAAA;CH8lB9C;AG7lBmC;EAAW,iBAAA;CHgmB9C;AG/lBmC;EAAW,iBAAA;CHkmB9C;AGjmBmC;EAAW,iBAAA;CHomB9C;AGnmBmC;EAAW,iBAAA;CHsmB9C;AGrmBmC;EAAW,iBAAA;CHwmB9C;AGvmBmC;EAAW,iBAAA;CH0mB9C;AGzmBmC;EAAW,iBAAA;CH4mB9C;AG3mBmC;EAAW,iBAAA;CH8mB9C;AG7mBmC;EAAW,iBAAA;CHgnB9C;AG/mBmC;EAAW,iBAAA;CHknB9C;AGjnBmC;EAAW,iBAAA;CHonB9C;AGnnBmC;EAAW,iBAAA;CHsnB9C;AGrnBmC;EAAW,iBAAA;CHwnB9C;AGvnBmC;EAAW,iBAAA;CH0nB9C;AGznBmC;EAAW,iBAAA;CH4nB9C;AG3nBmC;EAAW,iBAAA;CH8nB9C;AG7nBmC;EAAW,iBAAA;CHgoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AGvoBmC;EAAW,iBAAA;CH0oB9C;AGzoBmC;EAAW,iBAAA;CH4oB9C;AG3oBmC;EAAW,iBAAA;CH8oB9C;AG7oBmC;EAAW,iBAAA;CHgpB9C;AG/oBmC;EAAW,iBAAA;CHkpB9C;AGjpBmC;EAAW,iBAAA;CHopB9C;AGnpBmC;EAAW,iBAAA;CHspB9C;AGrpBmC;EAAW,iBAAA;CHwpB9C;AGvpBmC;EAAW,iBAAA;CH0pB9C;AGzpBmC;EAAW,iBAAA;CH4pB9C;AG3pBmC;EAAW,iBAAA;CH8pB9C;AG7pBmC;EAAW,iBAAA;CHgqB9C;AG/pBmC;EAAW,iBAAA;CHkqB9C;AGjqBmC;EAAW,iBAAA;CHoqB9C;AGnqBmC;EAAW,iBAAA;CHsqB9C;AGrqBmC;EAAW,iBAAA;CHwqB9C;AGvqBmC;EAAW,iBAAA;CH0qB9C;AGzqBmC;EAAW,iBAAA;CH4qB9C;AG3qBmC;EAAW,iBAAA;CH8qB9C;AG7qBmC;EAAW,iBAAA;CHgrB9C;AG/qBmC;EAAW,iBAAA;CHkrB9C;AGjrBmC;EAAW,iBAAA;CHorB9C;AGnrBmC;EAAW,iBAAA;CHsrB9C;AGrrBmC;EAAW,iBAAA;CHwrB9C;AGvrBmC;EAAW,iBAAA;CH0rB9C;AGzrBmC;EAAW,iBAAA;CH4rB9C;AG3rBmC;EAAW,iBAAA;CH8rB9C;AG7rBmC;EAAW,iBAAA;CHgsB9C;AG/rBmC;EAAW,iBAAA;CHksB9C;AGjsBmC;EAAW,iBAAA;CHosB9C;AGnsBmC;EAAW,iBAAA;CHssB9C;AGrsBmC;EAAW,iBAAA;CHwsB9C;AGvsBmC;EAAW,iBAAA;CH0sB9C;AGzsBmC;EAAW,iBAAA;CH4sB9C;AG3sBmC;EAAW,iBAAA;CH8sB9C;AG7sBmC;EAAW,iBAAA;CHgtB9C;AG/sBmC;EAAW,iBAAA;CHktB9C;AGjtBmC;EAAW,iBAAA;CHotB9C;AGntBmC;EAAW,iBAAA;CHstB9C;AGrtBmC;EAAW,iBAAA;CHwtB9C;AGvtBmC;EAAW,iBAAA;CH0tB9C;AGztBmC;EAAW,iBAAA;CH4tB9C;AG3tBmC;EAAW,iBAAA;CH8tB9C;AG7tBmC;EAAW,iBAAA;CHguB9C;AG/tBmC;EAAW,iBAAA;CHkuB9C;AGjuBmC;EAAW,iBAAA;CHouB9C;AGnuBmC;EAAW,iBAAA;CHsuB9C;AGruBmC;EAAW,iBAAA;CHwuB9C;AGvuBmC;EAAW,iBAAA;CH0uB9C;AGzuBmC;EAAW,iBAAA;CH4uB9C;AG3uBmC;EAAW,iBAAA;CH8uB9C;AG7uBmC;EAAW,iBAAA;CHgvB9C;AIxhCD;ECkEE,+BAAA;EACG,4BAAA;EACK,uBAAA;CLy9BT;AI1hCD;;EC+DE,+BAAA;EACG,4BAAA;EACK,uBAAA;CL+9BT;AIxhCD;EACE,gBAAA;EACA,8CAAA;CJ0hCD;AIvhCD;EACE,4DAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,uBAAA;CJyhCD;AIrhCD;;;;EAIE,qBAAA;EACA,mBAAA;EACA,qBAAA;CJuhCD;AIjhCD;EACE,eAAA;EACA,sBAAA;CJmhCD;AIjhCC;;EAEE,eAAA;EACA,2BAAA;CJmhCH;AIhhCC;EEnDA,2CAAA;EACA,qBAAA;CNskCD;AIzgCD;EACE,UAAA;CJ2gCD;AIrgCD;EACE,uBAAA;CJugCD;AIngCD;;;;;EG1EE,eAAA;EACA,gBAAA;EACA,aAAA;CPolCD;AIvgCD;EACE,mBAAA;CJygCD;AIngCD;EACE,aAAA;EACA,wBAAA;EACA,uBAAA;EACA,uBAAA;EACA,mBAAA;EC+FA,yCAAA;EACK,oCAAA;EACG,iCAAA;EE5LR,sBAAA;EACA,gBAAA;EACA,aAAA;CPomCD;AIngCD;EACE,mBAAA;CJqgCD;AI//BD;EACE,iBAAA;EACA,oBAAA;EACA,UAAA;EACA,8BAAA;CJigCD;AIz/BD;EACE,mBAAA;EACA,WAAA;EACA,YAAA;EACA,WAAA;EACA,aAAA;EACA,iBAAA;EACA,uBAAA;EACA,UAAA;CJ2/BD;AIn/BC;;EAEE,iBAAA;EACA,YAAA;EACA,aAAA;EACA,UAAA;EACA,kBAAA;EACA,WAAA;CJq/BH;AI1+BD;EACE,gBAAA;CJ4+BD;AQjoCD;;;;;;;;;;;;EAEE,qBAAA;EACA,iBAAA;EACA,iBAAA;EACA,eAAA;CR6oCD;AQlpCD;;;;;;;;;;;;;;;;;;;;;;;;EASI,iBAAA;EACA,eAAA;EACA,eAAA;CRmqCH;AQ/pCD;;;;;;EAGE,iBAAA;EACA,oBAAA;CRoqCD;AQxqCD;;;;;;;;;;;;EAQI,eAAA;CR8qCH;AQ3qCD;;;;;;EAGE,iBAAA;EACA,oBAAA;CRgrCD;AQprCD;;;;;;;;;;;;EAQI,eAAA;CR0rCH;AQtrCD;;EAAU,gBAAA;CR0rCT;AQzrCD;;EAAU,gBAAA;CR6rCT;AQ5rCD;;EAAU,gBAAA;CRgsCT;AQ/rCD;;EAAU,gBAAA;CRmsCT;AQlsCD;;EAAU,gBAAA;CRssCT;AQrsCD;;EAAU,gBAAA;CRysCT;AQnsCD;EACE,iBAAA;CRqsCD;AQlsCD;EACE,oBAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;CRosCD;AQlsCC;EAAA;IACE,gBAAA;GRqsCD;CACF;AQ7rCD;;EAEE,eAAA;CR+rCD;AQ5rCD;;EAEE,eAAA;EACA,0BAAA;CR8rCD;AQ1rCD;EAAuB,iBAAA;CR6rCtB;AQ5rCD;EAAuB,kBAAA;CR+rCtB;AQ9rCD;EAAuB,mBAAA;CRisCtB;AQhsCD;EAAuB,oBAAA;CRmsCtB;AQlsCD;EAAuB,oBAAA;CRqsCtB;AQlsCD;EAAuB,0BAAA;CRqsCtB;AQpsCD;EAAuB,0BAAA;CRusCtB;AQtsCD;EAAuB,2BAAA;CRysCtB;AQtsCD;EACE,eAAA;CRwsCD;AQtsCD;ECvGE,eAAA;CTgzCD;AS/yCC;;EAEE,eAAA;CTizCH;AQ1sCD;EC1GE,eAAA;CTuzCD;AStzCC;;EAEE,eAAA;CTwzCH;AQ9sCD;EC7GE,eAAA;CT8zCD;AS7zCC;;EAEE,eAAA;CT+zCH;AQltCD;EChHE,eAAA;CTq0CD;ASp0CC;;EAEE,eAAA;CTs0CH;AQttCD;ECnHE,eAAA;CT40CD;AS30CC;;EAEE,eAAA;CT60CH;AQttCD;EAGE,YAAA;EE7HA,0BAAA;CVo1CD;AUn1CC;;EAEE,0BAAA;CVq1CH;AQxtCD;EEhIE,0BAAA;CV21CD;AU11CC;;EAEE,0BAAA;CV41CH;AQ5tCD;EEnIE,0BAAA;CVk2CD;AUj2CC;;EAEE,0BAAA;CVm2CH;AQhuCD;EEtIE,0BAAA;CVy2CD;AUx2CC;;EAEE,0BAAA;CV02CH;AQpuCD;EEzIE,0BAAA;CVg3CD;AU/2CC;;EAEE,0BAAA;CVi3CH;AQnuCD;EACE,oBAAA;EACA,oBAAA;EACA,iCAAA;CRquCD;AQ7tCD;;EAEE,cAAA;EACA,oBAAA;CR+tCD;AQluCD;;;;EAMI,iBAAA;CRkuCH;AQ3tCD;EACE,gBAAA;EACA,iBAAA;CR6tCD;AQztCD;EALE,gBAAA;EACA,iBAAA;EAMA,kBAAA;CR4tCD;AQ9tCD;EAKI,sBAAA;EACA,mBAAA;EACA,kBAAA;CR4tCH;AQvtCD;EACE,cAAA;EACA,oBAAA;CRytCD;AQvtCD;;EAEE,wBAAA;CRytCD;AQvtCD;EACE,iBAAA;CRytCD;AQvtCD;EACE,eAAA;CRytCD;AQ5sCC;EAAA;IAEI,YAAA;IACA,aAAA;IACA,YAAA;IACA,kBAAA;IGxNJ,iBAAA;IACA,wBAAA;IACA,oBAAA;GXu6CC;EQttCD;IASI,mBAAA;GRgtCH;CACF;AQtsCD;;EAEE,aAAA;CRwsCD;AQrsCD;EACE,eAAA;EA9IqB,0BAAA;CRs1CtB;AQnsCD;EACE,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,+BAAA;CRqsCD;AQhsCG;;;EACE,iBAAA;CRosCL;AQ9sCD;;;EAmBI,eAAA;EACA,eAAA;EACA,wBAAA;EACA,eAAA;CRgsCH;AQ9rCG;;;EACE,uBAAA;CRksCL;AQ1rCD;;EAEE,oBAAA;EACA,gBAAA;EACA,kBAAA;EACA,gCAAA;EACA,eAAA;CR4rCD;AQtrCG;;;;;;EAAW,YAAA;CR8rCd;AQ7rCG;;;;;;EACE,uBAAA;CRosCL;AQ9rCD;EACE,oBAAA;EACA,mBAAA;EACA,wBAAA;CRgsCD;AYx+CD;;;;EAIE,+DAAA;CZ0+CD;AYt+CD;EACE,iBAAA;EACA,eAAA;EACA,eAAA;EACA,0BAAA;EACA,mBAAA;CZw+CD;AYp+CD;EACE,iBAAA;EACA,eAAA;EACA,YAAA;EACA,uBAAA;EACA,mBAAA;EACA,uDAAA;EAAA,+CAAA;CZs+CD;AY5+CD;EASI,WAAA;EACA,gBAAA;EACA,iBAAA;EACA,yBAAA;EAAA,iBAAA;CZs+CH;AYj+CD;EACE,eAAA;EACA,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,sBAAA;EACA,sBAAA;EACA,0BAAA;EACA,uBAAA;EACA,mBAAA;CZm+CD;AY9+CD;EAeI,WAAA;EACA,mBAAA;EACA,eAAA;EACA,sBAAA;EACA,8BAAA;EACA,iBAAA;CZk+CH;AY79CD;EACE,kBAAA;EACA,mBAAA;CZ+9CD;AazhDD;ECHE,oBAAA;EACA,mBAAA;EACA,mBAAA;EACA,kBAAA;Cd+hDD;Aa5hDC;EAAA;IACE,aAAA;Gb+hDD;CACF;Aa9hDC;EAAA;IACE,aAAA;GbiiDD;CACF;AahiDC;EAAA;IACE,cAAA;GbmiDD;CACF;Aa1hDD;ECvBE,oBAAA;EACA,mBAAA;EACA,mBAAA;EACA,kBAAA;CdojDD;AavhDD;ECvBE,oBAAA;EACA,mBAAA;CdijDD;AavhDD;EACE,gBAAA;EACA,eAAA;CbyhDD;Aa3hDD;EAKI,iBAAA;EACA,gBAAA;CbyhDH;AczkDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECiBK,mBAAA;EAEA,gBAAA;EAEA,oBAAA;EACA,mBAAA;CfwmDL;Ac9nDA;;;;;;;;;;;;ECuCK,YAAA;CfqmDL;Ac5oDA;EC+CG,YAAA;CfgmDH;Ac/oDA;EC+CG,oBAAA;CfmmDH;AclpDA;EC+CG,oBAAA;CfsmDH;AcrpDA;EC+CG,WAAA;CfymDH;AcxpDA;EC+CG,oBAAA;Cf4mDH;Ac3pDA;EC+CG,oBAAA;Cf+mDH;Ac9pDA;EC+CG,WAAA;CfknDH;AcjqDA;EC+CG,oBAAA;CfqnDH;AcpqDA;EC+CG,oBAAA;CfwnDH;AcvqDA;EC+CG,WAAA;Cf2nDH;Ac1qDA;EC+CG,oBAAA;Cf8nDH;Ac7qDA;EC+CG,mBAAA;CfioDH;AchrDA;EC8DG,YAAA;CfqnDH;AcnrDA;EC8DG,oBAAA;CfwnDH;ActrDA;EC8DG,oBAAA;Cf2nDH;AczrDA;EC8DG,WAAA;Cf8nDH;Ac5rDA;EC8DG,oBAAA;CfioDH;Ac/rDA;EC8DG,oBAAA;CfooDH;AclsDA;EC8DG,WAAA;CfuoDH;AcrsDA;EC8DG,oBAAA;Cf0oDH;AcxsDA;EC8DG,oBAAA;Cf6oDH;Ac3sDA;EC8DG,WAAA;CfgpDH;Ac9sDA;EC8DG,oBAAA;CfmpDH;AcjtDA;EC8DG,mBAAA;CfspDH;AcptDA;ECmEG,YAAA;CfopDH;AcvtDA;ECoDG,WAAA;CfsqDH;Ac1tDA;ECoDG,mBAAA;CfyqDH;Ac7tDA;ECoDG,mBAAA;Cf4qDH;AchuDA;ECoDG,UAAA;Cf+qDH;AcnuDA;ECoDG,mBAAA;CfkrDH;ActuDA;ECoDG,mBAAA;CfqrDH;AczuDA;ECoDG,UAAA;CfwrDH;Ac5uDA;ECoDG,mBAAA;Cf2rDH;Ac/uDA;ECoDG,mBAAA;Cf8rDH;AclvDA;ECoDG,UAAA;CfisDH;AcrvDA;ECoDG,mBAAA;CfosDH;AcxvDA;ECoDG,kBAAA;CfusDH;Ac3vDA;ECyDG,WAAA;CfqsDH;Ac9vDA;ECwEG,kBAAA;CfyrDH;AcjwDA;ECwEG,0BAAA;Cf4rDH;AcpwDA;ECwEG,0BAAA;Cf+rDH;AcvwDA;ECwEG,iBAAA;CfksDH;Ac1wDA;ECwEG,0BAAA;CfqsDH;Ac7wDA;ECwEG,0BAAA;CfwsDH;AchxDA;ECwEG,iBAAA;Cf2sDH;AcnxDA;ECwEG,0BAAA;Cf8sDH;ActxDA;ECwEG,0BAAA;CfitDH;AczxDA;ECwEG,iBAAA;CfotDH;Ac5xDA;ECwEG,0BAAA;CfutDH;Ac/xDA;ECwEG,yBAAA;Cf0tDH;AclyDA;ECwEG,gBAAA;Cf6tDH;Aa5tDD;ECzEC;;;;;;;;;;;;ICuCK,YAAA;Gf6wDH;EcpzDF;IC+CG,YAAA;GfwwDD;EcvzDF;IC+CG,oBAAA;Gf2wDD;Ec1zDF;IC+CG,oBAAA;Gf8wDD;Ec7zDF;IC+CG,WAAA;GfixDD;Ech0DF;IC+CG,oBAAA;GfoxDD;Ecn0DF;IC+CG,oBAAA;GfuxDD;Ect0DF;IC+CG,WAAA;Gf0xDD;Ecz0DF;IC+CG,oBAAA;Gf6xDD;Ec50DF;IC+CG,oBAAA;GfgyDD;Ec/0DF;IC+CG,WAAA;GfmyDD;Ecl1DF;IC+CG,oBAAA;GfsyDD;Ecr1DF;IC+CG,mBAAA;GfyyDD;Ecx1DF;IC8DG,YAAA;Gf6xDD;Ec31DF;IC8DG,oBAAA;GfgyDD;Ec91DF;IC8DG,oBAAA;GfmyDD;Ecj2DF;IC8DG,WAAA;GfsyDD;Ecp2DF;IC8DG,oBAAA;GfyyDD;Ecv2DF;IC8DG,oBAAA;Gf4yDD;Ec12DF;IC8DG,WAAA;Gf+yDD;Ec72DF;IC8DG,oBAAA;GfkzDD;Ech3DF;IC8DG,oBAAA;GfqzDD;Ecn3DF;IC8DG,WAAA;GfwzDD;Ect3DF;IC8DG,oBAAA;Gf2zDD;Ecz3DF;IC8DG,mBAAA;Gf8zDD;Ec53DF;ICmEG,YAAA;Gf4zDD;Ec/3DF;ICoDG,WAAA;Gf80DD;Ecl4DF;ICoDG,mBAAA;Gfi1DD;Ecr4DF;ICoDG,mBAAA;Gfo1DD;Ecx4DF;ICoDG,UAAA;Gfu1DD;Ec34DF;ICoDG,mBAAA;Gf01DD;Ec94DF;ICoDG,mBAAA;Gf61DD;Ecj5DF;ICoDG,UAAA;Gfg2DD;Ecp5DF;ICoDG,mBAAA;Gfm2DD;Ecv5DF;ICoDG,mBAAA;Gfs2DD;Ec15DF;ICoDG,UAAA;Gfy2DD;Ec75DF;ICoDG,mBAAA;Gf42DD;Ech6DF;ICoDG,kBAAA;Gf+2DD;Ecn6DF;ICyDG,WAAA;Gf62DD;Ect6DF;ICwEG,kBAAA;Gfi2DD;Ecz6DF;ICwEG,0BAAA;Gfo2DD;Ec56DF;ICwEG,0BAAA;Gfu2DD;Ec/6DF;ICwEG,iBAAA;Gf02DD;Ecl7DF;ICwEG,0BAAA;Gf62DD;Ecr7DF;ICwEG,0BAAA;Gfg3DD;Ecx7DF;ICwEG,iBAAA;Gfm3DD;Ec37DF;ICwEG,0BAAA;Gfs3DD;Ec97DF;ICwEG,0BAAA;Gfy3DD;Ecj8DF;ICwEG,iBAAA;Gf43DD;Ecp8DF;ICwEG,0BAAA;Gf+3DD;Ecv8DF;ICwEG,yBAAA;Gfk4DD;Ec18DF;ICwEG,gBAAA;Gfq4DD;CACF;Aa53DD;EClFC;;;;;;;;;;;;ICuCK,YAAA;Gfs7DH;Ec79DF;IC+CG,YAAA;Gfi7DD;Ech+DF;IC+CG,oBAAA;Gfo7DD;Ecn+DF;IC+CG,oBAAA;Gfu7DD;Ect+DF;IC+CG,WAAA;Gf07DD;Ecz+DF;IC+CG,oBAAA;Gf67DD;Ec5+DF;IC+CG,oBAAA;Gfg8DD;Ec/+DF;IC+CG,WAAA;Gfm8DD;Ecl/DF;IC+CG,oBAAA;Gfs8DD;Ecr/DF;IC+CG,oBAAA;Gfy8DD;Ecx/DF;IC+CG,WAAA;Gf48DD;Ec3/DF;IC+CG,oBAAA;Gf+8DD;Ec9/DF;IC+CG,mBAAA;Gfk9DD;EcjgEF;IC8DG,YAAA;Gfs8DD;EcpgEF;IC8DG,oBAAA;Gfy8DD;EcvgEF;IC8DG,oBAAA;Gf48DD;Ec1gEF;IC8DG,WAAA;Gf+8DD;Ec7gEF;IC8DG,oBAAA;Gfk9DD;EchhEF;IC8DG,oBAAA;Gfq9DD;EcnhEF;IC8DG,WAAA;Gfw9DD;EcthEF;IC8DG,oBAAA;Gf29DD;EczhEF;IC8DG,oBAAA;Gf89DD;Ec5hEF;IC8DG,WAAA;Gfi+DD;Ec/hEF;IC8DG,oBAAA;Gfo+DD;EcliEF;IC8DG,mBAAA;Gfu+DD;EcriEF;ICmEG,YAAA;Gfq+DD;EcxiEF;ICoDG,WAAA;Gfu/DD;Ec3iEF;ICoDG,mBAAA;Gf0/DD;Ec9iEF;ICoDG,mBAAA;Gf6/DD;EcjjEF;ICoDG,UAAA;GfggED;EcpjEF;ICoDG,mBAAA;GfmgED;EcvjEF;ICoDG,mBAAA;GfsgED;Ec1jEF;ICoDG,UAAA;GfygED;Ec7jEF;ICoDG,mBAAA;Gf4gED;EchkEF;ICoDG,mBAAA;Gf+gED;EcnkEF;ICoDG,UAAA;GfkhED;EctkEF;ICoDG,mBAAA;GfqhED;EczkEF;ICoDG,kBAAA;GfwhED;Ec5kEF;ICyDG,WAAA;GfshED;Ec/kEF;ICwEG,kBAAA;Gf0gED;EcllEF;ICwEG,0BAAA;Gf6gED;EcrlEF;ICwEG,0BAAA;GfghED;EcxlEF;ICwEG,iBAAA;GfmhED;Ec3lEF;ICwEG,0BAAA;GfshED;Ec9lEF;ICwEG,0BAAA;GfyhED;EcjmEF;ICwEG,iBAAA;Gf4hED;EcpmEF;ICwEG,0BAAA;Gf+hED;EcvmEF;ICwEG,0BAAA;GfkiED;Ec1mEF;ICwEG,iBAAA;GfqiED;Ec7mEF;ICwEG,0BAAA;GfwiED;EchnEF;ICwEG,yBAAA;Gf2iED;EcnnEF;ICwEG,gBAAA;Gf8iED;CACF;Aa5hED;EC3FC;;;;;;;;;;;;ICuCK,YAAA;Gf+lEH;EctoEF;IC+CG,YAAA;Gf0lED;EczoEF;IC+CG,oBAAA;Gf6lED;Ec5oEF;IC+CG,oBAAA;GfgmED;Ec/oEF;IC+CG,WAAA;GfmmED;EclpEF;IC+CG,oBAAA;GfsmED;EcrpEF;IC+CG,oBAAA;GfymED;EcxpEF;IC+CG,WAAA;Gf4mED;Ec3pEF;IC+CG,oBAAA;Gf+mED;Ec9pEF;IC+CG,oBAAA;GfknED;EcjqEF;IC+CG,WAAA;GfqnED;EcpqEF;IC+CG,oBAAA;GfwnED;EcvqEF;IC+CG,mBAAA;Gf2nED;Ec1qEF;IC8DG,YAAA;Gf+mED;Ec7qEF;IC8DG,oBAAA;GfknED;EchrEF;IC8DG,oBAAA;GfqnED;EcnrEF;IC8DG,WAAA;GfwnED;EctrEF;IC8DG,oBAAA;Gf2nED;EczrEF;IC8DG,oBAAA;Gf8nED;Ec5rEF;IC8DG,WAAA;GfioED;Ec/rEF;IC8DG,oBAAA;GfooED;EclsEF;IC8DG,oBAAA;GfuoED;EcrsEF;IC8DG,WAAA;Gf0oED;EcxsEF;IC8DG,oBAAA;Gf6oED;Ec3sEF;IC8DG,mBAAA;GfgpED;Ec9sEF;ICmEG,YAAA;Gf8oED;EcjtEF;ICoDG,WAAA;GfgqED;EcptEF;ICoDG,mBAAA;GfmqED;EcvtEF;ICoDG,mBAAA;GfsqED;Ec1tEF;ICoDG,UAAA;GfyqED;Ec7tEF;ICoDG,mBAAA;Gf4qED;EchuEF;ICoDG,mBAAA;Gf+qED;EcnuEF;ICoDG,UAAA;GfkrED;EctuEF;ICoDG,mBAAA;GfqrED;EczuEF;ICoDG,mBAAA;GfwrED;Ec5uEF;ICoDG,UAAA;Gf2rED;Ec/uEF;ICoDG,mBAAA;Gf8rED;EclvEF;ICoDG,kBAAA;GfisED;EcrvEF;ICyDG,WAAA;Gf+rED;EcxvEF;ICwEG,kBAAA;GfmrED;Ec3vEF;ICwEG,0BAAA;GfsrED;Ec9vEF;ICwEG,0BAAA;GfyrED;EcjwEF;ICwEG,iBAAA;Gf4rED;EcpwEF;ICwEG,0BAAA;Gf+rED;EcvwEF;ICwEG,0BAAA;GfksED;Ec1wEF;ICwEG,iBAAA;GfqsED;Ec7wEF;ICwEG,0BAAA;GfwsED;EchxEF;ICwEG,0BAAA;Gf2sED;EcnxEF;ICwEG,iBAAA;Gf8sED;EctxEF;ICwEG,0BAAA;GfitED;EczxEF;ICwEG,yBAAA;GfotED;Ec5xEF;ICwEG,gBAAA;GfutED;CACF;AgBzxED;EACE,8BAAA;ChB2xED;AgB5xED;EAQI,iBAAA;EACA,sBAAA;EACA,YAAA;ChBuxEH;AgBlxEG;;EACE,iBAAA;EACA,oBAAA;EACA,YAAA;ChBqxEL;AgBhxED;EACE,iBAAA;EACA,oBAAA;EACA,eAAA;EACA,iBAAA;ChBkxED;AgB/wED;EACE,iBAAA;ChBixED;AgB3wED;EACE,YAAA;EACA,gBAAA;EACA,oBAAA;ChB6wED;AgBhxED;;;;;;EAWQ,aAAA;EACA,wBAAA;EACA,oBAAA;EACA,2BAAA;ChB6wEP;AgB3xED;EAoBI,uBAAA;EACA,8BAAA;ChB0wEH;AgB/xED;;;;;;EA8BQ,cAAA;ChBywEP;AgBvyED;EAoCI,2BAAA;ChBswEH;AgB1yED;EAyCI,uBAAA;ChBowEH;AgB7vED;;;;;;EAOQ,aAAA;ChB8vEP;AgBnvED;EACE,uBAAA;ChBqvED;AgBtvED;;;;;;EAQQ,uBAAA;ChBsvEP;AgB9vED;;EAeM,yBAAA;ChBmvEL;AgBzuED;EAEI,0BAAA;ChB0uEH;AgBjuED;EAEI,0BAAA;ChBkuEH;AiBj3EC;;;;;;;;;;;;EAOI,0BAAA;CjBw3EL;AiBl3EC;;;;;EAMI,0BAAA;CjBm3EL;AiBt4EC;;;;;;;;;;;;EAOI,0BAAA;CjB64EL;AiBv4EC;;;;;EAMI,0BAAA;CjBw4EL;AiB35EC;;;;;;;;;;;;EAOI,0BAAA;CjBk6EL;AiB55EC;;;;;EAMI,0BAAA;CjB65EL;AiBh7EC;;;;;;;;;;;;EAOI,0BAAA;CjBu7EL;AiBj7EC;;;;;EAMI,0BAAA;CjBk7EL;AiBr8EC;;;;;;;;;;;;EAOI,0BAAA;CjB48EL;AiBt8EC;;;;;EAMI,0BAAA;CjBu8EL;AgBnzED;EACE,kBAAA;EACA,iBAAA;ChBqzED;AgBnzEC;EAAA;IACE,YAAA;IACA,oBAAA;IACA,mBAAA;IACA,6CAAA;IACA,uBAAA;GhBszED;EgB3zED;IASI,iBAAA;GhBqzEH;EgB9zED;;;;;;IAkBU,oBAAA;GhBozET;EgBt0ED;IA0BI,UAAA;GhB+yEH;EgBz0ED;;;;;;IAmCU,eAAA;GhB8yET;EgBj1ED;;;;;;IAuCU,gBAAA;GhBkzET;EgBz1ED;;;;IAoDU,iBAAA;GhB2yET;CACF;AkBrgFD;EAIE,aAAA;EACA,WAAA;EACA,UAAA;EACA,UAAA;ClBogFD;AkBjgFD;EACE,eAAA;EACA,YAAA;EACA,WAAA;EACA,oBAAA;EACA,gBAAA;EACA,qBAAA;EACA,eAAA;EACA,UAAA;EACA,iCAAA;ClBmgFD;AkBhgFD;EACE,sBAAA;EACA,gBAAA;EACA,mBAAA;EACA,iBAAA;ClBkgFD;AkBx/ED;Eb6BE,+BAAA;EACG,4BAAA;EACK,uBAAA;EarBR,yBAAA;EACA,sBAAA;EAAA,iBAAA;ClBo/ED;AkBh/ED;;EAEE,gBAAA;EACA,mBAAA;EACA,oBAAA;ClBk/ED;AkB5+EC;;;;;;EAGE,oBAAA;ClBi/EH;AkB7+ED;EACE,eAAA;ClB++ED;AkB3+ED;EACE,eAAA;EACA,YAAA;ClB6+ED;AkBz+ED;;EAEE,aAAA;ClB2+ED;AkBv+ED;;;EZ1FE,2CAAA;EACA,qBAAA;CNskFD;AkBt+ED;EACE,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;ClBw+ED;AkB98ED;EACE,eAAA;EACA,YAAA;EACA,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,uBAAA;EACA,uBAAA;EACA,uBAAA;EACA,mBAAA;Eb3EA,yDAAA;EACQ,iDAAA;EAyHR,+EAAA;EACK,0EAAA;EACG,uFAAA;EAAA,+EAAA;EAAA,uEAAA;EAAA,4GAAA;CLo6ET;AmB9iFC;EACE,sBAAA;EACA,WAAA;EdYF,0FAAA;EACQ,kFAAA;CLqiFT;AKpgFC;EACE,YAAA;EACA,WAAA;CLsgFH;AKpgFC;EAA0B,YAAA;CLugF3B;AKtgFC;EAAgC,YAAA;CLygFjC;AkB19EC;EACE,8BAAA;EACA,UAAA;ClB49EH;AkBp9EC;;;EAGE,0BAAA;EACA,WAAA;ClBs9EH;AkBn9EC;;EAEE,oBAAA;ClBq9EH;AkBj9EC;EACE,aAAA;ClBm9EH;AkBr8ED;EAKI;;;;IACE,kBAAA;GlBs8EH;EkBn8EC;;;;;;;;IAEE,kBAAA;GlB28EH;EkBx8EC;;;;;;;;IAEE,kBAAA;GlBg9EH;CACF;AkBt8ED;EACE,oBAAA;ClBw8ED;AkBh8ED;;EAEE,mBAAA;EACA,eAAA;EACA,iBAAA;EACA,oBAAA;ClBk8ED;AkB/7EC;;;;EAGI,oBAAA;ClBk8EL;AkB78ED;;EAgBI,iBAAA;EACA,mBAAA;EACA,iBAAA;EACA,iBAAA;EACA,gBAAA;ClBi8EH;AkB97ED;;;;EAIE,mBAAA;EACA,mBAAA;EACA,mBAAA;ClBg8ED;AkB77ED;;EAEE,iBAAA;ClB+7ED;AkB37ED;;EAEE,mBAAA;EACA,sBAAA;EACA,mBAAA;EACA,iBAAA;EACA,iBAAA;EACA,uBAAA;EACA,gBAAA;ClB67ED;AkB17EC;;;;EAEE,oBAAA;ClB87EH;AkB37ED;;EAEE,cAAA;EACA,kBAAA;ClB67ED;AkBp7ED;EACE,iBAAA;EAEA,iBAAA;EACA,oBAAA;EAEA,iBAAA;ClBo7ED;AkBl7EC;;EAEE,iBAAA;EACA,gBAAA;ClBo7EH;AkBv6ED;EC3PE,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CnBqqFD;AmBnqFC;EACE,aAAA;EACA,kBAAA;CnBqqFH;AmBlqFC;;EAEE,aAAA;CnBoqFH;AkBn7ED;EAEI,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;ClBo7EH;AkB17ED;EASI,aAAA;EACA,kBAAA;ClBo7EH;AkB97ED;;EAcI,aAAA;ClBo7EH;AkBl8ED;EAiBI,aAAA;EACA,iBAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;ClBo7EH;AkBh7ED;ECvRE,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CnB0sFD;AmBxsFC;EACE,aAAA;EACA,kBAAA;CnB0sFH;AmBvsFC;;EAEE,aAAA;CnBysFH;AkB57ED;EAEI,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;ClB67EH;AkBn8ED;EASI,aAAA;EACA,kBAAA;ClB67EH;AkBv8ED;;EAcI,aAAA;ClB67EH;AkB38ED;EAiBI,aAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;ClB67EH;AkBp7ED;EAEE,mBAAA;ClBq7ED;AkBv7ED;EAMI,sBAAA;ClBo7EH;AkBh7ED;EACE,mBAAA;EACA,OAAA;EACA,SAAA;EACA,WAAA;EACA,eAAA;EACA,YAAA;EACA,aAAA;EACA,kBAAA;EACA,mBAAA;EACA,qBAAA;ClBk7ED;AkBh7ED;;;EAGE,YAAA;EACA,aAAA;EACA,kBAAA;ClBk7ED;AkBh7ED;;;EAGE,YAAA;EACA,aAAA;EACA,kBAAA;ClBk7ED;AkB96ED;;;;;;;;;;EClZI,eAAA;CnB40FH;AkB17ED;EC9YI,sBAAA;EdiDF,yDAAA;EACQ,iDAAA;CL2xFT;AmB30FG;EACE,sBAAA;Ed8CJ,0EAAA;EACQ,kEAAA;CLgyFT;AkBp8ED;ECpYI,eAAA;EACA,0BAAA;EACA,sBAAA;CnB20FH;AkBz8ED;EC9XI,eAAA;CnB00FH;AkBz8ED;;;;;;;;;;ECrZI,eAAA;CnB02FH;AkBr9ED;ECjZI,sBAAA;EdiDF,yDAAA;EACQ,iDAAA;CLyzFT;AmBz2FG;EACE,sBAAA;Ed8CJ,0EAAA;EACQ,kEAAA;CL8zFT;AkB/9ED;ECvYI,eAAA;EACA,0BAAA;EACA,sBAAA;CnBy2FH;AkBp+ED;ECjYI,eAAA;CnBw2FH;AkBp+ED;;;;;;;;;;ECxZI,eAAA;CnBw4FH;AkBh/ED;ECpZI,sBAAA;EdiDF,yDAAA;EACQ,iDAAA;CLu1FT;AmBv4FG;EACE,sBAAA;Ed8CJ,0EAAA;EACQ,kEAAA;CL41FT;AkB1/ED;EC1YI,eAAA;EACA,0BAAA;EACA,sBAAA;CnBu4FH;AkB//ED;ECpYI,eAAA;CnBs4FH;AkB3/EC;EACE,UAAA;ClB6/EH;AkB3/EC;EACE,OAAA;ClB6/EH;AkBn/ED;EACE,eAAA;EACA,gBAAA;EACA,oBAAA;EACA,eAAA;ClBq/ED;AkBn+EC;EAAA;IAGI,sBAAA;IACA,iBAAA;IACA,uBAAA;GlBo+EH;EkBz+ED;IAUI,sBAAA;IACA,YAAA;IACA,uBAAA;GlBk+EH;EkB9+ED;IAiBI,sBAAA;GlBg+EH;EkBj/ED;IAqBI,sBAAA;IACA,uBAAA;GlB+9EH;EkBr/ED;;;IA2BM,YAAA;GlB+9EL;EkB1/ED;IAiCI,YAAA;GlB49EH;EkB7/ED;IAqCI,iBAAA;IACA,uBAAA;GlB29EH;EkBjgFD;;IA6CI,sBAAA;IACA,cAAA;IACA,iBAAA;IACA,uBAAA;GlBw9EH;EkBxgFD;;IAmDM,gBAAA;GlBy9EL;EkB5gFD;;IAwDI,mBAAA;IACA,eAAA;GlBw9EH;EkBjhFD;IA8DI,OAAA;GlBs9EH;CACF;AkB58ED;;;;EASI,iBAAA;EACA,cAAA;EACA,iBAAA;ClBy8EH;AkBp9ED;;EAiBI,iBAAA;ClBu8EH;AkBx9ED;EJ9gBE,oBAAA;EACA,mBAAA;Cdy+FD;AkBj8EC;EAAA;IAEI,iBAAA;IACA,iBAAA;IACA,kBAAA;GlBm8EH;CACF;AkBn+ED;EAwCI,YAAA;ClB87EH;AkBt7EG;EAAA;IAEI,kBAAA;IACA,gBAAA;GlBw7EL;CACF;AkBp7EG;EAAA;IAEI,iBAAA;IACA,gBAAA;GlBs7EL;CACF;AoBrgGD;EACE,sBAAA;EACA,iBAAA;EACA,oBAAA;EACA,mBAAA;EACA,oBAAA;EACA,uBAAA;EACA,+BAAA;EAAA,2BAAA;EACA,gBAAA;EACA,uBAAA;EACA,8BAAA;ECoCA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,mBAAA;EhBqKA,0BAAA;EACG,uBAAA;EACC,sBAAA;EACI,kBAAA;CLg0FT;AoBxgGG;;;;;;EdrBF,2CAAA;EACA,qBAAA;CNqiGD;AoB3gGC;;;EAGE,YAAA;EACA,sBAAA;CpB6gGH;AoB1gGC;;EAEE,uBAAA;EACA,WAAA;Ef2BF,yDAAA;EACQ,iDAAA;CLk/FT;AoB1gGC;;;EAGE,oBAAA;EE9CF,0BAAA;EACA,cAAA;EjBiEA,yBAAA;EACQ,iBAAA;CL2/FT;AoB1gGG;;EAEE,qBAAA;CpB4gGL;AoBngGD;EC7DE,YAAA;EACA,uBAAA;EACA,mBAAA;CrBmkGD;AqBjkGC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBmkGH;AqBjkGC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBmkGH;AqBjkGC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrBmkGH;AqBjkGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBykGL;AqBnkGG;;;;;;;;;EAGE,uBAAA;EACA,mBAAA;CrB2kGL;AoBpjGD;EClBI,YAAA;EACA,uBAAA;CrBykGH;AoBrjGD;EChEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBwnGD;AqBtnGC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBwnGH;AqBtnGC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBwnGH;AqBtnGC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrBwnGH;AqBtnGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB8nGL;AqBxnGG;;;;;;;;;EAGE,0BAAA;EACA,sBAAA;CrBgoGL;AoBtmGD;ECrBI,eAAA;EACA,uBAAA;CrB8nGH;AoBtmGD;ECpEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB6qGD;AqB3qGC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB6qGH;AqB3qGC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB6qGH;AqB3qGC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrB6qGH;AqB3qGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBmrGL;AqB7qGG;;;;;;;;;EAGE,0BAAA;EACA,sBAAA;CrBqrGL;AoBvpGD;ECzBI,eAAA;EACA,uBAAA;CrBmrGH;AoBvpGD;ECxEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBkuGD;AqBhuGC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBkuGH;AqBhuGC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBkuGH;AqBhuGC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrBkuGH;AqBhuGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBwuGL;AqBluGG;;;;;;;;;EAGE,0BAAA;EACA,sBAAA;CrB0uGL;AoBxsGD;EC7BI,eAAA;EACA,uBAAA;CrBwuGH;AoBxsGD;EC5EE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBuxGD;AqBrxGC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBuxGH;AqBrxGC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBuxGH;AqBrxGC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrBuxGH;AqBrxGG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB6xGL;AqBvxGG;;;;;;;;;EAGE,0BAAA;EACA,sBAAA;CrB+xGL;AoBzvGD;ECjCI,eAAA;EACA,uBAAA;CrB6xGH;AoBzvGD;EChFE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB40GD;AqB10GC;;EAEE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB40GH;AqB10GC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CrB40GH;AqB10GC;;;EAGE,YAAA;EACA,0BAAA;EACA,uBAAA;EACA,sBAAA;CrB40GH;AqB10GG;;;;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;CrBk1GL;AqB50GG;;;;;;;;;EAGE,0BAAA;EACA,sBAAA;CrBo1GL;AoB1yGD;ECrCI,eAAA;EACA,uBAAA;CrBk1GH;AoBryGD;EACE,iBAAA;EACA,eAAA;EACA,iBAAA;CpBuyGD;AoBryGC;;;;;EAKE,8BAAA;EfnCF,yBAAA;EACQ,iBAAA;CL20GT;AoBtyGC;;;;EAIE,0BAAA;CpBwyGH;AoBtyGC;;EAEE,eAAA;EACA,2BAAA;EACA,8BAAA;CpBwyGH;AoBpyGG;;;;EAEE,eAAA;EACA,sBAAA;CpBwyGL;AoB/xGD;;EC9EE,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CrBi3GD;AoBlyGD;;EClFE,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CrBw3GD;AoBryGD;;ECtFE,iBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CrB+3GD;AoBpyGD;EACE,eAAA;EACA,YAAA;CpBsyGD;AoBlyGD;EACE,gBAAA;CpBoyGD;AoB7xGC;;;EACE,YAAA;CpBiyGH;AuB37GD;EACE,WAAA;ElBoLA,yCAAA;EACK,oCAAA;EACG,iCAAA;CL0wGT;AuB77GC;EACE,WAAA;CvB+7GH;AuB37GD;EACE,cAAA;CvB67GD;AuB37GC;EAAY,eAAA;CvB87Gb;AuB77GC;EAAY,mBAAA;CvBg8Gb;AuB/7GC;EAAY,yBAAA;CvBk8Gb;AuB/7GD;EACE,mBAAA;EACA,UAAA;EACA,iBAAA;ElBsKA,gDAAA;EACQ,2CAAA;EAAA,wCAAA;EAOR,mCAAA;EACQ,8BAAA;EAAA,2BAAA;EAGR,yCAAA;EACQ,oCAAA;EAAA,iCAAA;CLoxGT;AwBh+GD;EACE,sBAAA;EACA,SAAA;EACA,UAAA;EACA,iBAAA;EACA,uBAAA;EACA,uBAAA;EACA,yBAAA;EACA,oCAAA;EACA,mCAAA;CxBk+GD;AwB99GD;;EAEE,mBAAA;CxBg+GD;AwB59GD;EACE,WAAA;CxB89GD;AwB19GD;EACE,mBAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,cAAA;EACA,YAAA;EACA,iBAAA;EACA,eAAA;EACA,gBAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;EACA,uBAAA;EACA,6BAAA;EACA,uBAAA;EACA,sCAAA;EACA,mBAAA;EnBuBA,oDAAA;EACQ,4CAAA;CLs8GT;AwBx9GC;EACE,SAAA;EACA,WAAA;CxB09GH;AwBn/GD;ECzBE,YAAA;EACA,cAAA;EACA,iBAAA;EACA,0BAAA;CzB+gHD;AwBz/GD;EAmCI,eAAA;EACA,kBAAA;EACA,YAAA;EACA,iBAAA;EACA,wBAAA;EACA,eAAA;EACA,oBAAA;CxBy9GH;AwBv9GG;;EAEE,eAAA;EACA,sBAAA;EACA,0BAAA;CxBy9GL;AwBl9GC;;;EAGE,YAAA;EACA,sBAAA;EACA,0BAAA;EACA,WAAA;CxBo9GH;AwB38GC;;;EAGE,eAAA;CxB68GH;AwBz8GC;;EAEE,sBAAA;EACA,oBAAA;EACA,8BAAA;EACA,uBAAA;EEzGF,oEAAA;C1BqjHD;AwBt8GD;EAGI,eAAA;CxBs8GH;AwBz8GD;EAQI,WAAA;CxBo8GH;AwB57GD;EACE,SAAA;EACA,WAAA;CxB87GD;AwBt7GD;EACE,YAAA;EACA,QAAA;CxBw7GD;AwBp7GD;EACE,eAAA;EACA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,oBAAA;CxBs7GD;AwBl7GD;EACE,gBAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,aAAA;CxBo7GD;AwBh7GD;EACE,SAAA;EACA,WAAA;CxBk7GD;AwB16GD;;EAII,YAAA;EACA,cAAA;EACA,0BAAA;EACA,4BAAA;CxB06GH;AwBj7GD;;EAWI,UAAA;EACA,aAAA;EACA,mBAAA;CxB06GH;AwBj6GD;EACE;IApEA,SAAA;IACA,WAAA;GxBw+GC;EwBr6GD;IA1DA,YAAA;IACA,QAAA;GxBk+GC;CACF;A2B7mHD;;EAEE,mBAAA;EACA,sBAAA;EACA,uBAAA;C3B+mHD;A2BnnHD;;EAMI,mBAAA;EACA,YAAA;C3BinHH;A2B/mHG;;;;;;;;EAIE,WAAA;C3BqnHL;A2B/mHD;;;;EAKI,kBAAA;C3BgnHH;A2B3mHD;EACE,kBAAA;C3B6mHD;A2B9mHD;;;EAOI,YAAA;C3B4mHH;A2BnnHD;;;EAYI,iBAAA;C3B4mHH;A2BxmHD;EACE,iBAAA;C3B0mHD;A2BtmHD;EACE,eAAA;C3BwmHD;A2BvmHC;ECpDA,2BAAA;EACA,8BAAA;C5B8pHD;A2BtmHD;;ECjDE,0BAAA;EACA,6BAAA;C5B2pHD;A2BrmHD;EACE,YAAA;C3BumHD;A2BrmHD;EACE,iBAAA;C3BumHD;A2BrmHD;;ECrEE,2BAAA;EACA,8BAAA;C5B8qHD;A2BpmHD;ECnEE,0BAAA;EACA,6BAAA;C5B0qHD;A2BnmHD;;EAEE,WAAA;C3BqmHD;A2BplHD;EACE,mBAAA;EACA,kBAAA;C3BslHD;A2BplHD;EACE,oBAAA;EACA,mBAAA;C3BslHD;A2BjlHD;EtB/CE,yDAAA;EACQ,iDAAA;CLmoHT;A2BjlHC;EtBnDA,yBAAA;EACQ,iBAAA;CLuoHT;A2B9kHD;EACE,eAAA;C3BglHD;A2B7kHD;EACE,wBAAA;EACA,uBAAA;C3B+kHD;A2B5kHD;EACE,wBAAA;C3B8kHD;A2BvkHD;;;EAII,eAAA;EACA,YAAA;EACA,YAAA;EACA,gBAAA;C3BwkHH;A2B/kHD;EAcM,YAAA;C3BokHL;A2BllHD;;;;EAsBI,iBAAA;EACA,eAAA;C3BkkHH;A2B7jHC;EACE,iBAAA;C3B+jHH;A2B7jHC;EC7KA,4BAAA;EACA,6BAAA;EAOA,8BAAA;EACA,6BAAA;C5BuuHD;A2B/jHC;ECjLA,0BAAA;EACA,2BAAA;EAOA,gCAAA;EACA,+BAAA;C5B6uHD;A2BhkHD;EACE,iBAAA;C3BkkHD;A2BhkHD;;ECjLE,8BAAA;EACA,6BAAA;C5BqvHD;A2B/jHD;EC/LE,0BAAA;EACA,2BAAA;C5BiwHD;A2B3jHD;EACE,eAAA;EACA,YAAA;EACA,oBAAA;EACA,0BAAA;C3B6jHD;A2BjkHD;;EAOI,oBAAA;EACA,YAAA;EACA,UAAA;C3B8jHH;A2BvkHD;EAYI,YAAA;C3B8jHH;A2B1kHD;EAgBI,WAAA;C3B6jHH;A2B5iHD;;;;EAKM,mBAAA;EACA,uBAAA;EACA,qBAAA;C3B6iHL;A6BvxHD;EACE,mBAAA;EACA,eAAA;EACA,0BAAA;C7ByxHD;A6BtxHC;EACE,YAAA;EACA,iBAAA;EACA,gBAAA;C7BwxHH;A6BjyHD;EAeI,mBAAA;EACA,WAAA;EAKA,YAAA;EAEA,YAAA;EACA,iBAAA;C7BgxHH;A6B9wHG;EACE,WAAA;C7BgxHL;A6BtwHD;;;EVwBE,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CnBmvHD;AmBjvHC;;;EACE,aAAA;EACA,kBAAA;CnBqvHH;AmBlvHC;;;;;;EAEE,aAAA;CnBwvHH;A6BxxHD;;;EVmBE,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CnB0wHD;AmBxwHC;;;EACE,aAAA;EACA,kBAAA;CnB4wHH;AmBzwHC;;;;;;EAEE,aAAA;CnB+wHH;A6BtyHD;;;EAGE,oBAAA;C7BwyHD;A6BtyHC;;;EACE,iBAAA;C7B0yHH;A6BtyHD;;EAEE,UAAA;EACA,oBAAA;EACA,uBAAA;C7BwyHD;A6BnyHD;EACE,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,eAAA;EACA,eAAA;EACA,mBAAA;EACA,0BAAA;EACA,uBAAA;EACA,mBAAA;C7BqyHD;A6BlyHC;EACE,kBAAA;EACA,gBAAA;EACA,mBAAA;C7BoyHH;A6BlyHC;EACE,mBAAA;EACA,gBAAA;EACA,mBAAA;C7BoyHH;A6BxzHD;;EA0BI,cAAA;C7BkyHH;A6B7xHD;;;;;;;EDtGE,2BAAA;EACA,8BAAA;C5B44HD;A6B9xHD;EACE,gBAAA;C7BgyHD;A6B9xHD;;;;;;;ED1GE,0BAAA;EACA,6BAAA;C5Bi5HD;A6B/xHD;EACE,eAAA;C7BiyHD;A6B5xHD;EACE,mBAAA;EAGA,aAAA;EACA,oBAAA;C7B4xHD;A6BjyHD;EAUI,mBAAA;C7B0xHH;A6BpyHD;EAYM,kBAAA;C7B2xHL;A6BxxHG;;;EAGE,WAAA;C7B0xHL;A6BrxHC;;EAGI,mBAAA;C7BsxHL;A6BnxHC;;EAGI,WAAA;EACA,kBAAA;C7BoxHL;A8Bn7HD;EACE,gBAAA;EACA,iBAAA;EACA,iBAAA;C9Bq7HD;A8Bx7HD;EAOI,mBAAA;EACA,eAAA;C9Bo7HH;A8B57HD;EAWM,mBAAA;EACA,eAAA;EACA,mBAAA;C9Bo7HL;A8Bn7HK;;EAEE,sBAAA;EACA,0BAAA;C9Bq7HP;A8Bh7HG;EACE,eAAA;C9Bk7HL;A8Bh7HK;;EAEE,eAAA;EACA,sBAAA;EACA,oBAAA;EACA,8BAAA;C9Bk7HP;A8B36HG;;;EAGE,0BAAA;EACA,sBAAA;C9B66HL;A8Bt9HD;ELLE,YAAA;EACA,cAAA;EACA,iBAAA;EACA,0BAAA;CzB89HD;A8B59HD;EA0DI,gBAAA;C9Bq6HH;A8B55HD;EACE,8BAAA;C9B85HD;A8B/5HD;EAGI,YAAA;EAEA,oBAAA;C9B85HH;A8Bn6HD;EASM,kBAAA;EACA,wBAAA;EACA,8BAAA;EACA,2BAAA;C9B65HL;A8B55HK;EACE,mCAAA;C9B85HP;A8Bx5HK;;;EAGE,eAAA;EACA,gBAAA;EACA,uBAAA;EACA,uBAAA;EACA,iCAAA;C9B05HP;A8Br5HC;EAqDA,YAAA;EA8BA,iBAAA;C9Bs0HD;A8Bz5HC;EAwDE,YAAA;C9Bo2HH;A8B55HC;EA0DI,mBAAA;EACA,mBAAA;C9Bq2HL;A8Bh6HC;EAgEE,UAAA;EACA,WAAA;C9Bm2HH;A8Bh2HC;EAAA;IAEI,oBAAA;IACA,UAAA;G9Bk2HH;E8Br2HD;IAKM,iBAAA;G9Bm2HL;CACF;A8B76HC;EAuFE,gBAAA;EACA,mBAAA;C9By1HH;A8Bj7HC;;;EA8FE,uBAAA;C9Bw1HH;A8Br1HC;EAAA;IAEI,8BAAA;IACA,2BAAA;G9Bu1HH;E8B11HD;;;IAQI,0BAAA;G9Bu1HH;CACF;A8Bx7HD;EAEI,YAAA;C9By7HH;A8B37HD;EAMM,mBAAA;C9Bw7HL;A8B97HD;EASM,iBAAA;C9Bw7HL;A8Bn7HK;;;EAGE,YAAA;EACA,0BAAA;C9Bq7HP;A8B76HD;EAEI,YAAA;C9B86HH;A8Bh7HD;EAIM,gBAAA;EACA,eAAA;C9B+6HL;A8Bn6HD;EACE,YAAA;C9Bq6HD;A8Bt6HD;EAII,YAAA;C9Bq6HH;A8Bz6HD;EAMM,mBAAA;EACA,mBAAA;C9Bs6HL;A8B76HD;EAYI,UAAA;EACA,WAAA;C9Bo6HH;A8Bj6HC;EAAA;IAEI,oBAAA;IACA,UAAA;G9Bm6HH;E8Bt6HD;IAKM,iBAAA;G9Bo6HL;CACF;A8B55HD;EACE,iBAAA;C9B85HD;A8B/5HD;EAKI,gBAAA;EACA,mBAAA;C9B65HH;A8Bn6HD;;;EAYI,uBAAA;C9B45HH;A8Bz5HC;EAAA;IAEI,8BAAA;IACA,2BAAA;G9B25HH;E8B95HD;;;IAQI,0BAAA;G9B25HH;CACF;A8Bl5HD;EAEI,cAAA;C9Bm5HH;A8Br5HD;EAKI,eAAA;C9Bm5HH;A8B14HD;EAEE,iBAAA;EF7OA,0BAAA;EACA,2BAAA;C5BynID;A+BjnID;EACE,mBAAA;EACA,iBAAA;EACA,oBAAA;EACA,8BAAA;C/BmnID;A+B9mIC;EAAA;IACE,mBAAA;G/BinID;CACF;A+BrmIC;EAAA;IACE,YAAA;G/BwmID;CACF;A+B1lID;EACE,oBAAA;EACA,mBAAA;EACA,oBAAA;EACA,kCAAA;EACA,2DAAA;EAAA,mDAAA;EAEA,kCAAA;C/B2lID;A+BzlIC;EACE,iBAAA;C/B2lIH;A+BxlIC;EAAA;IACE,YAAA;IACA,cAAA;IACA,yBAAA;IAAA,iBAAA;G/B2lID;E+BzlIC;IACE,0BAAA;IACA,wBAAA;IACA,kBAAA;IACA,6BAAA;G/B2lIH;E+BxlIC;IACE,oBAAA;G/B0lIH;E+BrlIC;;;IAGE,iBAAA;IACA,gBAAA;G/BulIH;CACF;A+BnlID;;EAWE,gBAAA;EACA,SAAA;EACA,QAAA;EACA,cAAA;C/B4kID;A+B1lID;;EAGI,kBAAA;C/B2lIH;A+BzlIG;EAAA;;IACE,kBAAA;G/B6lIH;CACF;A+BnlIC;EAAA;;IACE,iBAAA;G/BulID;CACF;A+BplID;EACE,OAAA;EACA,sBAAA;C/BslID;A+BplID;EACE,UAAA;EACA,iBAAA;EACA,sBAAA;C/BslID;A+B9kID;;;;EAII,oBAAA;EACA,mBAAA;C/BglIH;A+B9kIG;EAAA;;;;IACE,gBAAA;IACA,eAAA;G/BolIH;CACF;A+BxkID;EACE,cAAA;EACA,sBAAA;C/B0kID;A+BxkIC;EAAA;IACE,iBAAA;G/B2kID;CACF;A+BrkID;EACE,YAAA;EACA,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,kBAAA;C/BukID;A+BrkIC;;EAEE,sBAAA;C/BukIH;A+BhlID;EAaI,eAAA;C/BskIH;A+BnkIC;EACE;;IAEE,mBAAA;G/BqkIH;CACF;A+B3jID;EACE,mBAAA;EACA,aAAA;EACA,kBAAA;EACA,mBAAA;EC9LA,gBAAA;EACA,mBAAA;ED+LA,8BAAA;EACA,uBAAA;EACA,8BAAA;EACA,mBAAA;C/B8jID;A+B1jIC;EACE,WAAA;C/B4jIH;A+B1kID;EAmBI,eAAA;EACA,YAAA;EACA,YAAA;EACA,mBAAA;C/B0jIH;A+BhlID;EAyBI,gBAAA;C/B0jIH;A+BvjIC;EAAA;IACE,cAAA;G/B0jID;CACF;A+BjjID;EACE,oBAAA;C/BmjID;A+BpjID;EAII,kBAAA;EACA,qBAAA;EACA,kBAAA;C/BmjIH;A+BhjIC;EAAA;IAGI,iBAAA;IACA,YAAA;IACA,YAAA;IACA,cAAA;IACA,8BAAA;IACA,UAAA;IACA,yBAAA;IAAA,iBAAA;G/BijIH;E+B1jID;;IAYM,2BAAA;G/BkjIL;E+B9jID;IAeM,kBAAA;G/BkjIL;E+BjjIK;;IAEE,uBAAA;G/BmjIP;CACF;A+B7iIC;EAAA;IACE,YAAA;IACA,UAAA;G/BgjID;E+BljID;IAKI,YAAA;G/BgjIH;E+BrjID;IAOM,kBAAA;IACA,qBAAA;G/BijIL;CACF;A+BtiID;EACE,mBAAA;EACA,oBAAA;EACA,mBAAA;EACA,kCAAA;EACA,qCAAA;E1B5NA,6FAAA;EACQ,qFAAA;E2BjER,gBAAA;EACA,mBAAA;ChCu0ID;AkB13HC;EAAA;IAGI,sBAAA;IACA,iBAAA;IACA,uBAAA;GlB23HH;EkBh4HD;IAUI,sBAAA;IACA,YAAA;IACA,uBAAA;GlBy3HH;EkBr4HD;IAiBI,sBAAA;GlBu3HH;EkBx4HD;IAqBI,sBAAA;IACA,uBAAA;GlBs3HH;EkB54HD;;;IA2BM,YAAA;GlBs3HL;EkBj5HD;IAiCI,YAAA;GlBm3HH;EkBp5HD;IAqCI,iBAAA;IACA,uBAAA;GlBk3HH;EkBx5HD;;IA6CI,sBAAA;IACA,cAAA;IACA,iBAAA;IACA,uBAAA;GlB+2HH;EkB/5HD;;IAmDM,gBAAA;GlBg3HL;EkBn6HD;;IAwDI,mBAAA;IACA,eAAA;GlB+2HH;EkBx6HD;IA8DI,OAAA;GlB62HH;CACF;A+BtlIG;EAAA;IACE,mBAAA;G/BylIH;E+BvlIG;IACE,iBAAA;G/BylIL;CACF;A+BjlIC;EAAA;IACE,YAAA;IACA,eAAA;IACA,kBAAA;IACA,gBAAA;IACA,eAAA;IACA,UAAA;I1BvPF,yBAAA;IACQ,iBAAA;GL40IP;CACF;A+B9kID;EACE,cAAA;EHpUA,0BAAA;EACA,2BAAA;C5Bq5ID;A+B9kID;EACE,iBAAA;EHzUA,4BAAA;EACA,6BAAA;EAOA,8BAAA;EACA,6BAAA;C5Bo5ID;A+B1kID;EChVE,gBAAA;EACA,mBAAA;ChC65ID;A+B3kIC;ECnVA,iBAAA;EACA,oBAAA;ChCi6ID;A+B5kIC;ECtVA,iBAAA;EACA,oBAAA;ChCq6ID;A+BtkID;EChWE,iBAAA;EACA,oBAAA;ChCy6ID;A+BvkIC;EAAA;IACE,YAAA;IACA,mBAAA;IACA,kBAAA;G/B0kID;CACF;A+B9jID;EACE;IEtWA,uBAAA;GjCu6IC;E+BhkID;IE1WA,wBAAA;IF4WE,oBAAA;G/BkkID;E+BpkID;IAKI,gBAAA;G/BkkIH;CACF;A+BzjID;EACE,0BAAA;EACA,sBAAA;C/B2jID;A+B7jID;EAKI,YAAA;C/B2jIH;A+B1jIG;;EAEE,eAAA;EACA,8BAAA;C/B4jIL;A+BrkID;EAcI,YAAA;C/B0jIH;A+BxkID;EAmBM,YAAA;C/BwjIL;A+BtjIK;;EAEE,YAAA;EACA,8BAAA;C/BwjIP;A+BpjIK;;;EAGE,YAAA;EACA,0BAAA;C/BsjIP;A+BljIK;;;EAGE,YAAA;EACA,8BAAA;C/BojIP;A+B7iIK;;;EAGE,YAAA;EACA,0BAAA;C/B+iIP;A+B3iIG;EAAA;IAIM,YAAA;G/B2iIP;E+B1iIO;;IAEE,YAAA;IACA,8BAAA;G/B4iIT;E+BxiIO;;;IAGE,YAAA;IACA,0BAAA;G/B0iIT;E+BtiIO;;;IAGE,YAAA;IACA,8BAAA;G/BwiIT;CACF;A+BxnID;EAuFI,mBAAA;C/BoiIH;A+BniIG;;EAEE,uBAAA;C/BqiIL;A+B/nID;EA6FM,uBAAA;C/BqiIL;A+BloID;;EAmGI,sBAAA;C/BmiIH;A+BtoID;EA4GI,YAAA;C/B6hIH;A+B5hIG;EACE,YAAA;C/B8hIL;A+B5oID;EAmHI,YAAA;C/B4hIH;A+B3hIG;;EAEE,YAAA;C/B6hIL;A+BzhIK;;;;EAEE,YAAA;C/B6hIP;A+BrhID;EACE,uBAAA;EACA,sBAAA;C/BuhID;A+BzhID;EAKI,eAAA;C/BuhIH;A+BthIG;;EAEE,YAAA;EACA,8BAAA;C/BwhIL;A+BjiID;EAcI,eAAA;C/BshIH;A+BpiID;EAmBM,eAAA;C/BohIL;A+BlhIK;;EAEE,YAAA;EACA,8BAAA;C/BohIP;A+BhhIK;;;EAGE,YAAA;EACA,0BAAA;C/BkhIP;A+B9gIK;;;EAGE,YAAA;EACA,8BAAA;C/BghIP;A+B1gIK;;;EAGE,YAAA;EACA,0BAAA;C/B4gIP;A+BxgIG;EAAA;IAIM,sBAAA;G/BwgIP;E+B5gIC;IAOM,0BAAA;G/BwgIP;E+B/gIC;IAUM,eAAA;G/BwgIP;E+BvgIO;;IAEE,YAAA;IACA,8BAAA;G/BygIT;E+BrgIO;;;IAGE,YAAA;IACA,0BAAA;G/BugIT;E+BngIO;;;IAGE,YAAA;IACA,8BAAA;G/BqgIT;CACF;A+B1lID;EA6FI,mBAAA;C/BggIH;A+B//HG;;EAEE,uBAAA;C/BigIL;A+BjmID;EAmGM,uBAAA;C/BigIL;A+BpmID;;EAyGI,sBAAA;C/B+/HH;A+BxmID;EA6GI,eAAA;C/B8/HH;A+B7/HG;EACE,YAAA;C/B+/HL;A+B9mID;EAoHI,eAAA;C/B6/HH;A+B5/HG;;EAEE,YAAA;C/B8/HL;A+B1/HK;;;;EAEE,YAAA;C/B8/HP;AkCpoJD;EACE,kBAAA;EACA,oBAAA;EACA,iBAAA;EACA,0BAAA;EACA,mBAAA;ClCsoJD;AkC3oJD;EAQI,sBAAA;ClCsoJH;AkC9oJD;EAWM,eAAA;EACA,YAAA;EACA,kBAAA;ClCsoJL;AkCnpJD;EAkBI,eAAA;ClCooJH;AmCxpJD;EACE,sBAAA;EACA,gBAAA;EACA,eAAA;EACA,mBAAA;CnC0pJD;AmC9pJD;EAOI,gBAAA;CnC0pJH;AmCjqJD;;EAUM,mBAAA;EACA,YAAA;EACA,kBAAA;EACA,kBAAA;EACA,wBAAA;EACA,eAAA;EACA,sBAAA;EACA,uBAAA;EACA,uBAAA;CnC2pJL;AmCzpJK;;;;EAEE,WAAA;EACA,eAAA;EACA,0BAAA;EACA,mBAAA;CnC6pJP;AmC1pJG;;EAGI,eAAA;EPnBN,4BAAA;EACA,+BAAA;C5B+qJD;AmCzpJG;;EP/BF,6BAAA;EACA,gCAAA;C5B4rJD;AmCppJG;;;;;;EAGE,WAAA;EACA,YAAA;EACA,gBAAA;EACA,0BAAA;EACA,sBAAA;CnCypJL;AmC7sJD;;;;;;EA+DM,eAAA;EACA,oBAAA;EACA,uBAAA;EACA,mBAAA;CnCspJL;AmC7oJD;;ECxEM,mBAAA;EACA,gBAAA;EACA,uBAAA;CpCytJL;AoCvtJG;;ERKF,4BAAA;EACA,+BAAA;C5BstJD;AoCttJG;;ERTF,6BAAA;EACA,gCAAA;C5BmuJD;AmCxpJD;;EC7EM,kBAAA;EACA,gBAAA;EACA,iBAAA;CpCyuJL;AoCvuJG;;ERKF,4BAAA;EACA,+BAAA;C5BsuJD;AoCtuJG;;ERTF,6BAAA;EACA,gCAAA;C5BmvJD;AqCtvJD;EACE,gBAAA;EACA,eAAA;EACA,mBAAA;EACA,iBAAA;CrCwvJD;AqC5vJD;EAOI,gBAAA;CrCwvJH;AqC/vJD;;EAUM,sBAAA;EACA,kBAAA;EACA,uBAAA;EACA,uBAAA;EACA,oBAAA;CrCyvJL;AqCvwJD;;EAmBM,sBAAA;EACA,0BAAA;CrCwvJL;AqC5wJD;;EA2BM,aAAA;CrCqvJL;AqChxJD;;EAkCM,YAAA;CrCkvJL;AqCpxJD;;;;EA2CM,eAAA;EACA,oBAAA;EACA,uBAAA;CrC+uJL;AsC7xJD;EACE,gBAAA;EACA,2BAAA;EACA,eAAA;EACA,iBAAA;EACA,eAAA;EACA,YAAA;EACA,mBAAA;EACA,oBAAA;EACA,yBAAA;EACA,sBAAA;CtC+xJD;AsC3xJG;;EAEE,YAAA;EACA,sBAAA;EACA,gBAAA;CtC6xJL;AsCxxJC;EACE,cAAA;CtC0xJH;AsCtxJC;EACE,mBAAA;EACA,UAAA;CtCwxJH;AsCjxJD;ECtCE,0BAAA;CvC0zJD;AuCvzJG;;EAEE,0BAAA;CvCyzJL;AsCpxJD;EC1CE,0BAAA;CvCi0JD;AuC9zJG;;EAEE,0BAAA;CvCg0JL;AsCvxJD;EC9CE,0BAAA;CvCw0JD;AuCr0JG;;EAEE,0BAAA;CvCu0JL;AsC1xJD;EClDE,0BAAA;CvC+0JD;AuC50JG;;EAEE,0BAAA;CvC80JL;AsC7xJD;ECtDE,0BAAA;CvCs1JD;AuCn1JG;;EAEE,0BAAA;CvCq1JL;AsChyJD;EC1DE,0BAAA;CvC61JD;AuC11JG;;EAEE,0BAAA;CvC41JL;AwC91JD;EACE,sBAAA;EACA,gBAAA;EACA,iBAAA;EACA,gBAAA;EACA,kBAAA;EACA,eAAA;EACA,YAAA;EACA,mBAAA;EACA,oBAAA;EACA,uBAAA;EACA,0BAAA;EACA,oBAAA;CxCg2JD;AwC71JC;EACE,cAAA;CxC+1JH;AwC31JC;EACE,mBAAA;EACA,UAAA;CxC61JH;AwC11JC;;EAEE,OAAA;EACA,iBAAA;CxC41JH;AwCv1JG;;EAEE,YAAA;EACA,sBAAA;EACA,gBAAA;CxCy1JL;AwCp1JC;;EAEE,eAAA;EACA,uBAAA;CxCs1JH;AwCn1JC;EACE,aAAA;CxCq1JH;AwCl1JC;EACE,kBAAA;CxCo1JH;AwCj1JC;EACE,iBAAA;CxCm1JH;AyC74JD;EACE,kBAAA;EACA,qBAAA;EACA,oBAAA;EACA,eAAA;EACA,0BAAA;CzC+4JD;AyCp5JD;;EASI,eAAA;CzC+4JH;AyCx5JD;EAaI,oBAAA;EACA,gBAAA;EACA,iBAAA;CzC84JH;AyC75JD;EAmBI,0BAAA;CzC64JH;AyC14JC;;EAEE,oBAAA;EACA,mBAAA;EACA,mBAAA;CzC44JH;AyCt6JD;EA8BI,gBAAA;CzC24JH;AyCx4JC;EAAA;IACE,kBAAA;IACA,qBAAA;GzC24JD;EyCz4JC;;IAEE,oBAAA;IACA,mBAAA;GzC24JH;EyCl5JD;;IAYI,gBAAA;GzC04JH;CACF;A0Cr7JD;EACE,eAAA;EACA,aAAA;EACA,oBAAA;EACA,wBAAA;EACA,uBAAA;EACA,uBAAA;EACA,mBAAA;ErCiLA,4CAAA;EACK,uCAAA;EACG,oCAAA;CLuwJT;A0Cj8JD;;EAaI,mBAAA;EACA,kBAAA;C1Cw7JH;A0Cp7JC;;;EAGE,sBAAA;C1Cs7JH;A0C38JD;EA0BI,aAAA;EACA,eAAA;C1Co7JH;A2C/8JD;EACE,cAAA;EACA,oBAAA;EACA,8BAAA;EACA,mBAAA;C3Ci9JD;A2Cr9JD;EAQI,cAAA;EACA,eAAA;C3Cg9JH;A2Cz9JD;EAcI,kBAAA;C3C88JH;A2C59JD;;EAoBI,iBAAA;C3C48JH;A2Ch+JD;EAwBI,gBAAA;C3C28JH;A2Cl8JD;;EAEE,oBAAA;C3Co8JD;A2Ct8JD;;EAMI,mBAAA;EACA,UAAA;EACA,aAAA;EACA,eAAA;C3Co8JH;A2C57JD;ECvDE,eAAA;EACA,0BAAA;EACA,sBAAA;C5Cs/JD;A2Cj8JD;EClDI,0BAAA;C5Cs/JH;A2Cp8JD;EC9CI,eAAA;C5Cq/JH;A2Cn8JD;EC3DE,eAAA;EACA,0BAAA;EACA,sBAAA;C5CigKD;A2Cx8JD;ECtDI,0BAAA;C5CigKH;A2C38JD;EClDI,eAAA;C5CggKH;A2C18JD;EC/DE,eAAA;EACA,0BAAA;EACA,sBAAA;C5C4gKD;A2C/8JD;EC1DI,0BAAA;C5C4gKH;A2Cl9JD;ECtDI,eAAA;C5C2gKH;A2Cj9JD;ECnEE,eAAA;EACA,0BAAA;EACA,sBAAA;C5CuhKD;A2Ct9JD;EC9DI,0BAAA;C5CuhKH;A2Cz9JD;EC1DI,eAAA;C5CshKH;A6CvhKD;EACE;IAAQ,4BAAA;G7C0hKP;E6CzhKD;IAAQ,yBAAA;G7C4hKP;CACF;A6CzhKD;EACE;IAAQ,4BAAA;G7C4hKP;E6C3hKD;IAAQ,yBAAA;G7C8hKP;CACF;A6CjiKD;EACE;IAAQ,4BAAA;G7C4hKP;E6C3hKD;IAAQ,yBAAA;G7C8hKP;CACF;A6CvhKD;EACE,aAAA;EACA,oBAAA;EACA,iBAAA;EACA,0BAAA;EACA,mBAAA;ExCsCA,uDAAA;EACQ,+CAAA;CLo/JT;A6CthKD;EACE,YAAA;EACA,UAAA;EACA,aAAA;EACA,gBAAA;EACA,kBAAA;EACA,YAAA;EACA,mBAAA;EACA,0BAAA;ExCyBA,uDAAA;EACQ,+CAAA;EAyHR,oCAAA;EACK,+BAAA;EACG,4BAAA;CLw4JT;A6CnhKD;;ECDI,8MAAA;EACA,yMAAA;EACA,sMAAA;EDEF,mCAAA;EAAA,2BAAA;C7CuhKD;A6ChhKD;;ExC5CE,2DAAA;EACK,sDAAA;EACG,mDAAA;CLgkKT;A6C7gKD;EEvEE,0BAAA;C/CulKD;A+CplKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9CuiKH;A6CjhKD;EE3EE,0BAAA;C/C+lKD;A+C5lKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9C+iKH;A6CrhKD;EE/EE,0BAAA;C/CumKD;A+CpmKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9CujKH;A6CzhKD;EEnFE,0BAAA;C/C+mKD;A+C5mKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9C+jKH;AgDvnKD;EAEE,iBAAA;ChDwnKD;AgDtnKC;EACE,cAAA;ChDwnKH;AgDpnKD;;EAEE,iBAAA;EACA,QAAA;ChDsnKD;AgDnnKD;EACE,eAAA;ChDqnKD;AgDlnKD;EACE,eAAA;ChDonKD;AgDjnKC;EACE,gBAAA;ChDmnKH;AgD/mKD;;EAEE,mBAAA;ChDinKD;AgD9mKD;;EAEE,oBAAA;ChDgnKD;AgD7mKD;;;EAGE,oBAAA;EACA,oBAAA;ChD+mKD;AgD5mKD;EACE,uBAAA;ChD8mKD;AgD3mKD;EACE,uBAAA;ChD6mKD;AgDzmKD;EACE,cAAA;EACA,mBAAA;ChD2mKD;AgDrmKD;EACE,gBAAA;EACA,iBAAA;ChDumKD;AiD5pKD;EAEE,gBAAA;EACA,oBAAA;CjD6pKD;AiDrpKD;EACE,mBAAA;EACA,eAAA;EACA,mBAAA;EAEA,oBAAA;EACA,uBAAA;EACA,uBAAA;CjDspKD;AiDnpKC;ErB7BA,4BAAA;EACA,6BAAA;C5BmrKD;AiDppKC;EACE,iBAAA;ErBzBF,gCAAA;EACA,+BAAA;C5BgrKD;AiDnpKC;;;EAGE,eAAA;EACA,oBAAA;EACA,0BAAA;CjDqpKH;AiD1pKC;;;EASI,eAAA;CjDspKL;AiD/pKC;;;EAYI,eAAA;CjDwpKL;AiDnpKC;;;EAGE,WAAA;EACA,YAAA;EACA,0BAAA;EACA,sBAAA;CjDqpKH;AiD3pKC;;;;;;;;;EAYI,eAAA;CjD0pKL;AiDtqKC;;;EAeI,eAAA;CjD4pKL;AiDjpKD;;EAEE,YAAA;CjDmpKD;AiDrpKD;;EAKI,YAAA;CjDopKH;AiDhpKC;;;;EAEE,YAAA;EACA,sBAAA;EACA,0BAAA;CjDopKH;AiDhpKD;EACE,YAAA;EACA,iBAAA;CjDkpKD;AczvKA;EoCIG,eAAA;EACA,0BAAA;ClDwvKH;AkDtvKG;;EAEE,eAAA;ClDwvKL;AkD1vKG;;EAKI,eAAA;ClDyvKP;AkDtvKK;;;;EAEE,eAAA;EACA,0BAAA;ClD0vKP;AkDxvKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClD6vKP;ActxKA;EoCIG,eAAA;EACA,0BAAA;ClDqxKH;AkDnxKG;;EAEE,eAAA;ClDqxKL;AkDvxKG;;EAKI,eAAA;ClDsxKP;AkDnxKK;;;;EAEE,eAAA;EACA,0BAAA;ClDuxKP;AkDrxKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClD0xKP;AcnzKA;EoCIG,eAAA;EACA,0BAAA;ClDkzKH;AkDhzKG;;EAEE,eAAA;ClDkzKL;AkDpzKG;;EAKI,eAAA;ClDmzKP;AkDhzKK;;;;EAEE,eAAA;EACA,0BAAA;ClDozKP;AkDlzKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClDuzKP;Ach1KA;EoCIG,eAAA;EACA,0BAAA;ClD+0KH;AkD70KG;;EAEE,eAAA;ClD+0KL;AkDj1KG;;EAKI,eAAA;ClDg1KP;AkD70KK;;;;EAEE,eAAA;EACA,0BAAA;ClDi1KP;AkD/0KK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClDo1KP;AiDnvKD;EACE,cAAA;EACA,mBAAA;CjDqvKD;AiDnvKD;EACE,iBAAA;EACA,iBAAA;CjDqvKD;AmD72KD;EACE,oBAAA;EACA,uBAAA;EACA,8BAAA;EACA,mBAAA;E9C0DA,kDAAA;EACQ,0CAAA;CLszKT;AmD52KD;EACE,cAAA;CnD82KD;AmDz2KD;EACE,mBAAA;EACA,qCAAA;EvBtBA,4BAAA;EACA,6BAAA;C5Bk4KD;AmD/2KD;EAMI,eAAA;CnD42KH;AmDv2KD;EACE,cAAA;EACA,iBAAA;EACA,gBAAA;EACA,eAAA;CnDy2KD;AmD72KD;;;;;EAWI,eAAA;CnDy2KH;AmDp2KD;EACE,mBAAA;EACA,0BAAA;EACA,2BAAA;EvB1CA,gCAAA;EACA,+BAAA;C5Bi5KD;AmD91KD;;EAGI,iBAAA;CnD+1KH;AmDl2KD;;EAMM,oBAAA;EACA,iBAAA;CnDg2KL;AmD51KG;;EAEI,cAAA;EvBzEN,4BAAA;EACA,6BAAA;C5Bw6KD;AmD11KG;;EAEI,iBAAA;EvBzEN,gCAAA;EACA,+BAAA;C5Bs6KD;AmDn3KD;EvB5DE,0BAAA;EACA,2BAAA;C5Bk7KD;AmDt1KD;EAEI,oBAAA;CnDu1KH;AmDp1KD;EACE,oBAAA;CnDs1KD;AmD90KD;;;EAII,iBAAA;CnD+0KH;AmDn1KD;;;EAOM,oBAAA;EACA,mBAAA;CnDi1KL;AmDz1KD;;EvB3GE,4BAAA;EACA,6BAAA;C5Bw8KD;AmD91KD;;;;EAmBQ,4BAAA;EACA,6BAAA;CnDi1KP;AmDr2KD;;;;;;;;EAwBU,4BAAA;CnDu1KT;AmD/2KD;;;;;;;;EA4BU,6BAAA;CnD61KT;AmDz3KD;;EvBnGE,gCAAA;EACA,+BAAA;C5Bg+KD;AmD93KD;;;;EAyCQ,gCAAA;EACA,+BAAA;CnD21KP;AmDr4KD;;;;;;;;EA8CU,+BAAA;CnDi2KT;AmD/4KD;;;;;;;;EAkDU,gCAAA;CnDu2KT;AmDz5KD;;;;EA2DI,2BAAA;CnDo2KH;AmD/5KD;;EA+DI,cAAA;CnDo2KH;AmDn6KD;;EAmEI,UAAA;CnDo2KH;AmDv6KD;;;;;;;;;;;;EA0EU,eAAA;CnD22KT;AmDr7KD;;;;;;;;;;;;EA8EU,gBAAA;CnDq3KT;AmDn8KD;;;;;;;;EAuFU,iBAAA;CnDs3KT;AmD78KD;;;;;;;;EAgGU,iBAAA;CnDu3KT;AmDv9KD;EAsGI,iBAAA;EACA,UAAA;CnDo3KH;AmD12KD;EACE,oBAAA;CnD42KD;AmD72KD;EAKI,iBAAA;EACA,mBAAA;CnD22KH;AmDj3KD;EASM,gBAAA;CnD22KL;AmDp3KD;EAcI,iBAAA;CnDy2KH;AmDv3KD;;EAkBM,2BAAA;CnDy2KL;AmD33KD;EAuBI,cAAA;CnDu2KH;AmD93KD;EAyBM,8BAAA;CnDw2KL;AmDj2KD;EC5PE,mBAAA;CpDgmLD;AoD9lLC;EACE,eAAA;EACA,0BAAA;EACA,mBAAA;CpDgmLH;AoDnmLC;EAMI,uBAAA;CpDgmLL;AoDtmLC;EASI,eAAA;EACA,0BAAA;CpDgmLL;AoD7lLC;EAEI,0BAAA;CpD8lLL;AmDh3KD;EC/PE,sBAAA;CpDknLD;AoDhnLC;EACE,YAAA;EACA,0BAAA;EACA,sBAAA;CpDknLH;AoDrnLC;EAMI,0BAAA;CpDknLL;AoDxnLC;EASI,eAAA;EACA,uBAAA;CpDknLL;AoD/mLC;EAEI,6BAAA;CpDgnLL;AmD/3KD;EClQE,sBAAA;CpDooLD;AoDloLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDooLH;AoDvoLC;EAMI,0BAAA;CpDooLL;AoD1oLC;EASI,eAAA;EACA,0BAAA;CpDooLL;AoDjoLC;EAEI,6BAAA;CpDkoLL;AmD94KD;ECrQE,sBAAA;CpDspLD;AoDppLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDspLH;AoDzpLC;EAMI,0BAAA;CpDspLL;AoD5pLC;EASI,eAAA;EACA,0BAAA;CpDspLL;AoDnpLC;EAEI,6BAAA;CpDopLL;AmD75KD;ECxQE,sBAAA;CpDwqLD;AoDtqLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDwqLH;AoD3qLC;EAMI,0BAAA;CpDwqLL;AoD9qLC;EASI,eAAA;EACA,0BAAA;CpDwqLL;AoDrqLC;EAEI,6BAAA;CpDsqLL;AmD56KD;EC3QE,sBAAA;CpD0rLD;AoDxrLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpD0rLH;AoD7rLC;EAMI,0BAAA;CpD0rLL;AoDhsLC;EASI,eAAA;EACA,0BAAA;CpD0rLL;AoDvrLC;EAEI,6BAAA;CpDwrLL;AqDxsLD;EACE,mBAAA;EACA,eAAA;EACA,UAAA;EACA,WAAA;EACA,iBAAA;CrD0sLD;AqD/sLD;;;;;EAYI,mBAAA;EACA,OAAA;EACA,UAAA;EACA,QAAA;EACA,YAAA;EACA,aAAA;EACA,UAAA;CrD0sLH;AqDrsLD;EACE,uBAAA;CrDusLD;AqDnsLD;EACE,oBAAA;CrDqsLD;AsDhuLD;EACE,iBAAA;EACA,cAAA;EACA,oBAAA;EACA,0BAAA;EACA,0BAAA;EACA,mBAAA;EjD0DA,wDAAA;EACQ,gDAAA;CLyqLT;AsD1uLD;EASI,mBAAA;EACA,kCAAA;CtDouLH;AsD/tLD;EACE,cAAA;EACA,mBAAA;CtDiuLD;AsD/tLD;EACE,aAAA;EACA,mBAAA;CtDiuLD;AuDrvLD;EACE,aAAA;EACA,gBAAA;EACA,kBAAA;EACA,eAAA;EACA,YAAA;EACA,0BAAA;EjCTA,0BAAA;EACA,aAAA;CtBiwLD;AuDtvLC;;EAEE,YAAA;EACA,sBAAA;EACA,gBAAA;EjChBF,0BAAA;EACA,aAAA;CtBywLD;AuDlvLC;EACE,WAAA;EACA,gBAAA;EACA,wBAAA;EACA,UAAA;EACA,yBAAA;EACA,sBAAA;EAAA,iBAAA;CvDovLH;AwD5wLD;EACE,iBAAA;CxD8wLD;AwD1wLD;EACE,gBAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,cAAA;EACA,iBAAA;EACA,kCAAA;EAIA,WAAA;CxDywLD;AwDtwLC;EnDiHA,sCAAA;EACI,kCAAA;EACC,iCAAA;EACG,8BAAA;EAkER,oDAAA;EAEK,0CAAA;EACG,4CAAA;EAAA,oCAAA;EAAA,iGAAA;CLulLT;AwD5wLC;EnD6GA,mCAAA;EACI,+BAAA;EACC,8BAAA;EACG,2BAAA;CLkqLT;AwDhxLD;EACE,mBAAA;EACA,iBAAA;CxDkxLD;AwD9wLD;EACE,mBAAA;EACA,YAAA;EACA,aAAA;CxDgxLD;AwD5wLD;EACE,mBAAA;EACA,uBAAA;EACA,6BAAA;EACA,uBAAA;EACA,qCAAA;EACA,mBAAA;EnDcA,iDAAA;EACQ,yCAAA;EmDZR,WAAA;CxD8wLD;AwD1wLD;EACE,gBAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,uBAAA;CxD4wLD;AwD1wLC;ElCpEA,yBAAA;EACA,WAAA;CtBi1LD;AwD7wLC;ElCrEA,0BAAA;EACA,aAAA;CtBq1LD;AwD5wLD;EACE,cAAA;EACA,iCAAA;CxD8wLD;AwD1wLD;EACE,iBAAA;CxD4wLD;AwDxwLD;EACE,UAAA;EACA,wBAAA;CxD0wLD;AwDrwLD;EACE,mBAAA;EACA,cAAA;CxDuwLD;AwDnwLD;EACE,cAAA;EACA,kBAAA;EACA,8BAAA;CxDqwLD;AwDxwLD;EAQI,iBAAA;EACA,iBAAA;CxDmwLH;AwD5wLD;EAaI,kBAAA;CxDkwLH;AwD/wLD;EAiBI,eAAA;CxDiwLH;AwD5vLD;EACE,mBAAA;EACA,aAAA;EACA,YAAA;EACA,aAAA;EACA,iBAAA;CxD8vLD;AwD1vLD;EAEE;IACE,aAAA;IACA,kBAAA;GxD2vLD;EwDzvLD;InDrEA,kDAAA;IACQ,0CAAA;GLi0LP;EwDxvLD;IAAY,aAAA;GxD2vLX;CACF;AwDzvLD;EACE;IAAY,aAAA;GxD4vLX;CACF;AyD34LD;EACE,mBAAA;EACA,cAAA;EACA,eAAA;ECRA,4DAAA;EAEA,mBAAA;EACA,iBAAA;EACA,wBAAA;EACA,iBAAA;EACA,iBAAA;EACA,kBAAA;EACA,sBAAA;EACA,kBAAA;EACA,qBAAA;EACA,uBAAA;EACA,mBAAA;EACA,qBAAA;EACA,kBAAA;EACA,oBAAA;EDHA,gBAAA;EnCTA,yBAAA;EACA,WAAA;CtBm6LD;AyDv5LC;EnCbA,0BAAA;EACA,aAAA;CtBu6LD;AyD15LC;EACE,eAAA;EACA,iBAAA;CzD45LH;AyD15LC;EACE,eAAA;EACA,iBAAA;CzD45LH;AyD15LC;EACE,eAAA;EACA,gBAAA;CzD45LH;AyD15LC;EACE,eAAA;EACA,kBAAA;CzD45LH;AyDx5LC;EACE,UAAA;EACA,UAAA;EACA,kBAAA;EACA,wBAAA;EACA,uBAAA;CzD05LH;AyDx5LC;EACE,WAAA;EACA,UAAA;EACA,oBAAA;EACA,wBAAA;EACA,uBAAA;CzD05LH;AyDx5LC;EACE,UAAA;EACA,UAAA;EACA,oBAAA;EACA,wBAAA;EACA,uBAAA;CzD05LH;AyDx5LC;EACE,SAAA;EACA,QAAA;EACA,iBAAA;EACA,4BAAA;EACA,yBAAA;CzD05LH;AyDx5LC;EACE,SAAA;EACA,SAAA;EACA,iBAAA;EACA,4BAAA;EACA,wBAAA;CzD05LH;AyDx5LC;EACE,OAAA;EACA,UAAA;EACA,kBAAA;EACA,wBAAA;EACA,0BAAA;CzD05LH;AyDx5LC;EACE,OAAA;EACA,WAAA;EACA,iBAAA;EACA,wBAAA;EACA,0BAAA;CzD05LH;AyDx5LC;EACE,OAAA;EACA,UAAA;EACA,iBAAA;EACA,wBAAA;EACA,0BAAA;CzD05LH;AyDr5LD;EACE,iBAAA;EACA,iBAAA;EACA,YAAA;EACA,mBAAA;EACA,uBAAA;EACA,mBAAA;CzDu5LD;AyDn5LD;EACE,mBAAA;EACA,SAAA;EACA,UAAA;EACA,0BAAA;EACA,oBAAA;CzDq5LD;A2D9/LD;EACE,mBAAA;EACA,OAAA;EACA,QAAA;EACA,cAAA;EACA,cAAA;EACA,iBAAA;EACA,aAAA;EDXA,4DAAA;EAEA,mBAAA;EACA,iBAAA;EACA,wBAAA;EACA,iBAAA;EACA,iBAAA;EACA,kBAAA;EACA,sBAAA;EACA,kBAAA;EACA,qBAAA;EACA,uBAAA;EACA,mBAAA;EACA,qBAAA;EACA,kBAAA;EACA,oBAAA;ECAA,gBAAA;EACA,uBAAA;EACA,6BAAA;EACA,uBAAA;EACA,qCAAA;EACA,mBAAA;EtDiDA,kDAAA;EACQ,0CAAA;CL49LT;A2D1gMC;EAAQ,kBAAA;C3D6gMT;A2D5gMC;EAAU,kBAAA;C3D+gMX;A2D9gMC;EAAW,iBAAA;C3DihMZ;A2DhhMC;EAAS,mBAAA;C3DmhMV;A2D1iMD;EA4BI,mBAAA;C3DihMH;A2D/gMG;;EAEE,mBAAA;EACA,eAAA;EACA,SAAA;EACA,UAAA;EACA,0BAAA;EACA,oBAAA;C3DihML;A2D9gMG;EACE,YAAA;EACA,mBAAA;C3DghML;A2D5gMC;EACE,cAAA;EACA,UAAA;EACA,mBAAA;EACA,0BAAA;EACA,sCAAA;EACA,uBAAA;C3D8gMH;A2D7gMG;EACE,YAAA;EACA,mBAAA;EACA,aAAA;EACA,uBAAA;EACA,uBAAA;C3D+gML;A2D5gMC;EACE,SAAA;EACA,YAAA;EACA,kBAAA;EACA,4BAAA;EACA,wCAAA;EACA,qBAAA;C3D8gMH;A2D7gMG;EACE,cAAA;EACA,UAAA;EACA,aAAA;EACA,yBAAA;EACA,qBAAA;C3D+gML;A2D5gMC;EACE,WAAA;EACA,UAAA;EACA,mBAAA;EACA,oBAAA;EACA,6BAAA;EACA,yCAAA;C3D8gMH;A2D7gMG;EACE,SAAA;EACA,mBAAA;EACA,aAAA;EACA,oBAAA;EACA,0BAAA;C3D+gML;A2D3gMC;EACE,SAAA;EACA,aAAA;EACA,kBAAA;EACA,sBAAA;EACA,2BAAA;EACA,uCAAA;C3D6gMH;A2D5gMG;EACE,WAAA;EACA,cAAA;EACA,aAAA;EACA,sBAAA;EACA,wBAAA;C3D8gML;A2DzgMD;EACE,kBAAA;EACA,UAAA;EACA,gBAAA;EACA,0BAAA;EACA,iCAAA;EACA,2BAAA;C3D2gMD;A2DxgMD;EACE,kBAAA;C3D0gMD;A4D9nMD;EACE,mBAAA;C5DgoMD;A4D7nMD;EACE,mBAAA;EACA,YAAA;EACA,iBAAA;C5D+nMD;A4DloMD;EAMI,mBAAA;EACA,cAAA;EvD6KF,0CAAA;EACK,qCAAA;EACG,kCAAA;CLm9LT;A4DzoMD;;EAcM,eAAA;C5D+nML;A4D3nMG;EAAA;IvDuLF,uDAAA;IAEK,6CAAA;IACG,+CAAA;IAAA,uCAAA;IAAA,0GAAA;IA7JR,oCAAA;IAEQ,4BAAA;IA+GR,4BAAA;IAEQ,oBAAA;GLw/LP;E4DnoMG;;IvDmHJ,2CAAA;IACQ,mCAAA;IuDjHF,QAAA;G5DsoML;E4DpoMG;;IvD8GJ,4CAAA;IACQ,oCAAA;IuD5GF,QAAA;G5DuoML;E4DroMG;;;IvDyGJ,wCAAA;IACQ,gCAAA;IuDtGF,QAAA;G5DwoML;CACF;A4D9qMD;;;EA6CI,eAAA;C5DsoMH;A4DnrMD;EAiDI,QAAA;C5DqoMH;A4DtrMD;;EAsDI,mBAAA;EACA,OAAA;EACA,YAAA;C5DooMH;A4D5rMD;EA4DI,WAAA;C5DmoMH;A4D/rMD;EA+DI,YAAA;C5DmoMH;A4DlsMD;;EAmEI,QAAA;C5DmoMH;A4DtsMD;EAuEI,YAAA;C5DkoMH;A4DzsMD;EA0EI,WAAA;C5DkoMH;A4D1nMD;EACE,mBAAA;EACA,OAAA;EACA,UAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;EACA,YAAA;EACA,mBAAA;EACA,0CAAA;EACA,mCAAA;EtCpGA,0BAAA;EACA,aAAA;CtBiuMD;A4DxnMC;EdrGE,mGAAA;EACA,8FAAA;EACA,qHAAA;EAAA,+FAAA;EACA,uHAAA;EACA,4BAAA;C9CguMH;A4D5nMC;EACE,SAAA;EACA,WAAA;Ed1GA,mGAAA;EACA,8FAAA;EACA,qHAAA;EAAA,+FAAA;EACA,uHAAA;EACA,4BAAA;C9CyuMH;A4D9nMC;;EAEE,YAAA;EACA,sBAAA;EACA,WAAA;EtCxHF,0BAAA;EACA,aAAA;CtByvMD;A4DhqMD;;;;EAuCI,mBAAA;EACA,SAAA;EACA,WAAA;EACA,sBAAA;EACA,kBAAA;C5D+nMH;A4D1qMD;;EA+CI,UAAA;EACA,mBAAA;C5D+nMH;A4D/qMD;;EAoDI,WAAA;EACA,oBAAA;C5D+nMH;A4DprMD;;EAyDI,YAAA;EACA,aAAA;EACA,mBAAA;EACA,eAAA;C5D+nMH;A4D3nMG;EACE,iBAAA;C5D6nML;A4DznMG;EACE,iBAAA;C5D2nML;A4DjnMD;EACE,mBAAA;EACA,aAAA;EACA,UAAA;EACA,YAAA;EACA,WAAA;EACA,gBAAA;EACA,kBAAA;EACA,mBAAA;EACA,iBAAA;C5DmnMD;A4D5nMD;EAYI,sBAAA;EACA,YAAA;EACA,aAAA;EACA,YAAA;EACA,oBAAA;EACA,gBAAA;EAUA,0BAAA;EACA,mCAAA;EAEA,uBAAA;EACA,oBAAA;C5DymMH;A4DxoMD;EAmCI,YAAA;EACA,aAAA;EACA,UAAA;EACA,uBAAA;C5DwmMH;A4DjmMD;EACE,mBAAA;EACA,WAAA;EACA,aAAA;EACA,UAAA;EACA,YAAA;EACA,kBAAA;EACA,qBAAA;EACA,YAAA;EACA,mBAAA;EACA,0CAAA;C5DmmMD;A4DjmMC;EACE,kBAAA;C5DmmMH;A4D7lMD;EAGE;;;;IAKI,YAAA;IACA,aAAA;IACA,kBAAA;IACA,gBAAA;G5D4lMH;E4DpmMD;;IAYI,mBAAA;G5D4lMH;E4DxmMD;;IAgBI,oBAAA;G5D4lMH;E4DvlMD;IACE,WAAA;IACA,UAAA;IACA,qBAAA;G5DylMD;E4DrlMD;IACE,aAAA;G5DulMD;CACF;A6Dz1MC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEE,eAAA;EACA,aAAA;C7Dy3MH;A6Dv3MC;;;;;;;;;;;;;;;;EACE,YAAA;C7Dw4MH;AiC94MD;E6BVE,eAAA;EACA,mBAAA;EACA,kBAAA;C9D25MD;AiCh5MD;EACE,wBAAA;CjCk5MD;AiCh5MD;EACE,uBAAA;CjCk5MD;AiC14MD;EACE,yBAAA;CjC44MD;AiC14MD;EACE,0BAAA;CjC44MD;AiC14MD;EACE,mBAAA;CjC44MD;AiC14MD;E8BzBE,YAAA;EACA,mBAAA;EACA,kBAAA;EACA,8BAAA;EACA,UAAA;C/Ds6MD;AiCx4MD;EACE,yBAAA;CjC04MD;AiCn4MD;EACE,gBAAA;CjCq4MD;AgEt6MD;EACE,oBAAA;ChEw6MD;AgEl6MD;;;;EClBE,yBAAA;CjE07MD;AgEj6MD;;;;;;;;;;;;EAYE,yBAAA;ChEm6MD;AgE/5MC;EAAA;ICjDA,0BAAA;GjEo9MC;EiEn9MD;IAAU,0BAAA;GjEs9MT;EiEr9MD;IAAU,8BAAA;GjEw9MT;EiEv9MD;;IACU,+BAAA;GjE09MT;CACF;AgEz6MC;EAAA;IACE,0BAAA;GhE46MD;CACF;AgEz6MC;EAAA;IACE,2BAAA;GhE46MD;CACF;AgEz6MC;EAAA;IACE,iCAAA;GhE46MD;CACF;AgEx6MC;EAAA;ICtEA,0BAAA;GjEk/MC;EiEj/MD;IAAU,0BAAA;GjEo/MT;EiEn/MD;IAAU,8BAAA;GjEs/MT;EiEr/MD;;IACU,+BAAA;GjEw/MT;CACF;AgEl7MC;EAAA;IACE,0BAAA;GhEq7MD;CACF;AgEl7MC;EAAA;IACE,2BAAA;GhEq7MD;CACF;AgEl7MC;EAAA;IACE,iCAAA;GhEq7MD;CACF;AgEj7MC;EAAA;IC3FA,0BAAA;GjEghNC;EiE/gND;IAAU,0BAAA;GjEkhNT;EiEjhND;IAAU,8BAAA;GjEohNT;EiEnhND;;IACU,+BAAA;GjEshNT;CACF;AgE37MC;EAAA;IACE,0BAAA;GhE87MD;CACF;AgE37MC;EAAA;IACE,2BAAA;GhE87MD;CACF;AgE37MC;EAAA;IACE,iCAAA;GhE87MD;CACF;AgE17MC;EAAA;IChHA,0BAAA;GjE8iNC;EiE7iND;IAAU,0BAAA;GjEgjNT;EiE/iND;IAAU,8BAAA;GjEkjNT;EiEjjND;;IACU,+BAAA;GjEojNT;CACF;AgEp8MC;EAAA;IACE,0BAAA;GhEu8MD;CACF;AgEp8MC;EAAA;IACE,2BAAA;GhEu8MD;CACF;AgEp8MC;EAAA;IACE,iCAAA;GhEu8MD;CACF;AgEn8MC;EAAA;IC7HA,yBAAA;GjEokNC;CACF;AgEn8MC;EAAA;IClIA,yBAAA;GjEykNC;CACF;AgEn8MC;EAAA;ICvIA,yBAAA;GjE8kNC;CACF;AgEn8MC;EAAA;IC5IA,yBAAA;GjEmlNC;CACF;AgE77MD;ECvJE,yBAAA;CjEulND;AgE77MC;EAAA;IClKA,0BAAA;GjEmmNC;EiElmND;IAAU,0BAAA;GjEqmNT;EiEpmND;IAAU,8BAAA;GjEumNT;EiEtmND;;IACU,+BAAA;GjEymNT;CACF;AgEx8MD;EACE,yBAAA;ChE08MD;AgEx8MC;EAAA;IACE,0BAAA;GhE28MD;CACF;AgEz8MD;EACE,yBAAA;ChE28MD;AgEz8MC;EAAA;IACE,2BAAA;GhE48MD;CACF;AgE18MD;EACE,yBAAA;ChE48MD;AgE18MC;EAAA;IACE,iCAAA;GhE68MD;CACF;AgEz8MC;EAAA;ICrLA,yBAAA;GjEkoNC;CACF","file":"bootstrap.css","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","// 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