├── .gitignore ├── README.md ├── docker ├── Dockerfile ├── build.bat ├── build.sh └── docker-compose.yml ├── pom.xml ├── sql ├── sms_bomb.mysql.sql └── sms_bomb.sqlite3.sql ├── src ├── main │ ├── java │ │ └── com │ │ │ └── lei2j │ │ │ └── sms │ │ │ └── bomb │ │ │ ├── SmsBombApplication.java │ │ │ ├── base │ │ │ └── entity │ │ │ │ └── Pager.java │ │ │ ├── dao │ │ │ └── BaseDao.java │ │ │ ├── dto │ │ │ └── SmsSendDTO.java │ │ │ ├── entity │ │ │ ├── EntityBasePackage.java │ │ │ ├── SmsSendLog.java │ │ │ ├── SmsUrlConfig.java │ │ │ └── package-info.java │ │ │ ├── ocr │ │ │ ├── DdddOcrServiceImpl.java │ │ │ ├── OcrRequest.java │ │ │ └── OcrService.java │ │ │ ├── repository │ │ │ ├── CommonJpaRepository.java │ │ │ ├── SmsSendLogRepository.java │ │ │ └── SmsUrlConfigRepository.java │ │ │ ├── service │ │ │ └── impl │ │ │ │ ├── CancelException.java │ │ │ │ ├── CommonServiceImpl.java │ │ │ │ ├── GroovySmsScriptExecutorService.java │ │ │ │ ├── RetryInvokeException.java │ │ │ │ ├── ScriptContext.java │ │ │ │ ├── ScriptThreadContext.java │ │ │ │ ├── SmsSendService.java │ │ │ │ ├── SmsUrlConfigService.java │ │ │ │ └── SmsUrlConfigServiceImpl.java │ │ │ ├── util │ │ │ ├── HttpUtils.java │ │ │ ├── IgnoreEmptyStringValueTransformer.java │ │ │ └── SpringApplicationUtils.java │ │ │ └── web │ │ │ ├── config │ │ │ ├── BeanConfiguration.java │ │ │ └── WebConfig.java │ │ │ ├── controller │ │ │ ├── SmsBombController.java │ │ │ ├── SmsUrlConfigController.java │ │ │ └── ViewController.java │ │ │ └── interceptor │ │ │ └── ClientIpInterceptor.java │ └── resources │ │ ├── META-INF │ │ └── additional-spring-configuration-metadata.json │ │ ├── application-dev.properties │ │ ├── application.properties │ │ ├── com │ │ └── lei2j │ │ │ └── sms │ │ │ └── bomb │ │ │ └── script │ │ │ ├── SmsCommonScript.groovy │ │ │ └── SmsScript.groovy │ │ ├── db │ │ ├── README.MD │ │ └── sms_bomb.db │ │ ├── file-appender.xml │ │ ├── logback-spring.xml │ │ ├── static │ │ ├── img │ │ │ └── favicon.ico │ │ └── js │ │ │ ├── bootstrap@4.6.0 │ │ │ ├── css │ │ │ │ ├── bootstrap-grid.css │ │ │ │ ├── bootstrap-grid.css.map │ │ │ │ ├── bootstrap-grid.min.css │ │ │ │ ├── bootstrap-grid.min.css.map │ │ │ │ ├── bootstrap-reboot.css │ │ │ │ ├── bootstrap-reboot.css.map │ │ │ │ ├── bootstrap-reboot.min.css │ │ │ │ ├── bootstrap-reboot.min.css.map │ │ │ │ ├── bootstrap.css │ │ │ │ ├── bootstrap.css.map │ │ │ │ ├── bootstrap.min.css │ │ │ │ └── bootstrap.min.css.map │ │ │ ├── flat-ui │ │ │ │ ├── css │ │ │ │ │ ├── flat-ui.css │ │ │ │ │ └── flat-ui.min.css │ │ │ │ ├── flat-ui.js │ │ │ │ ├── flat-ui.min.js │ │ │ │ └── fonts │ │ │ │ │ ├── glyphicons │ │ │ │ │ ├── flat-ui-pro-icons-regular.eot │ │ │ │ │ ├── flat-ui-pro-icons-regular.svg │ │ │ │ │ ├── flat-ui-pro-icons-regular.ttf │ │ │ │ │ ├── flat-ui-pro-icons-regular.woff │ │ │ │ │ └── selection.json │ │ │ │ │ └── lato │ │ │ │ │ ├── lato-black.eot │ │ │ │ │ ├── lato-black.svg │ │ │ │ │ ├── lato-black.ttf │ │ │ │ │ ├── lato-black.woff │ │ │ │ │ ├── lato-bold.eot │ │ │ │ │ ├── lato-bold.svg │ │ │ │ │ ├── lato-bold.ttf │ │ │ │ │ ├── lato-bold.woff │ │ │ │ │ ├── lato-bolditalic.eot │ │ │ │ │ ├── lato-bolditalic.svg │ │ │ │ │ ├── lato-bolditalic.ttf │ │ │ │ │ ├── lato-bolditalic.woff │ │ │ │ │ ├── lato-italic.eot │ │ │ │ │ ├── lato-italic.svg │ │ │ │ │ ├── lato-italic.ttf │ │ │ │ │ ├── lato-italic.woff │ │ │ │ │ ├── lato-light.eot │ │ │ │ │ ├── lato-light.svg │ │ │ │ │ ├── lato-light.ttf │ │ │ │ │ ├── lato-light.woff │ │ │ │ │ ├── lato-regular.eot │ │ │ │ │ ├── lato-regular.svg │ │ │ │ │ ├── lato-regular.ttf │ │ │ │ │ └── lato-regular.woff │ │ │ ├── fonts │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ ├── js │ │ │ │ ├── bootstrap.bundle.js │ │ │ │ ├── bootstrap.bundle.js.map │ │ │ │ ├── bootstrap.bundle.min.js │ │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ │ ├── bootstrap.js │ │ │ │ ├── bootstrap.js.map │ │ │ │ ├── bootstrap.min.js │ │ │ │ └── bootstrap.min.js.map │ │ │ └── plugins │ │ │ │ ├── bootstrap-switch.min.css │ │ │ │ ├── bootstrap-switch.min.js │ │ │ │ ├── bootstrap2-toggle.min.css │ │ │ │ ├── bootstrap2-toggle.min.js │ │ │ │ └── bootstrap2-toggle.min.js.map │ │ │ ├── common │ │ │ └── public.js │ │ │ ├── jquery@3.3.1 │ │ │ ├── jquery-3.3.1.js │ │ │ └── jquery-3.3.1.min.js │ │ │ ├── popper@1.12.3 │ │ │ ├── popper.js │ │ │ └── popper.min.js │ │ │ ├── vue.js │ │ │ └── vue.min.js │ │ └── templates │ │ ├── admin │ │ └── sms │ │ │ ├── edit.html │ │ │ └── list.html │ │ ├── error │ │ ├── 404.html │ │ └── 5xx.html │ │ └── index.html └── test │ └── java │ └── com │ └── lei2j │ └── sms │ └── bomb │ └── AppTest.java └── update_version.bat /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /**/target/ 3 | /**/bin/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | /**/application-prod.properties 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | /**/.idea/** 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /build/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | /classpath 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 短信炸弹 2 | 该程序使用手机号往各个网站渠道发送短信验证码,从而对手机进行短信轰炸。 3 | 轰炸机仅供学习使用。 4 | ## 轰炸原理 5 | 搜集各个网站页面上发送短信验证码使用的api接口,利用这些api接口来发送短信验证码。 6 | ## 描述 7 | 该程序会内置少量的资源以供使用。但不负责提供网站发送验证码资源。网站短信发送接口资源可以通过后台页面 8 | 添加。一般地,新添加网站短信资源需要编写扩展脚本,需要具有Groovy知识。 9 | ## 快速开始 10 | 1. 配置JDK 11 | 2. 下载源码安装 12 | ``` 13 | git clone https://github.com/leijinjun/sms-bomb.git 14 | cd sms-bomb 15 | mvn clean install -Dmaven.test.skip=true 16 | mv target/sms-bomb.jar ./ 17 | Linux、MacOS平台下执行 18 | java -jar -Dspring.profiles.active=dev -DDB_FILE_PATH=sqlite3DB文件路径 -Docr.dddd.url=ddddocr服务图片识别地址 -Docr.dddd.base64.url=ddddocr服务base64图片识别地址 sms-bomb.jar 19 | Windows平台下执行 20 | java -jar "-Dspring.profiles.active=dev" "-DDB_FILE_PATH=sqlite3DB文件路径" "-Docr.dddd.url=ddddocr服务图片识别地址" "-Docr.dddd.base64.url=ddddocr服务base64图片识别地址" sms-bomb.jar 21 | ``` 22 | 3. 下载安装包安装 23 | 24 | [Release](https://github.com/leijinjun/sms-bomb/releases) 页面下载最新jar包。 25 | 执行命令 26 | ``` 27 | Linux、MacOS平台下执行 28 | java -jar -Dspring.profiles.active=dev -DDB_FILE_PATH=sqlite3DB文件路径 -Docr.dddd.url=ddddocr服务图片识别地址 -Docr.dddd.base64.url=ddddocr服务base64图片识别地址 sms-bomb.jar 29 | Windows平台下执行 30 | java -jar "-Dspring.profiles.active=dev" "-DDB_FILE_PATH=sqlite3DB文件路径" "-Docr.dddd.url=ddddocr服务图片识别地址" "-Docr.dddd.base64.url=ddddocr服务base64图片识别地址" sms-bomb.jar 31 | ``` 32 | 其中sqlite3DB文件在[sqllite3DB样例文件](https://github.com/leijinjun/sms-bomb/blob/develop/src/main/resources/db/sms_bomb.db) ,该文件为样例文件,仅用于测试。 33 | 4. docker安装 34 | 在路径/opt/smsBomb下添加db文件。 35 | ``` 36 | git clone https://github.com/leijinjun/sms-bomb.git 37 | cd sms-bomb 38 | chmod +x deploy.sh 39 | sh deploy.sh 40 | docker run --name sms-bomb \ 41 | -p 8080:8080 \ 42 | -v /opt/smsBomb:/opt/smsBomb \ 43 | -e DDDD_OCR_URL="ddddocr服务图片识别地址" \ 44 | -e DDDD_OCR_BASE64_URL="ddddocr服务BASE64图片识别地址" \ 45 | -e SQLITE3DB="sqlite3DB文件名" \ 46 | -d sms-bomb:[deploy.sh脚本中指定的version] 47 | ``` 48 | 5. 安装ocr服务 49 | 50 | [ddddocr](https://github.com/sml2h3/ocr_api_server),使用此服务将极大提高发送成功概率。 51 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:11-jdk-jammy 2 | WORKDIR /app 3 | COPY target/sms-bomb*.jar sms-bomb.jar 4 | #ENV CONFIG_LOCATION_FILE=application-prod.properties 5 | ENV SQLITE3DB=sms_bomb.db 6 | ENV JVM_XMS=256m 7 | ENV JVM_XMX=512m 8 | ENV DDDD_OCR_URL=http://192.168.100.1:9898/ocr/file 9 | ENV DDDD_OCR_BASE64_URL=http://192.168.100.1:9898/ocr/b64/text 10 | VOLUME /opt/smsbomb 11 | USER root 12 | EXPOSE 8080 13 | ENTRYPOINT java -jar -Xms${JVM_XMS} -Xmx${JVM_XMX} -Dspring.profiles.active=dev \ 14 | -DDB_FILE_PATH=/opt/smsbomb/${SQLITE3DB} -Docr.dddd.url=${DDDD_OCR_URL} -Docr.dddd.base64.url=${DDDD_OCR_BASE64_URL} sms-bomb.jar -------------------------------------------------------------------------------- /docker/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | SET version=latest 3 | SET current_dir=%CD% 4 | echo Current dir:%current_dir% 5 | cd .. 6 | @call mvn clean install -Dmaven.test.skip=true 7 | echo install finished 8 | cd /d %current_dir% 9 | rd /s /q target 10 | echo .original > EXCLUDE.txt 11 | mkdir target 12 | xcopy ..\target\*.jar target /exclude:EXCLUDE.txt /i 13 | del EXCLUDE.txt 14 | ren target\sms-bomb*.jar sms-bomb.jar 15 | docker image rm sms-bomb:%version% 16 | docker build -f ./Dockerfile -t sms-bomb:%version% ./target 17 | rd /s /q target -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | version="latest" 3 | current_dir=$(pwd) 4 | cd .. 5 | mvn clean install -Dmaven.test.skip=true 6 | echo "install finished" 7 | cd "$current_dir" 8 | rm -rf target 9 | mkdir -p target 10 | mv target/*.jar target/ 11 | docker image rm sms-bomb:$version 12 | docker build -f ./Dockerfile -t sms-bomb:$version ./target 13 | rm -rf target -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sms-server: 3 | image: sms-bomb:latest 4 | ports: 5 | - "8080:8080" 6 | environment: 7 | DDDD_OCR_URL: 'http://192.168.100.1:9898/ocr/file' 8 | DDDD_OCR_BASE64_URL: 'http://192.168.100.1:9898/ocr/b64/text' 9 | SQLITE3DB: 'sms_bomb.db' 10 | volumes: 11 | - D:\opt\smsBomb:/opt/smsBomb 12 | depends_on: 13 | - ocr-server 14 | ocr-server: 15 | image: ocr-server:v1 16 | ports: 17 | - "9898:9898" -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 2.4.0 11 | 12 | 13 | com.lei2j 14 | sms-bomb 15 | 1.0-SNAPSHOT 16 | jar 17 | 18 | sms-bomb project 19 | 20 | 21 | UTF-8 22 | 1.8 23 | 1.8 24 | 5.1.46 25 | 2.0.1.Final 26 | 4.5.13 27 | 3.0.7 28 | 3.11 29 | 1.15 30 | 1.2.75 31 | 3.0.4.RELEASE 32 | 1.15.4 33 | 1.73 34 | 35 | 36 | 37 | 38 | junit 39 | junit 40 | 4.12 41 | test 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-web 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-thymeleaf 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-jdbc 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-data-jpa 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-autoconfigure 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-configuration-processor 66 | 67 | 68 | mysql 69 | mysql-connector-java 70 | ${mysql.version} 71 | 72 | 73 | javax.validation 74 | validation-api 75 | ${validation.version} 76 | 77 | 78 | org.apache.httpcomponents 79 | httpclient 80 | ${httpClient.version} 81 | 82 | 83 | org.apache.httpcomponents 84 | httpmime 85 | ${httpClient.version} 86 | 87 | 88 | com.alibaba 89 | fastjson 90 | ${fastjson.version} 91 | 92 | 93 | org.codehaus.groovy 94 | groovy 95 | ${groovy.version} 96 | 97 | 98 | org.apache.commons 99 | commons-lang3 100 | ${apache.version} 101 | 102 | 103 | commons-codec 104 | commons-codec 105 | ${common.codec.version} 106 | 107 | 108 | org.codehaus.groovy 109 | groovy-json 110 | ${groovy.version} 111 | 112 | 113 | org.codehaus.groovy 114 | groovy-xml 115 | ${groovy.version} 116 | 117 | 118 | org.codehaus.groovy 119 | groovy-templates 120 | ${groovy.version} 121 | 122 | 123 | org.springframework.boot 124 | spring-boot-devtools 125 | true 126 | 127 | 128 | org.thymeleaf.extras 129 | thymeleaf-extras-java8time 130 | ${thymeleaf.extras.version} 131 | 132 | 133 | org.springframework.boot 134 | spring-boot-starter-test 135 | test 136 | 137 | 138 | org.jsoup 139 | jsoup 140 | ${jsoup.version} 141 | 142 | 143 | org.bouncycastle 144 | bcprov-jdk18on 145 | ${bouncycastle.version} 146 | 147 | 148 | com.github.binarywang 149 | java-testdata-generator 150 | 1.1.2 151 | 152 | 153 | com.lei2j 154 | id-extend 155 | 0.0.1 156 | 157 | 158 | com.lei2j 159 | jwt-lei2j 160 | 0.9.1 161 | 162 | 163 | org.xerial 164 | sqlite-jdbc 165 | 3.43.0.0 166 | 167 | 168 | com.github.gwenn 169 | sqlite-dialect 170 | 0.1.0 171 | 172 | 173 | 174 | 175 | release 176 | https://nexus.lei2j.com:8000/repository/maven-releases/ 177 | 178 | 179 | snapshots 180 | true 181 | https://nexus.lei2j.com:8000/repository/maven-snapshots/ 182 | 183 | 184 | 185 | 186 | sms-bomb 187 | 188 | 189 | org.codehaus.mojo 190 | versions-maven-plugin 191 | 2.16.2 192 | 193 | 194 | org.springframework.boot 195 | spring-boot-maven-plugin 196 | 197 | true 198 | 199 | 200 | 201 | maven-compiler-plugin 202 | 3.8.0 203 | 204 | 205 | maven-install-plugin 206 | 2.5.2 207 | 208 | 209 | maven-deploy-plugin 210 | 2.8.2 211 | 212 | 213 | 214 | 215 | -------------------------------------------------------------------------------- /sql/sms_bomb.sqlite3.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE s_sms_send_log ( 2 | id INTEGER, 3 | request_id TEXT, 4 | phone TEXT, 5 | ip TEXT, 6 | sms_url TEXT, 7 | web_site_name TEXT, 8 | params TEXT, 9 | response TEXT, 10 | create_at NUMERIC, 11 | request_duration INTEGER, 12 | response_status TEXT, 13 | CONSTRAINT s_sms_send_log_PK PRIMARY KEY (id) 14 | ); 15 | 16 | -- s_sms_url definition 17 | 18 | CREATE TABLE s_sms_url ( 19 | id INTEGER, 20 | icon TEXT, 21 | website_name TEXT, 22 | website TEXT, 23 | sms_url TEXT, 24 | phone_param_name TEXT, 25 | binding_params TEXT, 26 | create_at NUMERIC, 27 | update_at NUMERIC, 28 | is_normal INTEGER, 29 | business_name TEXT, 30 | success_code TEXT, 31 | end_code TEXT, 32 | open_script INTEGER, 33 | script_name TEXT, 34 | script_content TEXT, 35 | script_path TEXT, 36 | request_method TEXT, 37 | content_type TEXT, 38 | headers TEXT, 39 | response_type TEXT, 40 | last_used_time NUMERIC, 41 | max_retry_times INTEGER, 42 | CONSTRAINT s_sms_url_PK PRIMARY KEY (id) 43 | ); -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/SmsBombApplication.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb; 2 | 3 | import com.lei2j.sms.bomb.entity.EntityBasePackage; 4 | import com.lei2j.sms.bomb.repository.CommonJpaRepository; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.autoconfigure.domain.EntityScan; 8 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 9 | 10 | /** 11 | * @author leijinjun 12 | * @date 2020/11/25 13 | **/ 14 | @SpringBootApplication 15 | @EntityScan(basePackageClasses = {EntityBasePackage.class}) 16 | @EnableJpaRepositories(basePackageClasses = {CommonJpaRepository.class}) 17 | public class SmsBombApplication { 18 | 19 | public static void main(String[] args) { 20 | SpringApplication.run(SmsBombApplication.class, args); 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/base/entity/Pager.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.base.entity; 2 | 3 | import org.springframework.data.domain.Page; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | /** 9 | * @author leijinjun 10 | * @date 2021/1/17 11 | **/ 12 | public class Pager { 13 | 14 | public static final Integer MAX_SIZE = 5000; 15 | 16 | private final Integer pageNo; 17 | 18 | private final Integer pageSize; 19 | 20 | private List list; 21 | 22 | private Integer totalCount; 23 | 24 | private Integer totalPages; 25 | 26 | protected Pager(Integer pageNo, Integer pageSize) { 27 | this.pageNo = (pageNo == null || pageNo <= 0) ? 1 : pageNo; 28 | this.pageSize = pageSize == null ? 20 : (pageSize > MAX_SIZE ? MAX_SIZE : pageSize); 29 | } 30 | 31 | protected Pager(Integer pageNo, Integer pageSize, List list, Integer totalCount, Integer totalPages) { 32 | this.pageNo = pageNo; 33 | this.pageSize = pageSize; 34 | this.list = list == null ? Collections.emptyList() : list; 35 | this.totalCount = totalCount; 36 | this.totalPages = totalPages; 37 | } 38 | 39 | public static Pager of(Integer pageNo, Integer pageSize) { 40 | return new Pager<>(pageNo, pageSize); 41 | } 42 | 43 | public static Pager of(Integer pageNo, Integer pageSize, Integer totalCount, Integer totalPages, List content) { 44 | return new Pager<>(pageNo, pageSize, content, totalCount, totalPages); 45 | } 46 | 47 | public static Pager convert(Page page) { 48 | return of(page.getNumber() + 1, page.getSize(), (int) page.getTotalElements(), page.getTotalPages(), page.getContent()); 49 | } 50 | 51 | public Integer getPageNo() { 52 | return pageNo; 53 | } 54 | 55 | public Integer getPageSize() { 56 | return pageSize; 57 | } 58 | 59 | public List getList() { 60 | return list; 61 | } 62 | 63 | public void setList(List list) { 64 | this.list = list; 65 | } 66 | 67 | public Integer getTotalCount() { 68 | return totalCount; 69 | } 70 | 71 | public void setTotalCount(Integer totalCount) { 72 | this.totalCount = totalCount; 73 | } 74 | 75 | public Integer getTotalPages() { 76 | return totalPages; 77 | } 78 | 79 | public void setTotalPages(Integer totalPages) { 80 | this.totalPages = totalPages; 81 | } 82 | 83 | public Integer getOffset() { 84 | return (pageNo - 1) * pageSize; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/dao/BaseDao.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.dao; 2 | 3 | import org.springframework.jdbc.core.JdbcTemplate; 4 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 5 | 6 | /** 7 | * @author leijinjun 8 | * @date 2020/12/21 9 | **/ 10 | public class BaseDao { 11 | 12 | protected final JdbcTemplate jdbcTemplate; 13 | 14 | protected final NamedParameterJdbcTemplate parameterJdbcTemplate; 15 | 16 | public BaseDao(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate parameterJdbcTemplate) { 17 | this.jdbcTemplate = jdbcTemplate; 18 | this.parameterJdbcTemplate = parameterJdbcTemplate; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/dto/SmsSendDTO.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.dto; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * @author leijinjun 7 | * @date 2020/12/21 8 | **/ 9 | public class SmsSendDTO { 10 | 11 | /** 12 | * 手机号 13 | */ 14 | private String phone; 15 | 16 | /** 17 | * 客户端ip 18 | */ 19 | private String clientIp; 20 | 21 | /** 22 | * 请求id 23 | */ 24 | private String requestId; 25 | 26 | /** 27 | * 发送条数 28 | */ 29 | private Integer sendItems; 30 | 31 | public String getPhone() { 32 | return phone; 33 | } 34 | 35 | public void setPhone(String phone) { 36 | this.phone = phone; 37 | } 38 | 39 | public String getClientIp() { 40 | return clientIp; 41 | } 42 | 43 | public void setClientIp(String clientIp) { 44 | this.clientIp = clientIp; 45 | } 46 | 47 | public String getRequestId() { 48 | return requestId; 49 | } 50 | 51 | public void setRequestId(String requestId) { 52 | this.requestId = requestId; 53 | } 54 | 55 | public Integer getSendItems() { 56 | return sendItems; 57 | } 58 | 59 | public void setSendItems(Integer sendItems) { 60 | this.sendItems = sendItems; 61 | } 62 | 63 | @Override 64 | public boolean equals(Object o) { 65 | if (this == o) return true; 66 | if (o == null || getClass() != o.getClass()) return false; 67 | SmsSendDTO that = (SmsSendDTO) o; 68 | return Objects.equals(requestId, that.requestId); 69 | } 70 | 71 | @Override 72 | public int hashCode() { 73 | return Objects.hash(requestId); 74 | } 75 | 76 | @Override 77 | public String toString() { 78 | return "SmsSendDTO{" + 79 | "phone='" + phone + '\'' + 80 | ", clientIp='" + clientIp + '\'' + 81 | ", requestId='" + requestId + '\'' + 82 | '}'; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/entity/EntityBasePackage.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.entity; 2 | 3 | /** 4 | * @author leijinjun 5 | * @version V1.0 6 | * @date 2021/1/4 7 | **/ 8 | public class EntityBasePackage { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/entity/SmsSendLog.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.entity; 2 | 3 | import javax.persistence.*; 4 | import java.time.LocalDateTime; 5 | 6 | /** 7 | * @author leijinjun 8 | * @date 2020/12/21 9 | **/ 10 | @Entity 11 | @Table(name = "s_sms_send_log") 12 | public class SmsSendLog { 13 | 14 | public static final String SUCCESS_STATUS = "SUCCESS"; 15 | public static final String FAILURE_STATUS = "FAILURE"; 16 | 17 | @Id 18 | private Long id; 19 | 20 | private String phone; 21 | 22 | private String ip; 23 | 24 | @Column(name = "sms_url") 25 | private String smsUrl; 26 | 27 | @Column(name = "web_site_name") 28 | private String webSiteName; 29 | 30 | private String params; 31 | 32 | private String response; 33 | 34 | @Column(name = "create_at") 35 | private LocalDateTime createAt; 36 | 37 | @Column(name = "request_duration") 38 | private Integer requestDuration; 39 | 40 | /** 41 | * @see #SUCCESS_STATUS 42 | * @see #FAILURE_STATUS 43 | */ 44 | @Column(name = "response_status") 45 | private String responseStatus; 46 | 47 | private String requestId; 48 | 49 | public Long getId() { 50 | return id; 51 | } 52 | 53 | public void setId(Long id) { 54 | this.id = id; 55 | } 56 | 57 | public String getPhone() { 58 | return phone; 59 | } 60 | 61 | public void setPhone(String phone) { 62 | this.phone = phone; 63 | } 64 | 65 | public String getIp() { 66 | return ip; 67 | } 68 | 69 | public void setIp(String ip) { 70 | this.ip = ip; 71 | } 72 | 73 | public String getSmsUrl() { 74 | return smsUrl; 75 | } 76 | 77 | public void setSmsUrl(String smsUrl) { 78 | this.smsUrl = smsUrl; 79 | } 80 | 81 | public String getParams() { 82 | return params; 83 | } 84 | 85 | public void setParams(String params) { 86 | this.params = params; 87 | } 88 | 89 | public String getResponse() { 90 | return response; 91 | } 92 | 93 | public void setResponse(String response) { 94 | this.response = response; 95 | } 96 | 97 | public LocalDateTime getCreateAt() { 98 | return createAt; 99 | } 100 | 101 | public void setCreateAt(LocalDateTime createAt) { 102 | this.createAt = createAt; 103 | } 104 | 105 | public Integer getRequestDuration() { 106 | return requestDuration; 107 | } 108 | 109 | public void setRequestDuration(Integer requestDuration) { 110 | this.requestDuration = requestDuration; 111 | } 112 | 113 | public String getResponseStatus() { 114 | return responseStatus; 115 | } 116 | 117 | public void setResponseStatus(String responseStatus) { 118 | this.responseStatus = responseStatus; 119 | } 120 | 121 | public String getRequestId() { 122 | return requestId; 123 | } 124 | 125 | public void setRequestId(String requestId) { 126 | this.requestId = requestId; 127 | } 128 | 129 | public String getWebSiteName() { 130 | return webSiteName; 131 | } 132 | 133 | public void setWebSiteName(String webSiteName) { 134 | this.webSiteName = webSiteName; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/entity/SmsUrlConfig.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.entity; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.TypeReference; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.hibernate.annotations.CreationTimestamp; 7 | 8 | import javax.persistence.*; 9 | import java.time.LocalDateTime; 10 | import java.util.*; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | 14 | /** 15 | * @author leijinjun 16 | * @date 2020/11/29 17 | **/ 18 | @Table(name = "s_sms_url") 19 | @Entity 20 | public class SmsUrlConfig { 21 | 22 | public static final Pattern HEADER_COMPILE = Pattern.compile("\\[.+?]"); 23 | 24 | /** 25 | * 主键 26 | */ 27 | @Id 28 | private Long id; 29 | 30 | /** 31 | * 网站名称 32 | */ 33 | @Column(name = "website_name") 34 | private String websiteName; 35 | 36 | /** 37 | * 网站图标 38 | */ 39 | @Column(name = "icon") 40 | private String icon; 41 | 42 | /** 43 | * 网站地址 44 | */ 45 | @Column(name = "website") 46 | private String website; 47 | 48 | /** 49 | * 发送短信验证码URL 50 | */ 51 | @Column(name = "sms_url",nullable = false) 52 | private String smsUrl; 53 | 54 | /** 55 | * 手机号参数名称 56 | */ 57 | @Column(name = "phone_param_name",nullable = false) 58 | private String phoneParamName; 59 | 60 | /** 61 | * body绑定固定参数 62 | */ 63 | @Column(name = "binding_params") 64 | private String bindingParams; 65 | 66 | /** 67 | * 绑定固定查询参数 68 | */ 69 | @Column(name = "binding_query_params") 70 | private String bindingQueryParams; 71 | 72 | /** 73 | * 创建时间 74 | */ 75 | @Column(name = "create_at") 76 | private LocalDateTime createAt; 77 | 78 | /** 79 | * 最后更新时间 80 | */ 81 | @Column(name = "update_at") 82 | @CreationTimestamp 83 | private LocalDateTime updateAt; 84 | 85 | /** 86 | * 是否启用,{@code true}表示启用 87 | */ 88 | @Column(name = "is_normal") 89 | private Boolean normal; 90 | 91 | /** 92 | * 短信验证码的业务场景名称 93 | */ 94 | @Column(name = "business_name") 95 | private String businessName; 96 | 97 | /** 98 | * 发送验证码响应成功值 99 | */ 100 | @Column(name = "success_code") 101 | private String successCode; 102 | 103 | /** 104 | * 发送验证码重试 105 | */ 106 | @Column(name = "end_code") 107 | private String endCode; 108 | 109 | /** 110 | * 脚本名称 111 | */ 112 | @Column(name = "script_name") 113 | private String scriptName; 114 | 115 | /** 116 | * 脚本内容 117 | */ 118 | @Column(name = "script_content") 119 | private String scriptContent; 120 | 121 | /** 122 | * 脚本路径 123 | */ 124 | @Column(name = "script_path") 125 | private String scriptPath; 126 | 127 | /** 128 | * 请求方法,例如:GET、POST 129 | */ 130 | @Column(name = "request_method") 131 | private String requestMethod; 132 | 133 | /** 134 | * 请求类型,例如:application/json 135 | */ 136 | @Column(name = "content_type") 137 | private String contentType; 138 | 139 | /** 140 | * 固定请求头 141 | */ 142 | @Column(name = "headers") 143 | private String headers; 144 | 145 | /** 146 | * 是否开始脚本,{@code true}表示开启脚本 147 | */ 148 | @Column(name = "open_script") 149 | private Boolean openScript; 150 | 151 | /** 152 | * 响应类型,例如:application/json 153 | */ 154 | private String responseType; 155 | 156 | /** 157 | * 最后使用时间 158 | */ 159 | @Column(name = "last_used_time") 160 | private LocalDateTime lastUsedTime; 161 | 162 | /** 163 | * 发送失败时,最大尝试次数 164 | */ 165 | @Column(name = "max_retry_times") 166 | private Integer maxRetryTimes; 167 | 168 | public SmsUrlConfig() { 169 | } 170 | 171 | public SmsUrlConfig(Long id, String websiteName, String icon, String website, String smsUrl, String phoneParamName, 172 | String bindingParams,String bindingQueryParams, LocalDateTime createAt, LocalDateTime updateAt, Boolean normal, String businessName, String successCode, 173 | String endCode, String scriptName, String scriptContent, String scriptPath, String requestMethod, String contentType, String headers, 174 | Boolean openScript, String responseType, LocalDateTime lastUsedTime, Integer maxRetryTimes) { 175 | this.id = id; 176 | this.websiteName = websiteName; 177 | this.icon = icon; 178 | this.website = website; 179 | this.smsUrl = smsUrl; 180 | this.phoneParamName = phoneParamName; 181 | this.bindingParams = bindingParams; 182 | this.bindingQueryParams = bindingQueryParams; 183 | this.createAt = createAt; 184 | this.updateAt = updateAt; 185 | this.normal = normal; 186 | this.businessName = businessName; 187 | this.successCode = successCode; 188 | this.endCode = endCode; 189 | this.scriptName = scriptName; 190 | this.scriptContent = scriptContent; 191 | this.scriptPath = scriptPath; 192 | this.requestMethod = requestMethod; 193 | this.contentType = contentType; 194 | this.headers = headers; 195 | this.openScript = openScript; 196 | this.responseType = responseType; 197 | this.lastUsedTime = lastUsedTime; 198 | this.maxRetryTimes = maxRetryTimes; 199 | } 200 | 201 | public List getHeaderList(){ 202 | List headerList = new ArrayList<>(); 203 | //解析固定请求头 204 | if (StringUtils.isNotBlank(headers)) { 205 | Matcher matcher = SmsUrlConfig.HEADER_COMPILE.matcher(headers); 206 | while (matcher.find()) { 207 | String group = matcher.group(); 208 | String headerPair = group.substring(1, group.length() - 1); 209 | headerList.add(headerPair); 210 | } 211 | } 212 | return headerList; 213 | } 214 | 215 | public Map getBindingParamsMap() { 216 | if (StringUtils.isNotBlank(bindingParams)) { 217 | return JSON.parseObject(bindingParams, new TypeReference>() { 218 | }); 219 | } 220 | return Collections.emptyMap(); 221 | } 222 | 223 | public Map getBindingQueryParamsMap() { 224 | if (StringUtils.isNotBlank(bindingQueryParams)) { 225 | return JSON.parseObject(bindingQueryParams, new TypeReference>() { 226 | }); 227 | } 228 | return Collections.emptyMap(); 229 | } 230 | 231 | public Long getId() { 232 | return id; 233 | } 234 | 235 | public void setId(Long id) { 236 | this.id = id; 237 | } 238 | 239 | public String getSmsUrl() { 240 | return smsUrl; 241 | } 242 | 243 | public void setSmsUrl(String smsUrl) { 244 | this.smsUrl = smsUrl; 245 | } 246 | 247 | public String getPhoneParamName() { 248 | return phoneParamName; 249 | } 250 | 251 | public void setPhoneParamName(String phoneParamName) { 252 | this.phoneParamName = phoneParamName; 253 | } 254 | 255 | public LocalDateTime getCreateAt() { 256 | return createAt; 257 | } 258 | 259 | public void setCreateAt(LocalDateTime createAt) { 260 | this.createAt = createAt; 261 | } 262 | 263 | public LocalDateTime getUpdateAt() { 264 | return updateAt; 265 | } 266 | 267 | public void setUpdateAt(LocalDateTime updateAt) { 268 | this.updateAt = updateAt; 269 | } 270 | 271 | public Boolean getNormal() { 272 | return normal; 273 | } 274 | 275 | public void setNormal(Boolean normal) { 276 | this.normal = normal; 277 | } 278 | 279 | public String getBusinessName() { 280 | return businessName; 281 | } 282 | 283 | public void setBusinessName(String businessName) { 284 | this.businessName = businessName; 285 | } 286 | 287 | public String getSuccessCode() { 288 | return successCode; 289 | } 290 | 291 | public void setSuccessCode(String successCode) { 292 | this.successCode = successCode; 293 | } 294 | 295 | public String getScriptName() { 296 | return scriptName; 297 | } 298 | 299 | public void setScriptName(String scriptName) { 300 | this.scriptName = scriptName; 301 | } 302 | 303 | public String getScriptContent() { 304 | return scriptContent; 305 | } 306 | 307 | public void setScriptContent(String scriptContent) { 308 | this.scriptContent = scriptContent; 309 | } 310 | 311 | public String getScriptPath() { 312 | return scriptPath; 313 | } 314 | 315 | public void setScriptPath(String scriptPath) { 316 | this.scriptPath = scriptPath; 317 | } 318 | 319 | public String getRequestMethod() { 320 | return requestMethod; 321 | } 322 | 323 | public void setRequestMethod(String requestMethod) { 324 | this.requestMethod = requestMethod; 325 | } 326 | 327 | public String getContentType() { 328 | return contentType; 329 | } 330 | 331 | public void setContentType(String contentType) { 332 | this.contentType = contentType; 333 | } 334 | 335 | public String getHeaders() { 336 | return headers; 337 | } 338 | 339 | public void setHeaders(String headers) { 340 | this.headers = headers; 341 | } 342 | 343 | public String getBindingParams() { 344 | return bindingParams; 345 | } 346 | 347 | public void setBindingParams(String bindingParams) { 348 | this.bindingParams = bindingParams; 349 | } 350 | 351 | public Boolean getOpenScript() { 352 | return openScript; 353 | } 354 | 355 | public void setOpenScript(Boolean openScript) { 356 | this.openScript = openScript; 357 | } 358 | 359 | public LocalDateTime getLastUsedTime() { 360 | return lastUsedTime; 361 | } 362 | 363 | public void setLastUsedTime(LocalDateTime lastUsedTime) { 364 | this.lastUsedTime = lastUsedTime; 365 | } 366 | 367 | public String getResponseType() { 368 | return responseType; 369 | } 370 | 371 | public void setResponseType(String responseType) { 372 | this.responseType = responseType; 373 | } 374 | 375 | public String getWebsiteName() { 376 | return websiteName; 377 | } 378 | 379 | public void setWebsiteName(String websiteName) { 380 | this.websiteName = websiteName; 381 | } 382 | 383 | public String getIcon() { 384 | return icon; 385 | } 386 | 387 | public void setIcon(String icon) { 388 | this.icon = icon; 389 | } 390 | 391 | public String getWebsite() { 392 | return website; 393 | } 394 | 395 | public void setWebsite(String website) { 396 | this.website = website; 397 | } 398 | 399 | public String getEndCode() { 400 | return endCode; 401 | } 402 | 403 | public void setEndCode(String endCode) { 404 | this.endCode = endCode; 405 | } 406 | 407 | public Integer getMaxRetryTimes() { 408 | return maxRetryTimes; 409 | } 410 | 411 | public void setMaxRetryTimes(Integer maxRetryTimes) { 412 | this.maxRetryTimes = maxRetryTimes; 413 | } 414 | 415 | public String getBindingQueryParams() { 416 | return bindingQueryParams; 417 | } 418 | 419 | @Override 420 | public String toString() { 421 | return "SmsUrlConfig{" + 422 | "id=" + id + 423 | ", websiteName='" + websiteName + '\'' + 424 | ", icon='" + icon + '\'' + 425 | ", website='" + website + '\'' + 426 | ", smsUrl='" + smsUrl + '\'' + 427 | ", phoneParamName='" + phoneParamName + '\'' + 428 | ", bindingParams='" + bindingParams + '\'' + 429 | ", bindingQueryParams='" + bindingQueryParams + '\'' + 430 | ", createAt=" + createAt + 431 | ", updateAt=" + updateAt + 432 | ", normal=" + normal + 433 | ", businessName='" + businessName + '\'' + 434 | ", successCode='" + successCode + '\'' + 435 | ", endCode='" + endCode + '\'' + 436 | ", scriptName='" + scriptName + '\'' + 437 | ", scriptContent='" + scriptContent + '\'' + 438 | ", scriptPath='" + scriptPath + '\'' + 439 | ", requestMethod='" + requestMethod + '\'' + 440 | ", contentType='" + contentType + '\'' + 441 | ", headers='" + headers + '\'' + 442 | ", openScript=" + openScript + 443 | ", responseType='" + responseType + '\'' + 444 | ", lastUsedTime=" + lastUsedTime + 445 | ", maxRetryTimes=" + maxRetryTimes + 446 | '}'; 447 | } 448 | 449 | public void setBindingQueryParams(String bindingQueryParams) { 450 | 451 | this.bindingQueryParams = bindingQueryParams; 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/entity/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * author: leijinjun 3 | * date: 2021/1/4 4 | */ 5 | package com.lei2j.sms.bomb.entity; -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/ocr/DdddOcrServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.ocr; 2 | 3 | import com.lei2j.sms.bomb.util.HttpUtils; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.io.IOException; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | /** 13 | * @author leijinjun 14 | * @date 2023/3/15 15 | **/ 16 | @Service(DdddOcrServiceImpl.PROVIDER_NAME) 17 | public class DdddOcrServiceImpl implements OcrService { 18 | 19 | static final String PROVIDER_NAME = "ddddOcr"; 20 | 21 | @Value("${ocr.dddd.url:}") 22 | private String url; 23 | 24 | @Value("${ocr.dddd.base64.url:}") 25 | private String base64Url; 26 | 27 | @Override 28 | public String ocr(OcrRequest ocrRequest) throws IOException { 29 | if (ocrRequest.getInputStream() != null) { 30 | if (StringUtils.isBlank(url)) { 31 | throw new RuntimeException("url is not configured"); 32 | } 33 | HttpUtils.MultiPart multiPart = 34 | new HttpUtils.MultiPart.Builder().setName("image").setFileName("image_" + System.currentTimeMillis()).setContentType("image/jpeg").setInputStream(ocrRequest.getInputStream()).build(); 35 | return HttpUtils.postWithFormData(url, multiPart); 36 | } else if (ocrRequest.getBase64() != null) { 37 | if (StringUtils.isBlank(base64Url)) { 38 | throw new RuntimeException("base64Url is not configured"); 39 | } 40 | return HttpUtils.postForUpload(base64Url, new ByteArrayInputStream(ocrRequest.getBase64().getBytes(StandardCharsets.US_ASCII))); 41 | } 42 | throw new IllegalArgumentException("parameter is empty"); 43 | } 44 | 45 | @Override 46 | public String getProviderName() { 47 | return PROVIDER_NAME; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/ocr/OcrRequest.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.ocr; 2 | 3 | import java.io.InputStream; 4 | 5 | /** 6 | * ocr请求参数 7 | * @author leijinjun 8 | * @date 2023/3/15 9 | **/ 10 | public class OcrRequest { 11 | 12 | private InputStream inputStream; 13 | 14 | private String base64; 15 | 16 | public InputStream getInputStream() { 17 | return inputStream; 18 | } 19 | 20 | public void setInputStream(InputStream inputStream) { 21 | this.inputStream = inputStream; 22 | } 23 | 24 | public String getBase64() { 25 | return base64; 26 | } 27 | 28 | public void setBase64(String base64) { 29 | this.base64 = base64; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/ocr/OcrService.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.ocr; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * 图片识别服务 7 | * @author leijinjun 8 | * @date 2023/3/15 9 | **/ 10 | public interface OcrService { 11 | 12 | /** 13 | * ocr识别服务 14 | * @param ocrRequest 15 | * @return 16 | */ 17 | String ocr(OcrRequest ocrRequest) throws IOException; 18 | 19 | /** 20 | * 获取服务提供商名称 21 | * @return 22 | */ 23 | String getProviderName(); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/repository/CommonJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.repository; 2 | 3 | import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation; 4 | import org.springframework.data.repository.NoRepositoryBean; 5 | 6 | /** 7 | * @author leijinjun 8 | * @date 2021/1/4 9 | **/ 10 | @NoRepositoryBean 11 | public interface CommonJpaRepository extends JpaRepositoryImplementation { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/repository/SmsSendLogRepository.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.repository; 2 | 3 | import com.lei2j.sms.bomb.entity.SmsSendLog; 4 | import org.springframework.data.jpa.repository.Query; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | 9 | /** 10 | * @author leijinjun 11 | * @version V1.0 12 | * @date 2021/1/4 13 | **/ 14 | public interface SmsSendLogRepository extends CommonJpaRepository { 15 | 16 | interface GroupStatus{ 17 | 18 | Integer getTotalCount(); 19 | 20 | String getResponseStatus(); 21 | } 22 | 23 | long countByPhoneEqualsAndCreateAtBetween(String phone, LocalDateTime before, LocalDateTime after); 24 | 25 | long countByIpEqualsAndCreateAtBetween(String ip, LocalDateTime before, LocalDateTime after); 26 | 27 | @Query(value ="select count(responseStatus) as totalCount,responseStatus as responseStatus from SmsSendLog where requestId =?1 group by responseStatus") 28 | List groupByResponseStatus(String requestId); 29 | 30 | long countByRequestIdEqualsAndResponseStatusEquals(String requestId, String responseStatus); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/repository/SmsUrlConfigRepository.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.repository; 2 | 3 | import com.lei2j.sms.bomb.entity.SmsUrlConfig; 4 | import org.springframework.data.jpa.repository.Modifying; 5 | import org.springframework.data.jpa.repository.Query; 6 | 7 | import javax.transaction.Transactional; 8 | import java.util.List; 9 | 10 | /** 11 | * @author leijinjun 12 | * @version V1.0 13 | * @date 2021/1/4 14 | **/ 15 | public interface SmsUrlConfigRepository extends CommonJpaRepository { 16 | 17 | /** 18 | * @param normal 19 | * @return 20 | */ 21 | List findByNormalEquals(Boolean normal); 22 | 23 | Integer countByNormalEquals(Boolean normal); 24 | 25 | @Query(nativeQuery = true,value = "select s.* from s_sms_url s where s.is_normal =?1 order by s.last_used_time limit ?2") 26 | List findTopListByNormalEquals(Boolean normal,Integer size); 27 | 28 | @Modifying 29 | @Transactional 30 | @Query(nativeQuery = true, value = "update s_sms_url set is_normal=?1") 31 | void updateAllStatus(Boolean normal); 32 | } -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/service/impl/CancelException.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.service.impl; 2 | 3 | /** 4 | * 该异常标志取消发送 5 | * @author leijinjun 6 | * @date 2023/5/13 7 | **/ 8 | public class CancelException extends RuntimeException { 9 | 10 | public CancelException(String message) { 11 | super(message); 12 | } 13 | 14 | public CancelException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/service/impl/CommonServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.service.impl; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | /** 7 | * @author leijinjun 8 | * @date 2020/12/20 9 | **/ 10 | public class CommonServiceImpl { 11 | 12 | protected final Logger logger = LoggerFactory.getLogger(this.getClass()); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/service/impl/GroovySmsScriptExecutorService.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.service.impl; 2 | 3 | import com.lei2j.sms.bomb.entity.SmsUrlConfig; 4 | import com.lei2j.sms.bomb.util.SpringApplicationUtils; 5 | import groovy.lang.GroovyClassLoader; 6 | import groovy.lang.GroovyObject; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.core.io.ClassPathResource; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.util.DigestUtils; 14 | 15 | import java.io.*; 16 | import java.nio.charset.StandardCharsets; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.nio.file.Paths; 20 | import java.util.Map; 21 | import java.util.Objects; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | 24 | /** 25 | * @author leijinjun 26 | * @date 2020/12/17 27 | **/ 28 | @Configuration 29 | @Service 30 | public class GroovySmsScriptExecutorService { 31 | 32 | private static final String SC = "/com/lei2j/sms/bomb/script/SmsCommonScript.groovy"; 33 | 34 | private static final GroovyClassLoader LOADER = new GroovyClassLoader(); 35 | 36 | private static final Map SCRIPT_CACHE = new ConcurrentHashMap<>(64); 37 | 38 | private final GroovyObject defaultObject; 39 | 40 | @Autowired 41 | private SpringApplicationUtils springApplicationUtils; 42 | 43 | public GroovySmsScriptExecutorService() throws Exception { 44 | ClassPathResource classPathResource = new ClassPathResource(SC); 45 | InputStream inputStream = classPathResource.getInputStream(); 46 | Class defaultClazz = LOADER.parseClass(new InputStreamReader(inputStream), "SmsCommonScript.groovy"); 47 | defaultObject = (GroovyObject) defaultClazz.newInstance(); 48 | } 49 | 50 | @Value("${smb.bomb.script.base.path}") 51 | private String groovyScriptBasePath; 52 | 53 | private GroovyObject parse(SmsUrlConfig smsUrlConfig, String base) throws IOException, IllegalAccessException, InstantiationException { 54 | String content = smsUrlConfig.getScriptContent(); 55 | Boolean openScript = smsUrlConfig.getOpenScript(); 56 | Class cls = null; 57 | if (Boolean.TRUE.equals(openScript)) { 58 | if (StringUtils.isNotBlank(content)) { 59 | cls = parseContent(content, smsUrlConfig.getScriptName()); 60 | } else if (StringUtils.isNotBlank(smsUrlConfig.getScriptPath())) { 61 | String path = smsUrlConfig.getScriptPath(); 62 | Path filePath = Paths.get(base, path); 63 | BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8); 64 | cls = LOADER.parseClass(reader, path); 65 | } else { 66 | throw new IllegalArgumentException("script content or scriptPath is null"); 67 | } 68 | } 69 | if (cls == null) { 70 | return defaultObject; 71 | } 72 | return (GroovyObject) cls.newInstance(); 73 | } 74 | 75 | private Class parseContent(final String content, final String name) { 76 | final String checkDigit = DigestUtils.md5DigestAsHex(content.getBytes(StandardCharsets.UTF_8)); 77 | ScriptCache scriptCache; 78 | if (!SCRIPT_CACHE.containsKey(name) || !(scriptCache = SCRIPT_CACHE.get(name)).checkDigit(checkDigit)) { 79 | Class cls = LOADER.parseClass(content, name); 80 | scriptCache = new ScriptCache(cls, checkDigit); 81 | SCRIPT_CACHE.put(name, scriptCache); 82 | } 83 | return scriptCache.getCls(); 84 | } 85 | 86 | private Object invoke0(GroovyObject instance, String method, Object... args) { 87 | Objects.requireNonNull(instance); 88 | return instance.invokeMethod(method, args); 89 | } 90 | 91 | public void preInvoke(ScriptContext scriptContext) throws Exception { 92 | scriptContext.setSpringApplicationUtils(springApplicationUtils); 93 | try { 94 | ScriptThreadContext.set(scriptContext); 95 | invoke0(parse(scriptContext.getSmsUrlConfig(), groovyScriptBasePath), "preProcess", scriptContext); 96 | }finally { 97 | ScriptThreadContext.remove(); 98 | } 99 | } 100 | 101 | public Object postInvoke(ScriptContext scriptContext) throws Exception { 102 | scriptContext.setSpringApplicationUtils(springApplicationUtils); 103 | try { 104 | ScriptThreadContext.set(scriptContext); 105 | return invoke0(parse(scriptContext.getSmsUrlConfig(), groovyScriptBasePath), "postProcess", scriptContext); 106 | }finally { 107 | ScriptThreadContext.remove(); 108 | } 109 | } 110 | 111 | public Object retry(ScriptContext scriptContext) throws Exception { 112 | scriptContext.setSpringApplicationUtils(springApplicationUtils); 113 | try { 114 | ScriptThreadContext.set(scriptContext); 115 | return invoke0(parse(scriptContext.getSmsUrlConfig(), groovyScriptBasePath), "retry", scriptContext); 116 | }finally { 117 | ScriptThreadContext.remove(); 118 | } 119 | } 120 | 121 | private static class ScriptCache { 122 | private Class cls; 123 | 124 | private String checkDigit; 125 | 126 | public ScriptCache() { 127 | } 128 | 129 | public ScriptCache(Class cls, String checkDigit) { 130 | this.cls = cls; 131 | this.checkDigit = checkDigit; 132 | } 133 | 134 | public boolean checkDigit(String checkDigit) { 135 | return this.checkDigit.equals(checkDigit); 136 | } 137 | 138 | public Class getCls() { 139 | return cls; 140 | } 141 | 142 | public void setCls(Class cls) { 143 | this.cls = cls; 144 | } 145 | 146 | public String getCheckDigit() { 147 | return checkDigit; 148 | } 149 | 150 | public void setCheckDigit(String checkDigit) { 151 | this.checkDigit = checkDigit; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/service/impl/RetryInvokeException.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.service.impl; 2 | 3 | /** 4 | * 短信接口重试异常 5 | * @author leijinjun 6 | * @date 2023/4/24 7 | **/ 8 | public class RetryInvokeException extends RuntimeException { 9 | 10 | public RetryInvokeException(String message) { 11 | super(message); 12 | } 13 | 14 | public RetryInvokeException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/service/impl/ScriptContext.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.service.impl; 2 | 3 | import com.lei2j.sms.bomb.entity.SmsUrlConfig; 4 | import com.lei2j.sms.bomb.util.SpringApplicationUtils; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | import java.util.Optional; 10 | 11 | /** 12 | * @author leijinjun 13 | * @date 2023/1/9 14 | **/ 15 | public class ScriptContext { 16 | 17 | private SmsUrlConfig smsUrlConfig; 18 | 19 | private final Map paramsMap = new HashMap<>(); 20 | 21 | private final Map headerMap = new HashMap<>(); 22 | 23 | private final Map queryMap = new HashMap<>(); 24 | 25 | /** 26 | * 27 | */ 28 | private final Map contextDataMap = new HashMap<>(); 29 | 30 | private String response; 31 | 32 | private transient SpringApplicationUtils springApplicationUtils; 33 | 34 | /** 35 | * 上一次脚本上下文内容 36 | */ 37 | private transient ScriptContext preScriptContext; 38 | 39 | public SmsUrlConfig getSmsUrlConfig() { 40 | return smsUrlConfig; 41 | } 42 | 43 | public void setSmsUrlConfig(SmsUrlConfig smsUrlConfig) { 44 | this.smsUrlConfig = smsUrlConfig; 45 | } 46 | 47 | public Map getParamsMap() { 48 | return paramsMap; 49 | } 50 | 51 | public Map getHeaderMap() { 52 | return headerMap; 53 | } 54 | 55 | public Map getQueryMap() { 56 | return queryMap; 57 | } 58 | 59 | public Map getContextDataMap() { 60 | return contextDataMap; 61 | } 62 | 63 | public String getResponse() { 64 | return response; 65 | } 66 | 67 | public void setResponse(String response) { 68 | this.response = response; 69 | } 70 | 71 | /** 72 | * @return 手机号参数名称 73 | */ 74 | public final String getPhoneParamName(){ 75 | return Optional.ofNullable(smsUrlConfig).map(SmsUrlConfig::getPhoneParamName).orElse(null); 76 | } 77 | 78 | /** 79 | * @return 手机号 80 | */ 81 | public final String getPhoneValue(){ 82 | return paramsMap.get(getPhoneParamName()).toString(); 83 | } 84 | 85 | public void cloneFromPre() { 86 | if (Objects.nonNull(preScriptContext)) { 87 | return; 88 | } 89 | getParamsMap().putAll(preScriptContext.getParamsMap()); 90 | getQueryMap().putAll(preScriptContext.getQueryMap()); 91 | getHeaderMap().putAll(preScriptContext.getHeaderMap()); 92 | getContextDataMap().putAll(preScriptContext.getContextDataMap()); 93 | } 94 | 95 | public SpringApplicationUtils getSpringApplicationUtils() { 96 | return springApplicationUtils; 97 | } 98 | 99 | public void setSpringApplicationUtils(SpringApplicationUtils springApplicationUtils) { 100 | this.springApplicationUtils = springApplicationUtils; 101 | } 102 | 103 | public ScriptContext getPreScriptContext() { 104 | return preScriptContext; 105 | } 106 | 107 | public void setPreScriptContext(ScriptContext preScriptContext) { 108 | this.preScriptContext = preScriptContext; 109 | } 110 | 111 | public Object setParamsEntry(String key, Object value) { 112 | return getParamsMap().put(key, value); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/service/impl/ScriptThreadContext.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.service.impl; 2 | 3 | /** 4 | * @author leijinjun 5 | * @date 2023/3/15 6 | **/ 7 | public class ScriptThreadContext { 8 | 9 | private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>(); 10 | 11 | public static void set(ScriptContext scriptContext){ 12 | THREAD_LOCAL.set(scriptContext); 13 | } 14 | 15 | public static ScriptContext get(){ 16 | return THREAD_LOCAL.get(); 17 | } 18 | 19 | public static void remove(){ 20 | THREAD_LOCAL.remove(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/service/impl/SmsSendService.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.service.impl; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.lei2j.idgen.core.IdGenerator; 6 | import com.lei2j.sms.bomb.repository.SmsSendLogRepository; 7 | import com.lei2j.sms.bomb.repository.SmsUrlConfigRepository; 8 | import com.lei2j.sms.bomb.util.HttpUtils; 9 | import com.lei2j.sms.bomb.dto.SmsSendDTO; 10 | import com.lei2j.sms.bomb.entity.SmsUrlConfig; 11 | import com.lei2j.sms.bomb.entity.SmsSendLog; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.apache.http.client.HttpResponseException; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.data.domain.Example; 16 | import org.springframework.data.domain.ExampleMatcher; 17 | import org.springframework.http.HttpMethod; 18 | import org.springframework.stereotype.Service; 19 | import org.springframework.util.CollectionUtils; 20 | 21 | import java.io.IOException; 22 | import java.nio.charset.StandardCharsets; 23 | import java.time.LocalDateTime; 24 | import java.util.*; 25 | import java.util.concurrent.*; 26 | import java.util.concurrent.locks.LockSupport; 27 | 28 | /** 29 | * @author leijinjun 30 | * @date 2020/11/29 31 | **/ 32 | @Service 33 | public class SmsSendService extends CommonServiceImpl { 34 | public static final Map FIXED_HEADER = new LinkedHashMap<>(); 35 | 36 | static{ 37 | FIXED_HEADER.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, " + "like Gecko) Chrome/87.0.4280.67 Safari/537.36 Edg/87.0.664.47"); 38 | } 39 | 40 | private final int coreSize = Runtime.getRuntime().availableProcessors() + 1; 41 | 42 | private final ThreadPoolExecutor executorService = 43 | new ThreadPoolExecutor(coreSize, 2 * coreSize + 1, 15, TimeUnit.MINUTES, new ArrayBlockingQueue<>(2000), new ThreadPoolExecutor.DiscardOldestPolicy()); 44 | 45 | private final ThreadPoolExecutor logExecutorService = 46 | new ThreadPoolExecutor(2, 5, 15, TimeUnit.MINUTES, new ArrayBlockingQueue<>(2000), new ThreadPoolExecutor.DiscardOldestPolicy()); 47 | 48 | private final ScheduledExecutorService checkSendSmsExecutor = Executors.newSingleThreadScheduledExecutor(); 49 | 50 | private final List> futureList = new ArrayList<>(); 51 | 52 | private final GroovySmsScriptExecutorService groovyScriptExecutorService; 53 | 54 | private final SmsSendLogRepository smsSendLogRepository; 55 | 56 | private final SmsUrlConfigService smsUrlConfigService; 57 | 58 | private final IdGenerator idGenerator; 59 | 60 | /** 61 | * 最大发送短信条数 62 | */ 63 | @Value("${smb.bomb.send.window.size:3}") 64 | private Integer sendSize; 65 | 66 | /** 67 | * 发送短信失败时最大重试次数 68 | */ 69 | @Value("${smb.bomb.send.retry.size:5}") 70 | private Integer maxRetryTimes; 71 | 72 | public SmsSendService(SmsUrlConfigService smsUrlConfigService, 73 | GroovySmsScriptExecutorService groovyScriptExecutorService, 74 | SmsSendLogRepository smsSendLogRepository, 75 | IdGenerator idGenerator) { 76 | this.smsUrlConfigService = smsUrlConfigService; 77 | this.groovyScriptExecutorService = groovyScriptExecutorService; 78 | this.smsSendLogRepository = smsSendLogRepository; 79 | this.idGenerator = idGenerator; 80 | loopCheckSmsSendResult(); 81 | } 82 | 83 | public Integer send(SmsSendDTO smsSendDTO) { 84 | if (StringUtils.isBlank(smsSendDTO.getPhone())) { 85 | return 0; 86 | } 87 | Integer windowSize = smsSendDTO.getSendItems(); 88 | List list = smsUrlConfigService.get(windowSize); 89 | if (!CollectionUtils.isEmpty(list)) { 90 | for (SmsUrlConfig entity : list) { 91 | if (entity.getMaxRetryTimes() == null) { 92 | entity.setMaxRetryTimes(maxRetryTimes); 93 | } else { 94 | entity.setMaxRetryTimes(Math.min(maxRetryTimes, entity.getMaxRetryTimes())); 95 | } 96 | final Future future = executorService.submit(new SmsTask(entity, smsSendDTO)); 97 | futureList.add(future); 98 | } 99 | return list.size(); 100 | } else { 101 | return 0; 102 | } 103 | } 104 | 105 | public void shutdown(String id) { 106 | if (executorService.isShutdown() || executorService.isTerminated()) { 107 | return; 108 | } 109 | final BlockingQueue queue = executorService.getQueue(); 110 | queue.removeIf(p -> { 111 | if (p instanceof SmsTask) { 112 | return Objects.equals(id, ((SmsTask) p).smsSendDTO.getRequestId()); 113 | } 114 | return false; 115 | }); 116 | } 117 | 118 | private void loopCheckSmsSendResult(){ 119 | /*checkSendSmsExecutor.scheduleAtFixedRate(() -> { 120 | if (!CollectionUtils.isEmpty(futureList)) { 121 | final Iterator> iterator = futureList.iterator(); 122 | while (iterator.hasNext()) { 123 | final Future next = iterator.next(); 124 | try { 125 | next.get(5, TimeUnit.SECONDS); 126 | } catch (Exception e) { 127 | next.cancel(true); 128 | e.printStackTrace(); 129 | }finally { 130 | iterator.remove(); 131 | } 132 | } 133 | } 134 | }, 10, 5, TimeUnit.SECONDS);*/ 135 | } 136 | 137 | public Boolean isFinished(String requestId, int windowSize) { 138 | SmsSendLog smsSendLog = new SmsSendLog(); 139 | smsSendLog.setRequestId(requestId); 140 | final ExampleMatcher exampleMatcher = ExampleMatcher.matching().withMatcher("requestId", 141 | ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.DEFAULT, true)); 142 | final long count = smsSendLogRepository.count(Example.of(smsSendLog, exampleMatcher)); 143 | return windowSize <= count; 144 | } 145 | 146 | private String request(ScriptContext scriptContext) throws IOException { 147 | SmsUrlConfig smsUrlConfig = scriptContext.getSmsUrlConfig(); 148 | Map paramsMap = scriptContext.getParamsMap(); 149 | Map headerMap = scriptContext.getHeaderMap(); 150 | Map queryMap = scriptContext.getQueryMap(); 151 | String requestMethod = smsUrlConfig.getRequestMethod().toUpperCase(); 152 | String smsUrl = smsUrlConfig.getSmsUrl(); 153 | if (HttpMethod.GET.matches(requestMethod)) { 154 | return HttpUtils.get(smsUrl, paramsMap, headerMap); 155 | } else if (HttpMethod.POST.matches(requestMethod)) { 156 | String contentType = smsUrlConfig.getContentType(); 157 | if ("application/json".equalsIgnoreCase(contentType)) { 158 | return HttpUtils.post(smsUrl, JSON.toJSONString(paramsMap), queryMap, headerMap, null); 159 | } else if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) { 160 | return HttpUtils.postFormUrlencoded(smsUrl, paramsMap, headerMap, queryMap); 161 | } else { 162 | throw new UnsupportedOperationException(contentType); 163 | } 164 | } else { 165 | throw new UnsupportedOperationException(requestMethod); 166 | } 167 | } 168 | 169 | private void saveAsyncSendLog(SmsSendDTO smsSendDTO, SmsUrlConfig smsUrlConfig, Object params, String response, 170 | int requestDuration, boolean success) { 171 | logExecutorService.execute(() -> { 172 | SmsSendLog record = new SmsSendLog(); 173 | record.setId((Long) idGenerator.next()); 174 | record.setRequestId(smsSendDTO.getRequestId()); 175 | record.setWebSiteName(smsUrlConfig.getWebsiteName()); 176 | record.setPhone(Base64.getEncoder().encodeToString(smsSendDTO.getPhone().getBytes(StandardCharsets.UTF_8))); 177 | record.setIp(smsSendDTO.getClientIp()); 178 | record.setSmsUrl(smsUrlConfig.getSmsUrl()); 179 | record.setParams(JSONObject.toJSONString(params)); 180 | if (StringUtils.isEmpty(response)) { 181 | record.setResponse(response); 182 | } else { 183 | record.setResponse(response.substring(0, Math.min(1024, response.length()))); 184 | } 185 | record.setRequestDuration(requestDuration); 186 | record.setResponseStatus(success ? SmsSendLog.SUCCESS_STATUS : SmsSendLog.FAILURE_STATUS); 187 | record.setCreateAt(LocalDateTime.now()); 188 | smsSendLogRepository.save(record); 189 | }); 190 | } 191 | 192 | private class SmsTask implements Runnable{ 193 | 194 | private final SmsUrlConfig entity; 195 | 196 | private final SmsSendDTO smsSendDTO; 197 | 198 | public SmsTask(SmsUrlConfig entity, SmsSendDTO smsSendDTO) { 199 | this.entity = entity; 200 | this.smsSendDTO = smsSendDTO; 201 | } 202 | 203 | private ScriptContext newScriptContext(ScriptContext preContext){ 204 | ScriptContext scriptContext = new ScriptContext(); 205 | scriptContext.getHeaderMap().putAll(FIXED_HEADER); 206 | scriptContext.getParamsMap().put(entity.getPhoneParamName(), smsSendDTO.getPhone()); 207 | scriptContext.getQueryMap().putAll(entity.getBindingQueryParamsMap()); 208 | scriptContext.setSmsUrlConfig(entity); 209 | scriptContext.setPreScriptContext(preContext); 210 | return scriptContext; 211 | } 212 | 213 | @Override 214 | public void run() { 215 | ScriptContext scriptContext = newScriptContext(null); 216 | int duration = -1; 217 | Boolean success = Boolean.FALSE; 218 | String response = null; 219 | try { 220 | logger.info("[smb.send]url:{},params:{},headers:{}", entity.getSmsUrl(), scriptContext.getParamsMap(), scriptContext.getQueryMap()); 221 | Integer maxRetryTimes = entity.getMaxRetryTimes(); 222 | if (maxRetryTimes == null || maxRetryTimes <= 0) { 223 | maxRetryTimes = 1; 224 | } 225 | for (int i = 0; i < maxRetryTimes; i++) { 226 | try { 227 | groovyScriptExecutorService.preInvoke(scriptContext); 228 | } catch (CancelException e) { 229 | logger.warn("[sms.send]该任务取消", e); 230 | response = e.getClass().getName() + ":" + e.getMessage(); 231 | break; 232 | } catch (Exception e) { 233 | logger.error("[sms.send]前置处理异常:", e); 234 | continue; 235 | } 236 | long startTime = System.currentTimeMillis(); 237 | long endTime = System.currentTimeMillis(); 238 | duration = (int) (endTime - startTime); 239 | logger.info("[sms.send]response:{},requestTime:{}ms", response, duration); 240 | try { 241 | response = request(scriptContext); 242 | //解析响应 243 | scriptContext.setResponse(response); 244 | success = (Boolean) groovyScriptExecutorService.postInvoke(scriptContext); 245 | if (Boolean.TRUE.equals(success)) { 246 | break; 247 | } 248 | } catch (HttpUtils.SimpleHttpResponseException e) { 249 | e.printStackTrace(); 250 | scriptContext.setResponse(e.getErrorEntity()); 251 | } 252 | if (groovyScriptExecutorService.retry(scriptContext) == Boolean.TRUE) { 253 | LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(new Random().nextInt(3) + 1)); 254 | scriptContext = newScriptContext(scriptContext); 255 | } else { 256 | break; 257 | } 258 | } 259 | } catch (HttpResponseException e) { 260 | response = String.format("{\"httpStatusCode\":\"%s\",\"reason\":\"%s\"}", e.getStatusCode(), e.getReasonPhrase()); 261 | e.printStackTrace(); 262 | } catch (Exception e) { 263 | e.printStackTrace(); 264 | response = StringUtils.isNotBlank(response) ? response + "\n" + e.getMessage() : e.getMessage(); 265 | } finally { 266 | saveAsyncSendLog(smsSendDTO, entity, scriptContext.getParamsMap(), response, duration, success); 267 | } 268 | } 269 | } 270 | } 271 | 272 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/service/impl/SmsUrlConfigService.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.service.impl; 2 | 3 | import com.lei2j.sms.bomb.entity.SmsUrlConfig; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * @author leijinjun 9 | * @date 2023/5/4 10 | **/ 11 | public interface SmsUrlConfigService { 12 | 13 | /** 14 | * 查询所有 15 | * @return 16 | */ 17 | List findAll(); 18 | 19 | /** 20 | * 查询指定数量列表 21 | * @param size 22 | * @return 23 | */ 24 | List get(int size); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/service/impl/SmsUrlConfigServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.service.impl; 2 | 3 | import com.lei2j.sms.bomb.entity.SmsUrlConfig; 4 | import com.lei2j.sms.bomb.repository.SmsUrlConfigRepository; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.*; 8 | 9 | /** 10 | * @author leijinjun 11 | * @date 2023/5/4 12 | **/ 13 | @Service 14 | public class SmsUrlConfigServiceImpl implements SmsUrlConfigService { 15 | 16 | private final SmsUrlConfigRepository smsUrlConfigRepository; 17 | 18 | public SmsUrlConfigServiceImpl(SmsUrlConfigRepository smsUrlConfigRepository) { 19 | this.smsUrlConfigRepository = smsUrlConfigRepository; 20 | } 21 | 22 | @Override 23 | public List findAll() { 24 | return smsUrlConfigRepository.findByNormalEquals(true); 25 | } 26 | 27 | @Override 28 | public List get(int size) { 29 | final List smsUrlConfigs = findAll(); 30 | final int length = smsUrlConfigs.size(); 31 | final Set set = rand(length, size, null); 32 | List result = new ArrayList<>(); 33 | for (Integer index : set) { 34 | result.add(smsUrlConfigs.get(index)); 35 | } 36 | return result; 37 | } 38 | 39 | private Set rand(int max, int size, Set set) { 40 | final Random random = new Random(); 41 | if (set == null) { 42 | set = new HashSet<>(); 43 | } 44 | size = Math.min(max, size); 45 | if (size <= 0) { 46 | return set; 47 | } 48 | final int nextInt = random.nextInt(max); 49 | set.add(nextInt); 50 | if (set.size() >= size) { 51 | return set; 52 | } else { 53 | return rand(max, size, set); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/util/IgnoreEmptyStringValueTransformer.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.util; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.springframework.data.domain.ExampleMatcher; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * @author leijinjun 10 | * @date 2021/1/19 11 | **/ 12 | public enum IgnoreEmptyStringValueTransformer implements ExampleMatcher.PropertyValueTransformer{ 13 | 14 | /** 15 | * 忽略空字符串 16 | */ 17 | IGNORE_EMPTY; 18 | 19 | 20 | @Override 21 | public Optional apply(Optional source) { 22 | boolean present = source.isPresent(); 23 | if (!present) { 24 | return source; 25 | } 26 | return source.filter(p -> { 27 | if (p instanceof String) { 28 | return StringUtils.trimToNull((String) p) != null; 29 | } 30 | return true; 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/util/SpringApplicationUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * 版权所有(C),上海海鼎信息科技有限公司,2022,所有权利保留。 3 | * 项目名: server-parent 4 | * 文件名: SpringApplicationUtil.java 5 | * 模块说明: 6 | * 修改历史: 7 | * 2022/6/29 下午11:25 - lei jinjun - 创建。 8 | */ 9 | 10 | package com.lei2j.sms.bomb.util; 11 | 12 | import org.springframework.beans.BeansException; 13 | import org.springframework.beans.factory.ListableBeanFactory; 14 | import org.springframework.beans.factory.config.BeanFactoryPostProcessor; 15 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 16 | import org.springframework.context.ApplicationContext; 17 | import org.springframework.context.ApplicationContextAware; 18 | import org.springframework.core.env.Environment; 19 | import org.springframework.stereotype.Component; 20 | 21 | import java.util.Objects; 22 | 23 | /** 24 | * 25 | * @author leijinjun 26 | * @date 2022/6/29 27 | **/ 28 | @Component 29 | public class SpringApplicationUtils implements BeanFactoryPostProcessor, ApplicationContextAware { 30 | 31 | private static ApplicationContext applicationContext; 32 | 33 | /** 34 | * "@PostConstruct"注解标记的类中,由于ApplicationContext还未加载,导致空指针
35 | * 因此实现BeanFactoryPostProcessor注入ConfigurableListableBeanFactory实现bean的操作 36 | */ 37 | private static ConfigurableListableBeanFactory beanFactory; 38 | 39 | @Override 40 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 41 | this.applicationContext = applicationContext; 42 | } 43 | 44 | @Override 45 | public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { 46 | this.beanFactory = beanFactory; 47 | } 48 | 49 | public static ListableBeanFactory getBeanFactory() { 50 | return Objects.requireNonNull(null == beanFactory ? applicationContext : beanFactory, "not initialized"); 51 | } 52 | 53 | public static ApplicationContext getApplicationContext() { 54 | return applicationContext; 55 | } 56 | 57 | /** 58 | * 通过name获取 Bean 59 | * 60 | * @param Bean类型 61 | * @param name Bean名称 62 | * @return Bean 63 | */ 64 | @SuppressWarnings("unchecked") 65 | public static T getBean(String name) { 66 | return (T) getBeanFactory().getBean(name); 67 | } 68 | 69 | /** 70 | * 通过class获取Bean 71 | * 72 | * @param Bean类型 73 | * @param clazz Bean类 74 | * @return Bean对象 75 | */ 76 | public static T getBean(Class clazz) { 77 | return getBeanFactory().getBean(clazz); 78 | } 79 | 80 | /** 81 | * 通过name,以及Clazz返回指定的Bean 82 | * 83 | * @param bean类型 84 | * @param name Bean名称 85 | * @param clazz bean类型 86 | * @return Bean对象 87 | */ 88 | public static T getBean(String name, Class clazz) { 89 | return getBeanFactory().getBean(name, clazz); 90 | } 91 | 92 | public static String getProperty(String key){ 93 | final Environment environment = getBeanFactory().getBean(Environment.class); 94 | return environment.getProperty(key); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/web/config/BeanConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.web.config; 2 | 3 | import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; 4 | import com.lei2j.idgen.core.IdGenerator; 5 | import com.lei2j.idgen.core.snowflake.SnowFlakeGenerator; 6 | import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; 7 | import org.springframework.boot.web.client.RestTemplateBuilder; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.client.RestTemplate; 11 | import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect; 12 | 13 | import java.time.Duration; 14 | 15 | /** 16 | * @author leijinjun 17 | * @date 2020/11/29 18 | **/ 19 | @Component 20 | public class BeanConfiguration { 21 | 22 | @Bean 23 | public RestTemplate restTemplate() { 24 | return new RestTemplateBuilder() 25 | .setConnectTimeout(Duration.ofSeconds(10)) 26 | .setReadTimeout(Duration.ofMinutes(30)) 27 | .build(); 28 | } 29 | 30 | @Bean 31 | public Java8TimeDialect java8TimeDialect() { 32 | return new Java8TimeDialect(); 33 | } 34 | 35 | @Bean 36 | public IdGenerator snowFlakeGenerator(){ 37 | return new SnowFlakeGenerator(); 38 | } 39 | 40 | @Bean 41 | public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { 42 | return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder 43 | .serializerByType(Long.class, ToStringSerializer.instance) 44 | .serializerByType(Long.TYPE, ToStringSerializer.instance); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/web/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.web.config; 2 | 3 | import com.lei2j.sms.bomb.web.interceptor.ClientIpInterceptor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.core.env.Environment; 6 | import org.springframework.util.StringUtils; 7 | import org.springframework.web.context.WebApplicationContext; 8 | import org.springframework.web.servlet.config.annotation.*; 9 | 10 | import javax.annotation.Resource; 11 | import javax.servlet.ServletContext; 12 | 13 | /** 14 | * @author leijinjun 15 | * @date 2020/12/22 16 | **/ 17 | @Configuration 18 | //此注解会导致一些web mvc配置失效,如默认首页无法找到,也不能继承{@code WebMvcConfigurationSupport} 19 | //@EnableWebMvc 20 | public class WebConfig implements WebMvcConfigurer { 21 | 22 | @Resource 23 | private Environment environment; 24 | 25 | @Override 26 | public void addInterceptors(InterceptorRegistry registry) { 27 | InterceptorRegistration registration = registry.addInterceptor(new ClientIpInterceptor()); 28 | String staticBase = environment.getProperty("mvc.static.resource.base"); 29 | if (StringUtils.hasText(staticBase)) { 30 | registration.excludePathPatterns(staticBase + "/**"); 31 | } 32 | } 33 | 34 | @Resource 35 | private void setAttrServletContext(WebApplicationContext attrServletContext){ 36 | ServletContext servletContext = attrServletContext.getServletContext(); 37 | String contextPath = environment.getProperty("server.servlet.context-path"); 38 | String staticResourcePath = environment.getProperty("mvc.static.resource.base"); 39 | String staticPath = ""; 40 | if (StringUtils.hasText(contextPath)) { 41 | staticPath += contextPath; 42 | } 43 | if (StringUtils.hasText(staticResourcePath)) { 44 | staticPath += staticResourcePath; 45 | } 46 | assert servletContext != null; 47 | servletContext.setAttribute("smsBombStaticPath", staticPath); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/web/controller/SmsBombController.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.web.controller; 2 | 3 | import com.lei2j.sms.bomb.dto.SmsSendDTO; 4 | import com.lei2j.sms.bomb.entity.SmsSendLog; 5 | import com.lei2j.sms.bomb.repository.SmsSendLogRepository; 6 | import com.lei2j.sms.bomb.repository.SmsSendLogRepository.GroupStatus; 7 | import com.lei2j.sms.bomb.service.impl.SmsSendService; 8 | import com.lei2j.sms.bomb.service.impl.SmsUrlConfigServiceImpl; 9 | import com.lei2j.sms.bomb.web.interceptor.ClientIpInterceptor; 10 | import org.apache.commons.lang3.RandomStringUtils; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import javax.servlet.http.HttpServletRequest; 18 | import java.util.*; 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.regex.Pattern; 21 | 22 | /** 23 | * @author leijinjun 24 | * @date 2020/11/29 25 | **/ 26 | @RestController 27 | public class SmsBombController { 28 | 29 | private final Pattern phonePattern = Pattern.compile("^1[3456789]\\d{9}$"); 30 | 31 | private final SmsSendService smsSendService; 32 | 33 | private final SmsSendLogRepository smsSendLogRepository; 34 | 35 | @Value("${smb.bomb.phone.send.day.max.count}") 36 | private Integer maxSendCount; 37 | 38 | @Value("${smb.bomb.send.window.size}") 39 | private Integer sendSize; 40 | 41 | @Autowired 42 | private SmsUrlConfigServiceImpl smsUrlConfigService; 43 | 44 | @Autowired 45 | public SmsBombController(SmsSendService smsSendService, SmsSendLogRepository smsSendLogRepository) { 46 | this.smsSendService = smsSendService; 47 | this.smsSendLogRepository = smsSendLogRepository; 48 | } 49 | 50 | @PostMapping("/smsBomb/send") 51 | public ResponseEntity send(@RequestParam("phone") String phone, 52 | @RequestParam("sendItems") Integer sendItems, 53 | HttpServletRequest request) { 54 | if (!phonePattern.asPredicate().test(phone)) { 55 | return ResponseEntity.badRequest().build(); 56 | } 57 | String clientIp = String.valueOf(request.getAttribute(ClientIpInterceptor.CLIENT_IP)); 58 | /*LocalDateTime now = LocalDateTime.now(ZoneId.of("CTT", ZoneId.SHORT_IDS)); 59 | LocalDateTime before = now.with(ChronoField.HOUR_OF_DAY, 0).with(ChronoField.MINUTE_OF_HOUR, 0).with(ChronoField.SECOND_OF_MINUTE, 0); 60 | LocalDateTime after = now.with(ChronoField.HOUR_OF_DAY, 23).with(ChronoField.MINUTE_OF_HOUR, 59).with(ChronoField.SECOND_OF_MINUTE, 59); 61 | long count = smsSendLogRepository.countByPhoneEqualsAndCreateAtBetween(phone, before, after); 62 | if (count >= maxSendCount) { 63 | return ResponseEntity.unprocessableEntity().body("手机号受限"); 64 | } 65 | long ipCount = smsSendLogRepository.countByIpEqualsAndCreateAtBetween(clientIp, before, after); 66 | if (ipCount >= maxSendCount) { 67 | return ResponseEntity.unprocessableEntity().body("IP受限"); 68 | }*/ 69 | if (sendItems == null) { 70 | sendItems = maxSendCount; 71 | } else { 72 | sendItems = Math.min(sendItems, maxSendCount); 73 | } 74 | String requestId = System.nanoTime() + "00" + RandomStringUtils.randomNumeric(7); 75 | SmsSendDTO smsSendDTO = new SmsSendDTO(); 76 | smsSendDTO.setPhone(phone); 77 | smsSendDTO.setClientIp(clientIp); 78 | smsSendDTO.setRequestId(requestId); 79 | smsSendDTO.setSendItems(sendItems); 80 | Integer sendSize = smsSendService.send(smsSendDTO); 81 | Map resultMap = new HashMap<>(3); 82 | resultMap.put("requestId", requestId); 83 | resultMap.put("sendSize", sendSize); 84 | return sendSize != null && sendSize > 0 ? ResponseEntity.ok(resultMap) : ResponseEntity.unprocessableEntity().body( 85 | "没有可用的短信API资源"); 86 | } 87 | 88 | @RequestMapping("/smsBomb/shutdown/{id}") 89 | public ResponseEntity shutdown(@PathVariable("id") String id) { 90 | smsSendService.shutdown(id); 91 | return ResponseEntity.ok().build(); 92 | } 93 | 94 | @GetMapping("/smsBomb/getResult") 95 | public ResponseEntity getResult(String requestId, 96 | int sendSize) throws InterruptedException { 97 | if (StringUtils.isBlank(requestId)) { 98 | return ResponseEntity.badRequest().build(); 99 | } 100 | Object[] result = new Object[]{0, 0}; 101 | Boolean finished = smsSendService.isFinished(requestId, sendSize); 102 | result[0] = Objects.equals(true, finished) ? 1 : 0; 103 | TimeUnit.MILLISECONDS.sleep(500); 104 | List mapList = smsSendLogRepository.groupByResponseStatus(requestId); 105 | int totalCount = mapList.stream().mapToInt(GroupStatus::getTotalCount).sum(); 106 | result[1] = 107 | mapList.stream().filter(p -> Objects.equals(p.getResponseStatus(), SmsSendLog.SUCCESS_STATUS)).mapToInt(GroupStatus::getTotalCount).sum(); 108 | return ResponseEntity.ok(result); 109 | } 110 | 111 | @GetMapping("/smsBomb/getSize") 112 | public ResponseEntity getSize(int size) throws InterruptedException { 113 | return ResponseEntity.ok(smsUrlConfigService.get(size)); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/web/controller/SmsUrlConfigController.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.web.controller; 2 | 3 | import com.lei2j.idgen.core.IdGenerator; 4 | import com.lei2j.sms.bomb.base.entity.Pager; 5 | import com.lei2j.sms.bomb.entity.SmsUrlConfig; 6 | import com.lei2j.sms.bomb.repository.SmsUrlConfigRepository; 7 | import com.lei2j.sms.bomb.util.HttpUtils; 8 | import com.lei2j.sms.bomb.util.IgnoreEmptyStringValueTransformer; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.jsoup.Connection; 11 | import org.jsoup.Jsoup; 12 | import org.jsoup.nodes.Document; 13 | import org.jsoup.select.Elements; 14 | import org.springframework.beans.BeanUtils; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.data.domain.*; 17 | import org.springframework.http.ResponseEntity; 18 | import org.springframework.web.bind.annotation.*; 19 | 20 | import java.time.LocalDateTime; 21 | import java.util.Objects; 22 | import java.util.Optional; 23 | 24 | /** 25 | * @author leijinjun 26 | * @date 2021/1/16 27 | **/ 28 | @RestController 29 | @RequestMapping("/url/config") 30 | public class SmsUrlConfigController { 31 | 32 | private final SmsUrlConfigRepository smsUrlConfigRepository; 33 | 34 | private final IdGenerator idGenerator; 35 | 36 | @Autowired 37 | public SmsUrlConfigController(SmsUrlConfigRepository smsUrlConfigRepository, 38 | IdGenerator idGenerator) { 39 | this.smsUrlConfigRepository = smsUrlConfigRepository; 40 | this.idGenerator = idGenerator; 41 | } 42 | 43 | @GetMapping("/page") 44 | public ResponseEntity urlConfig(SmsUrlConfig params, 45 | @RequestParam(value = "pageNo",required = false,defaultValue = "1")Integer pageNo, 46 | @RequestParam(value = "pageSize",required = false,defaultValue = "20")Integer pageSize){ 47 | PageRequest pageRequest = PageRequest.of(Math.max(pageNo - 1, 0), pageSize, Sort.by("id").descending()); 48 | ExampleMatcher exampleMatcher = ExampleMatcher.matching().withIgnoreNullValues() 49 | .withMatcher("websiteName", 50 | ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING, true).transform(IgnoreEmptyStringValueTransformer.IGNORE_EMPTY)); 51 | Example example = Example.of(params, exampleMatcher); 52 | Page page = smsUrlConfigRepository.findAll(example, pageRequest); 53 | return ResponseEntity.ok(Pager.convert(page)); 54 | } 55 | 56 | @GetMapping("/{id}") 57 | public ResponseEntity get(@PathVariable("id")Long id){ 58 | return ResponseEntity.of(smsUrlConfigRepository.findById(id)); 59 | } 60 | 61 | @PostMapping("/updateStatus") 62 | public ResponseEntity update(Long id, 63 | @RequestParam(value = "normal") Boolean normal) { 64 | return smsUrlConfigRepository.findById(id).map(c -> { 65 | c.setNormal(normal); 66 | smsUrlConfigRepository.saveAndFlush(c); 67 | return ResponseEntity.noContent().build(); 68 | }).orElse(ResponseEntity.notFound().build()); 69 | } 70 | 71 | @PostMapping("/updateAllStatus") 72 | public ResponseEntity update(@RequestParam(value = "status") Boolean normal) { 73 | smsUrlConfigRepository.updateAllStatus(normal); 74 | return ResponseEntity.ok().build(); 75 | } 76 | 77 | @PostMapping("/save") 78 | public ResponseEntity update(SmsUrlConfig smsUrlConfig) { 79 | if (StringUtils.isBlank(smsUrlConfig.getSmsUrl())) { 80 | return ResponseEntity.badRequest().body("接口地址不能为空"); 81 | } 82 | return Optional.of(smsUrlConfig) 83 | .map(c -> { 84 | Long id = c.getId(); 85 | final boolean isCreated = Objects.isNull(id); 86 | Optional configOptional = isCreated ? Optional.of(c) : smsUrlConfigRepository.findById(id); 87 | return configOptional.map(t -> { 88 | BeanUtils.copyProperties(c, t, "createAt", "updateAt", "lastUsedTime","normal"); 89 | if (isCreated) { 90 | t.setId(((Long) idGenerator.next())); 91 | t.setNormal(true); 92 | } 93 | if (StringUtils.isNotBlank(t.getWebsite())&&StringUtils.isBlank(t.getWebsiteName())) { 94 | try { 95 | final Connection connect = Jsoup.connect(t.getWebsite()); 96 | final Document document = connect.get(); 97 | final Elements titleEle = document.getElementsByTag("title"); 98 | if (!titleEle.isEmpty()) { 99 | String text = titleEle.get(0).text(); 100 | text = text.substring(0, Math.min(text.length(), 255)); 101 | t.setWebsiteName(text); 102 | } 103 | Elements elements = document.getElementsByAttributeValueContaining("href", "favicon"); 104 | if (elements.isEmpty()) { 105 | elements = document.getElementsByAttributeValueContaining("href", "logo"); 106 | } 107 | if (!elements.isEmpty()) { 108 | final String href = elements.get(0).attr("href"); 109 | if (href.startsWith("http")) { 110 | t.setIcon(href); 111 | } else { 112 | final int index = href.indexOf("/"); 113 | String iconUrl = t.getWebsite() + "" + href.substring(index); 114 | t.setIcon(iconUrl); 115 | } 116 | } 117 | } catch (Exception ignore) { 118 | 119 | } 120 | } 121 | if (Objects.isNull(t.getCreateAt())) { 122 | t.setCreateAt(LocalDateTime.now()); 123 | } 124 | t.setUpdateAt(LocalDateTime.now()); 125 | smsUrlConfigRepository.save(t); 126 | return ResponseEntity.ok().build(); 127 | }).orElse(ResponseEntity.notFound().build()); 128 | }).get(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/web/controller/ViewController.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.web.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | 7 | /** 8 | * @author leijinjun 9 | * @date 2021/1/24 10 | **/ 11 | @Controller 12 | public class ViewController { 13 | 14 | @GetMapping("/admin/{path}/{function}") 15 | public String view(@PathVariable("path") String path, 16 | @PathVariable("function")String function) { 17 | return String.format("admin/%s/%s", path, function); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/lei2j/sms/bomb/web/interceptor/ClientIpInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.web.interceptor; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.springframework.web.servlet.HandlerInterceptor; 5 | 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | 9 | /** 10 | * @author leijinjun 11 | * @date 2020/12/22 12 | **/ 13 | public class ClientIpInterceptor implements HandlerInterceptor { 14 | 15 | public static final String CLIENT_IP = "client_ip"; 16 | 17 | @Override 18 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 19 | String clientIp; 20 | String ip = request.getHeader("X-Forwarded-For"); 21 | if (StringUtils.isNotBlank(ip) && isNotUnknown(ip)) { 22 | clientIp = ip.split(",", 1)[0]; 23 | } else if (StringUtils.isNotBlank(ip = request.getHeader("X-Real-IP")) && isNotUnknown(ip)) { 24 | clientIp = ip; 25 | } else if (StringUtils.isNotBlank(ip = request.getHeader("Proxy-Client-IP")) && isNotUnknown(ip)) { 26 | clientIp = ip; 27 | } else { 28 | clientIp = request.getRemoteAddr(); 29 | } 30 | request.setAttribute(CLIENT_IP, clientIp); 31 | return true; 32 | } 33 | 34 | private boolean isNotUnknown(String ip) { 35 | return !"unknown".equalsIgnoreCase(ip); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "smb.bomb.script.base.path", 5 | "type": "java.lang.String", 6 | "description": "groovy script base path for smb.bomb.script.base.path." 7 | }, 8 | { 9 | "name": "smb.bomb.send.window.size", 10 | "type": "java.lang.String", 11 | "description": "Description for smb.bomb.send.window.size." 12 | }, 13 | { 14 | "name": "mvc.static.resource.base", 15 | "type": "java.lang.String", 16 | "description": "Description for mvc.static.resource.base." 17 | } 18 | ] } -------------------------------------------------------------------------------- /src/main/resources/application-dev.properties: -------------------------------------------------------------------------------- 1 | debug=true 2 | #使用sqlite 3 | spring.datasource.type=org.sqlite.SQLiteDataSource 4 | spring.datasource.driver-class-name=org.sqlite.JDBC 5 | #默认使用classpath下sms_bomb.db 6 | spring.datasource.url=jdbc:sqlite:${DB_FILE_PATH::resource:db/sms_bomb.db} 7 | #使用mysql 8 | #spring.datasource.driver-class-name=com.mysql.jdbc.Driver 9 | #spring.datasource.type=com.zaxxer.hikari.HikariDataSource 10 | #\u6570\u636E\u5E93\u5730\u5740 11 | #spring.datasource.url=jdbc:mysql://${DATABASE-HOST:nuc.local\ 12 | # .com:3306}/${DATABASE-NAME:sms_bomb}?characterEncoding=utf8&useUnicode=true&autoReconnect\ 13 | # =true&failOverReadOnly=false&useSSL=false 14 | spring.datasource.hikari.login-timeout=2000 15 | #\u6570\u636E\u5E93\u7528\u6237 16 | spring.datasource.hikari.username=${DATABASE-USER:dev} 17 | #\u6570\u636E\u5E93\u5BC6\u7801 18 | spring.datasource.hikari.password=${DATABASE-PASSWORD:123456} 19 | spring.datasource.hikari.connection-test-query=select 1 20 | spring.datasource.hikari.maximum-pool-size=8 21 | spring.datasource.hikari.minimum-idle=3 22 | spring.datasource.hikari.pool-name=sms-bomb 23 | spring.jpa.open-in-view=false 24 | 25 | spring.thymeleaf.cache=false 26 | 27 | logging.file.name=sms-bomb.log 28 | logging.file.path=C:\\temp\\smsBomb 29 | #ocr\u670D\u52A1\u63D0\u4F9B\u5546\u540D\u79F0\uFF0C\u76EE\u524D\u53EA\u652F\u6301ddddOcr 30 | ocr.provider.name=ddddOcr 31 | #ocr\u670D\u52A1\u5730\u5740 32 | ocr.dddd.url=http://192.168.100.1:9898/ocr/file 33 | #ocr\u670D\u52A1\u5730\u5740\uFF0C\u4F7F\u7528base64\u4F20\u9012\u8BF7\u6C42\u53C2\u6570 34 | ocr.dddd.base64.url=http://192.168.100.1:9898/ocr/b64/text 35 | 36 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.profiles.active=dev 2 | server.port=8080 3 | spring.application.name=smb-bomb 4 | spring.output.ansi.enabled=detect 5 | 6 | spring.jackson.date-format=uuuu-MM-dd HH:mm:ss 7 | spring.jackson.time-zone=GMT+8 8 | spring.jackson.generator.ignore-unknown=true 9 | #日志配置 10 | logging.level.root = info 11 | logging.charset.console=UTF-8 12 | logging.charset.file=UTF-8 13 | #logging.config=classpath:logback-spring.xml 14 | logging.file.name=sms-bomb.log 15 | #logging.file.name=/opt/smsBomb/${spring.application.name}.log 16 | 17 | mvc.static.resource.base=/static 18 | spring.mvc.static-path-pattern=${mvc.static.resource.base}/** 19 | spring.thymeleaf.prefix=classpath:/templates/ 20 | spring.thymeleaf.suffix=.html 21 | spring.thymeleaf.check-template=true 22 | spring.thymeleaf.enabled=true 23 | spring.thymeleaf.enable-spring-el-compiler=true 24 | 25 | smb.bomb.script.base.path = /sms-bomb/script/ 26 | smb.bomb.send.window.size = 10 27 | smb.bomb.phone.send.day.max.count = 50 -------------------------------------------------------------------------------- /src/main/resources/com/lei2j/sms/bomb/script/SmsCommonScript.groovy: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.script 2 | 3 | import com.lei2j.sms.bomb.ocr.OcrRequest 4 | import com.lei2j.sms.bomb.ocr.OcrService 5 | import com.lei2j.sms.bomb.service.impl.ScriptThreadContext 6 | import com.lei2j.sms.bomb.util.SpringApplicationUtils 7 | 8 | class SmsCommonScript implements SmsScript{ 9 | 10 | protected String identifyImgCaptcha(InputStream imgStream) { 11 | def ocrService = getOcrService() 12 | OcrRequest ocrRequest = new OcrRequest() 13 | ocrRequest.setInputStream(imgStream) 14 | def text = ocrService.ocr(ocrRequest) 15 | return text ? text : '' 16 | } 17 | 18 | protected String identifyImgCaptcha(File imgFile) { 19 | def inputStream = new FileInputStream(imgFile) 20 | return identifyImgCaptcha(inputStream) 21 | } 22 | 23 | protected String identifyImgCaptcha(URL imgUrl) { 24 | return identifyImgCaptcha(imgUrl.openStream()) 25 | } 26 | 27 | protected String identifyImgCaptcha(String base64) { 28 | def ocrService = getOcrService() 29 | OcrRequest ocrRequest = new OcrRequest() 30 | ocrRequest.setBase64(base64) 31 | def text = ocrService.ocr(ocrRequest) 32 | return text ? text : '' 33 | } 34 | 35 | private static OcrService getOcrService() { 36 | def scriptContext = ScriptThreadContext.get() 37 | SpringApplicationUtils applicationUtils = scriptContext.getSpringApplicationUtils() 38 | def providerName = applicationUtils.getProperty("ocr.provider.name") 39 | def ocrService = applicationUtils.getBean(providerName, OcrService.class) 40 | return ocrService 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/resources/com/lei2j/sms/bomb/script/SmsScript.groovy: -------------------------------------------------------------------------------- 1 | package com.lei2j.sms.bomb.script 2 | 3 | import com.alibaba.fastjson.JSON 4 | import com.alibaba.fastjson.JSONObject 5 | import com.lei2j.sms.bomb.entity.SmsUrlConfig 6 | import com.lei2j.sms.bomb.service.impl.ScriptContext 7 | import com.lei2j.util.Base64Util 8 | import com.sun.imageio.plugins.gif.GIFImageReader 9 | import com.sun.imageio.plugins.gif.GIFImageReaderSpi 10 | import com.sun.imageio.plugins.gif.GIFImageWriter 11 | import com.sun.imageio.plugins.gif.GIFImageWriterSpi 12 | import groovy.json.JsonSlurper 13 | import groovy.text.GStringTemplateEngine 14 | import groovy.xml.XmlSlurper 15 | import org.apache.commons.lang3.RandomStringUtils 16 | import org.apache.commons.lang3.StringUtils 17 | import org.apache.http.HeaderElement 18 | import org.apache.http.message.BasicHeaderElement 19 | 20 | import javax.crypto.Cipher 21 | import javax.crypto.spec.IvParameterSpec 22 | import javax.crypto.spec.SecretKeySpec 23 | import javax.imageio.spi.ImageReaderSpi 24 | import javax.imageio.spi.ImageWriterSpi 25 | import javax.imageio.stream.FileImageInputStream 26 | import javax.imageio.stream.FileImageOutputStream 27 | import javax.imageio.stream.ImageOutputStream 28 | import javax.imageio.stream.MemoryCacheImageInputStream 29 | import javax.imageio.stream.MemoryCacheImageOutputStream 30 | import java.nio.charset.StandardCharsets 31 | import java.nio.file.Files 32 | import java.nio.file.Paths 33 | import java.nio.file.StandardCopyOption 34 | import java.security.Key 35 | import java.security.KeyFactory 36 | import java.security.spec.X509EncodedKeySpec 37 | import java.util.function.BiFunction 38 | 39 | trait SmsScript { 40 | 41 | GStringTemplateEngine stringTemplateEngine = new GStringTemplateEngine() 42 | 43 | enum ResponseTypeEnum { 44 | JSON, 45 | XML, 46 | JSONP, 47 | TEXT; 48 | } 49 | 50 | void preProcess(ScriptContext scriptContext) { 51 | SmsUrlConfig smsUrlConfig = scriptContext.getSmsUrlConfig() 52 | Map paramsMap = scriptContext.getParamsMap() 53 | Map headerMap = scriptContext.getHeaderMap() 54 | List headerList = smsUrlConfig.getHeaderList() 55 | headerList.forEach((headerPair) -> { 56 | String[] split = headerPair.split(":", 2) 57 | String name = split[0] 58 | String value = split[1] 59 | if (StringUtils.isNotBlank(value)) { 60 | if ("cookie".equalsIgnoreCase(name)) { 61 | BiFunction f = (key, val) -> { (val + (val.endsWith(";") ? "" : ";") + value) } 62 | String o1 = headerMap.computeIfPresent("Cookie", f) 63 | String o2 = headerMap.computeIfPresent("cookie", f) 64 | if (o1 == null && o2 == null) { 65 | headerMap.put("Cookie", value) 66 | } 67 | } else { 68 | headerMap.put(name, value.trim()) 69 | } 70 | } 71 | }) 72 | if (!headerMap.containsKey("Accept") && !headerMap.containsKey("accept")) { 73 | headerMap.put("Accept", "*/*") 74 | } 75 | if (!headerMap.containsKey("Referer") && !headerMap.containsKey("referer")) { 76 | headerMap.put("Referer", scriptContext.getSmsUrlConfig().getWebsite()) 77 | } 78 | //解析固定请求参数 79 | String bindingParams = smsUrlConfig.getBindingParams() 80 | if (StringUtils.isNotBlank(bindingParams)) { 81 | JSONObject object = JSONObject.parseObject(bindingParams) 82 | for (Map.Entry entry : object.entrySet()) { 83 | paramsMap.put(entry.getKey(), entry.getValue()) 84 | } 85 | } 86 | if (ResponseTypeEnum.valueOf(scriptContext.getSmsUrlConfig().responseType.toUpperCase()) == ResponseTypeEnum.JSONP) { 87 | paramsMap.put(getJsonpRequestKey(), getJsonpResponseValue()) 88 | } 89 | parseExpressionScriptContext(scriptContext) 90 | } 91 | 92 | /** 93 | * 解析表达式 94 | * @param scriptContext 95 | */ 96 | void parseExpressionScriptContext(ScriptContext scriptContext) { 97 | def map = JSON.parseObject(JSON.toJSONString(scriptContext), Map.class) 98 | scriptContext.getQueryMap().forEach((k, v) -> { 99 | def template = stringTemplateEngine.createTemplate(v.toString()) 100 | scriptContext.getQueryMap().put(k, template.make(map).toString()) 101 | }) 102 | scriptContext.getHeaderMap().forEach((k, v) -> { 103 | def template = stringTemplateEngine.createTemplate(v.toString()) 104 | scriptContext.getHeaderMap().put(k, template.make(map).toString()) 105 | }) 106 | scriptContext.getParamsMap().forEach((k, v) -> { 107 | def template = stringTemplateEngine.createTemplate(v.toString()) 108 | scriptContext.getParamsMap().put(k, template.make(map).toString()) 109 | }) 110 | scriptContext.getSmsUrlConfig().setSmsUrl(stringTemplateEngine.createTemplate( 111 | scriptContext.getSmsUrlConfig().getSmsUrl()).make(scriptContext.getParamsMap()).toString()) 112 | } 113 | 114 | Boolean postProcess(ScriptContext scriptContext) { 115 | SmsUrlConfig smsUrlConfig = scriptContext.getSmsUrlConfig() 116 | String response = scriptContext.getResponse() 117 | if (response == null || response.isEmpty()) { 118 | return Boolean.TRUE 119 | } 120 | def successCode = smsUrlConfig.getSuccessCode() 121 | if (successCode == null || successCode.isEmpty()) { 122 | return Boolean.FALSE 123 | } 124 | def responseType = smsUrlConfig.getResponseType() 125 | try { 126 | return parseResponse(scriptContext, smsUrlConfig.getSuccessCode(), response, ResponseTypeEnum.valueOf(responseType.toUpperCase())) 127 | } catch (Exception e) { 128 | e.printStackTrace() 129 | return false 130 | } 131 | } 132 | 133 | Boolean parseResponse(ScriptContext scriptContext, String resultFormatCode, String response, ResponseTypeEnum responseType) { 134 | def parseObject = null 135 | if (responseType == ResponseTypeEnum.XML) { 136 | parseObject = new XmlSlurper().parseText(response) 137 | } else if (responseType == ResponseTypeEnum.JSON) { 138 | parseObject = new JsonSlurper().parseText(response) 139 | } else if (responseType == ResponseTypeEnum.TEXT) { 140 | return parseText(scriptContext, response, resultFormatCode) 141 | } else if (responseType == ResponseTypeEnum.JSONP) { 142 | return parseJsonp(scriptContext, response, resultFormatCode) 143 | } 144 | if (parseObject) { 145 | def sp = resultFormatCode.split(",", 2) 146 | def keyPair = sp[0].trim() 147 | def valuePair = sp[1].trim() 148 | def code = keyPair.split("=", 2)[1] 149 | def value = valuePair.split("=", 2)[1] 150 | def split = code.split("\\.") 151 | if (Objects.nonNull(parseObject)) { 152 | for (int i = 0; i < split.length; i++) { 153 | parseObject = parseObject.(split[i].trim()) 154 | if (parseObject == null) { 155 | return false 156 | } 157 | } 158 | parseObject = parseObject.toString() 159 | return parseObject == value 160 | } 161 | } 162 | return false 163 | } 164 | 165 | Boolean retry(ScriptContext scriptContext) { 166 | SmsUrlConfig smsUrlConfig = scriptContext.getSmsUrlConfig() 167 | String response = scriptContext.getResponse() 168 | if (smsUrlConfig.getEndCode()) { 169 | String[] split = smsUrlConfig.getEndCode().split('[\r\n]') 170 | for (String sp : split) { 171 | if (parseResponse(scriptContext, sp.trim(), response, ResponseTypeEnum.valueOf(smsUrlConfig.getResponseType().toUpperCase()))) { 172 | return true 173 | } 174 | } 175 | } 176 | false 177 | } 178 | 179 | /** 180 | * 由子类重写 181 | * @param scriptContext 182 | * @return 183 | */ 184 | Boolean parseText(ScriptContext scriptContext, String response, String resultFormatCode) { 185 | if (!response) { 186 | return true 187 | } 188 | response == resultFormatCode 189 | } 190 | 191 | Boolean parseJsonp(ScriptContext scriptContext, String response, String resultFormatCode){ 192 | def callbackMethod = scriptContext.getParamsMap().get(getJsonpRequestKey()).toString() 193 | def regex = ~/${callbackMethod}\((.*)\)/ 194 | def matcher = regex.matcher(response) 195 | assert matcher.find() 196 | parseResponse(scriptContext, resultFormatCode, matcher.group(1), ResponseTypeEnum.JSON) 197 | } 198 | 199 | 200 | void setCookie(String key, String value, Map headerMap) { 201 | Map> responseHeaderMap = new HashMap<>() 202 | def list = new ArrayList() 203 | list.add(new BasicHeaderElement[]{new BasicHeaderElement(key, value)}) 204 | responseHeaderMap.put('Set-Cookie', list) 205 | setCookie(responseHeaderMap, headerMap) 206 | } 207 | 208 | void setCookie(Map> responseHeaderMap, Map headerMap) { 209 | setCookie(responseHeaderMap, headerMap, false) 210 | } 211 | 212 | void setCookie(Map> responseHeaderMap, Map headerMap, boolean ifPresentReserve) { 213 | if (responseHeaderMap.containsKey("Set-Cookie")) { 214 | def headerElements = responseHeaderMap.get('Set-Cookie') as List 215 | setCookie(headerElements, headerMap, ifPresentReserve) 216 | } 217 | if (responseHeaderMap.containsKey("set-cookie")) { 218 | def headerElements = responseHeaderMap.get('set-cookie') as List 219 | setCookie(headerElements, headerMap, ifPresentReserve) 220 | } 221 | if (responseHeaderMap.containsKey("set-Cookie")) { 222 | def headerElements = responseHeaderMap.get('set-Cookie') as List 223 | setCookie(headerElements, headerMap, ifPresentReserve) 224 | } 225 | if (responseHeaderMap.containsKey("Set-cookie")) { 226 | def headerElements = responseHeaderMap.get('Set-cookie') as List 227 | setCookie(headerElements, headerMap, ifPresentReserve) 228 | } 229 | if (responseHeaderMap.containsKey("SET-COOKIE")) { 230 | def headerElements = responseHeaderMap.get('SET-COOKIE') as List 231 | setCookie(headerElements, headerMap, ifPresentReserve) 232 | } 233 | } 234 | 235 | void setCookie(List headerElements, Map headerMap, boolean ifPresentReserve){ 236 | headerElements.stream().forEach(c -> { 237 | String cookie = headerMap.get('Cookie') 238 | if (cookie) { 239 | List list = new ArrayList<>(Arrays.asList(cookie.split(';'))) 240 | boolean exist = list.stream().anyMatch(p -> { 241 | String[] itemSplit = p.split('=', 2) 242 | return Objects.equals(itemSplit[0].trim(), c[0].getName()) 243 | }) 244 | if (!(exist && ifPresentReserve)) { 245 | list.removeIf(p -> p.trim().startsWith(c[0].getName().trim())) 246 | list.add(c[0].getName() + '=' + c[0].getValue()) 247 | } 248 | headerMap.put('Cookie', String.join(';', list)) 249 | } else { 250 | headerMap.put('Cookie', String.format('%s=%s', c[0].getName(), c[0].getValue())) 251 | } 252 | }) 253 | } 254 | 255 | String getCookieValue(Map headerMap, String key) { 256 | def cookie = headerMap.get('Cookie') 257 | if (!cookie) { 258 | cookie = headerMap.get('cookie') 259 | } 260 | if (cookie) { 261 | String[] split = cookie.split(";") 262 | def list = split.toList() 263 | for (String str : list) { 264 | def split1 = str.split("=") 265 | if (split1[0].trim() == key) { 266 | return split1.length > 1 ? split1[1].trim() : null 267 | } 268 | } 269 | } 270 | return null 271 | } 272 | 273 | /** 274 | * 使用RSA加密 275 | * @param data 276 | * @param publicKeyBase64 277 | * @return 278 | */ 279 | String encryptByRsa(String data, String publicKeyBase64) { 280 | def keySpec = new X509EncodedKeySpec(publicKeyBase64.decodeBase64()) 281 | def keyFactory = KeyFactory.getInstance("RSA") 282 | def publicKey = keyFactory.generatePublic(keySpec) 283 | Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()) 284 | cipher.init(Cipher.ENCRYPT_MODE, publicKey) 285 | return cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)).encodeBase64().toString() 286 | } 287 | 288 | /** 289 | * AES加密 290 | * @param text 291 | * @param encryptionKey 292 | * @return 返回加密后的16进制字符串 293 | */ 294 | String encryptAes(String text,String encryptionKey) { 295 | Key aesKey = new SecretKeySpec(encryptionKey.getBytes("UTF-8"), "AES") 296 | if (!text) return text 297 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") 298 | cipher.init(Cipher.ENCRYPT_MODE, aesKey) 299 | String encryptedStr = cipher.doFinal(text.getBytes("UTF-8")).encodeHex() 300 | return encryptedStr 301 | } 302 | 303 | /** 304 | * AES加密 305 | * @param text 306 | * @param encryptionKey 307 | * @return 返回加密后的base64字符串 308 | */ 309 | String encryptAesBase64(String text,String encryptionKey) { 310 | Key aesKey = new SecretKeySpec(encryptionKey.getBytes("UTF-8"), "AES") 311 | if (!text) return text 312 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") 313 | cipher.init(Cipher.ENCRYPT_MODE, aesKey) 314 | String encryptedStr = cipher.doFinal(text.getBytes("UTF-8")).encodeBase64() 315 | return encryptedStr 316 | } 317 | 318 | /** 319 | * AES加密 320 | * @param text 321 | * @param encryptionKey 322 | * @return 323 | */ 324 | String encryptAes(String text, String encryptionKey, String iv) { 325 | Key aesKey = new SecretKeySpec(encryptionKey.getBytes("UTF-8"), "AES") 326 | if (!text) return text 327 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") 328 | cipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(iv.getBytes())) 329 | String encryptedStr = cipher.doFinal(text.getBytes("UTF-8")).encodeBase64() 330 | return encryptedStr 331 | } 332 | 333 | String getJsonpRequestKey(){ 334 | 'callback' 335 | } 336 | 337 | String getJsonpResponseValue(){ 338 | 'jQuery' + RandomStringUtils.randomNumeric(19) + "_" + System.currentTimeMillis() 339 | } 340 | 341 | File getGifOneFrame(InputStream inputStream,String targetName, int frame) { 342 | String path = System.getenv("tmp") +File.separator+ "${targetName}.gif" 343 | Files.copy(inputStream, Paths.get(path), StandardCopyOption.REPLACE_EXISTING) 344 | ImageReaderSpi readerSpi = new GIFImageReaderSpi() 345 | GIFImageReader gifReader = (GIFImageReader) readerSpi.createReaderInstance() 346 | def imageInputStream = new FileImageInputStream(new File(path)) 347 | gifReader.setInput(imageInputStream) 348 | int num = gifReader.getNumImages(true) 349 | if (num > frame) { 350 | for (int i = 0; i < num; i++) { 351 | if (frame == i) { 352 | ImageWriterSpi writerSpi = new GIFImageWriterSpi() 353 | GIFImageWriter writer = (GIFImageWriter) writerSpi.createWriterInstance() 354 | def file = new File(System.getenv("tmp") + File.separator + "${targetName}.jpg") 355 | FileImageOutputStream out = new FileImageOutputStream(file) 356 | writer.setOutput(out) 357 | // 读取读取帧的图片 358 | writer.write(gifReader.read(i)) 359 | out.flush() 360 | out.close() 361 | imageInputStream.close() 362 | return file 363 | } 364 | } 365 | } else { 366 | imageInputStream.close() 367 | } 368 | return null 369 | } 370 | 371 | /** 372 | * 373 | * @param base64 图片base64 374 | * @param frame 获取第几帧,等于-1时,返回所有帧 375 | * @return 返回帧图片的base64格式集合 376 | */ 377 | List getGifFrame(String base64, int frame) { 378 | List result = new ArrayList<>() 379 | ImageReaderSpi readerSpi = new GIFImageReaderSpi() 380 | GIFImageReader gifReader = (GIFImageReader) readerSpi.createReaderInstance() 381 | gifReader.setInput(new MemoryCacheImageInputStream(new ByteArrayInputStream(Base64Util.decode(base64)))) 382 | int num = gifReader.getNumImages(true) 383 | if (num > frame) { 384 | for (int i = 0; i < num; i++) { 385 | ImageWriterSpi writerSpi = new GIFImageWriterSpi() 386 | GIFImageWriter writer = (GIFImageWriter) writerSpi.createWriterInstance() 387 | if (frame == -1 || frame == i) { 388 | def outputStream = new ByteArrayOutputStream() 389 | ImageOutputStream out = new MemoryCacheImageOutputStream(outputStream) 390 | writer.setOutput(out) 391 | // 读取读取帧的图片 392 | writer.write(gifReader.read(i)) 393 | out.flush() 394 | result.add(Base64Util.encodeToString(outputStream.toByteArray())) 395 | } 396 | } 397 | } 398 | return result 399 | } 400 | 401 | /** 402 | * 403 | * @param inputStream 图片gif流 404 | * @param frame 获取第几帧,等于-1时,返回所有帧 405 | * @return 返回帧图片的base64格式集合 406 | */ 407 | List getGifFrame(InputStream inputStream, int frame) { 408 | byte[] b = new byte[4096] 409 | int len = 0 410 | ByteArrayOutputStream out = new ByteArrayOutputStream() 411 | while ((len = inputStream.read(b)) != -1) { 412 | out.write(b, 0, len) 413 | } 414 | getGifFrame(Base64Util.encodeToString(out.toByteArray()), -1) 415 | } 416 | } -------------------------------------------------------------------------------- /src/main/resources/db/README.MD: -------------------------------------------------------------------------------- 1 | sms_bomb.db文件中有少量测试数据,可用于测试发送短信验证码。 -------------------------------------------------------------------------------- /src/main/resources/db/sms_bomb.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/db/sms_bomb.db -------------------------------------------------------------------------------- /src/main/resources/file-appender.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 11 | 12 | ${FILE_LOG_PATTERN} 13 | 14 | ${logFile} 15 | 16 | ${logFile}.%d{yyyy-MM-dd}.%i.gz 17 | ${LOG_FILE_MAX_SIZE:-50MB} 18 | ${LOG_FILE_MAX_HISTORY:-0} 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | logback 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/resources/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/img/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.6.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 28 | font-size: 1rem; 29 | font-weight: 400; 30 | line-height: 1.5; 31 | color: #212529; 32 | text-align: left; 33 | background-color: #fff; 34 | } 35 | 36 | [tabindex="-1"]:focus:not(:focus-visible) { 37 | outline: 0 !important; 38 | } 39 | 40 | hr { 41 | box-sizing: content-box; 42 | height: 0; 43 | overflow: visible; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | margin-top: 0; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | p { 52 | margin-top: 0; 53 | margin-bottom: 1rem; 54 | } 55 | 56 | abbr[title], 57 | abbr[data-original-title] { 58 | text-decoration: underline; 59 | -webkit-text-decoration: underline dotted; 60 | text-decoration: underline dotted; 61 | cursor: help; 62 | border-bottom: 0; 63 | -webkit-text-decoration-skip-ink: none; 64 | text-decoration-skip-ink: none; 65 | } 66 | 67 | address { 68 | margin-bottom: 1rem; 69 | font-style: normal; 70 | line-height: inherit; 71 | } 72 | 73 | ol, 74 | ul, 75 | dl { 76 | margin-top: 0; 77 | margin-bottom: 1rem; 78 | } 79 | 80 | ol ol, 81 | ul ul, 82 | ol ul, 83 | ul ol { 84 | margin-bottom: 0; 85 | } 86 | 87 | dt { 88 | font-weight: 700; 89 | } 90 | 91 | dd { 92 | margin-bottom: .5rem; 93 | margin-left: 0; 94 | } 95 | 96 | blockquote { 97 | margin: 0 0 1rem; 98 | } 99 | 100 | b, 101 | strong { 102 | font-weight: bolder; 103 | } 104 | 105 | small { 106 | font-size: 80%; 107 | } 108 | 109 | sub, 110 | sup { 111 | position: relative; 112 | font-size: 75%; 113 | line-height: 0; 114 | vertical-align: baseline; 115 | } 116 | 117 | sub { 118 | bottom: -.25em; 119 | } 120 | 121 | sup { 122 | top: -.5em; 123 | } 124 | 125 | a { 126 | color: #007bff; 127 | text-decoration: none; 128 | background-color: transparent; 129 | } 130 | 131 | a:hover { 132 | color: #0056b3; 133 | text-decoration: underline; 134 | } 135 | 136 | a:not([href]):not([class]) { 137 | color: inherit; 138 | text-decoration: none; 139 | } 140 | 141 | a:not([href]):not([class]):hover { 142 | color: inherit; 143 | text-decoration: none; 144 | } 145 | 146 | pre, 147 | code, 148 | kbd, 149 | samp { 150 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 151 | font-size: 1em; 152 | } 153 | 154 | pre { 155 | margin-top: 0; 156 | margin-bottom: 1rem; 157 | overflow: auto; 158 | -ms-overflow-style: scrollbar; 159 | } 160 | 161 | figure { 162 | margin: 0 0 1rem; 163 | } 164 | 165 | img { 166 | vertical-align: middle; 167 | border-style: none; 168 | } 169 | 170 | svg { 171 | overflow: hidden; 172 | vertical-align: middle; 173 | } 174 | 175 | table { 176 | border-collapse: collapse; 177 | } 178 | 179 | caption { 180 | padding-top: 0.75rem; 181 | padding-bottom: 0.75rem; 182 | color: #6c757d; 183 | text-align: left; 184 | caption-side: bottom; 185 | } 186 | 187 | th { 188 | text-align: inherit; 189 | text-align: -webkit-match-parent; 190 | } 191 | 192 | label { 193 | display: inline-block; 194 | margin-bottom: 0.5rem; 195 | } 196 | 197 | button { 198 | border-radius: 0; 199 | } 200 | 201 | button:focus:not(:focus-visible) { 202 | outline: 0; 203 | } 204 | 205 | input, 206 | button, 207 | select, 208 | optgroup, 209 | textarea { 210 | margin: 0; 211 | font-family: inherit; 212 | font-size: inherit; 213 | line-height: inherit; 214 | } 215 | 216 | button, 217 | input { 218 | overflow: visible; 219 | } 220 | 221 | button, 222 | select { 223 | text-transform: none; 224 | } 225 | 226 | [role="button"] { 227 | cursor: pointer; 228 | } 229 | 230 | select { 231 | word-wrap: normal; 232 | } 233 | 234 | button, 235 | [type="button"], 236 | [type="reset"], 237 | [type="submit"] { 238 | -webkit-appearance: button; 239 | } 240 | 241 | button:not(:disabled), 242 | [type="button"]:not(:disabled), 243 | [type="reset"]:not(:disabled), 244 | [type="submit"]:not(:disabled) { 245 | cursor: pointer; 246 | } 247 | 248 | button::-moz-focus-inner, 249 | [type="button"]::-moz-focus-inner, 250 | [type="reset"]::-moz-focus-inner, 251 | [type="submit"]::-moz-focus-inner { 252 | padding: 0; 253 | border-style: none; 254 | } 255 | 256 | input[type="radio"], 257 | input[type="checkbox"] { 258 | box-sizing: border-box; 259 | padding: 0; 260 | } 261 | 262 | textarea { 263 | overflow: auto; 264 | resize: vertical; 265 | } 266 | 267 | fieldset { 268 | min-width: 0; 269 | padding: 0; 270 | margin: 0; 271 | border: 0; 272 | } 273 | 274 | legend { 275 | display: block; 276 | width: 100%; 277 | max-width: 100%; 278 | padding: 0; 279 | margin-bottom: .5rem; 280 | font-size: 1.5rem; 281 | line-height: inherit; 282 | color: inherit; 283 | white-space: normal; 284 | } 285 | 286 | progress { 287 | vertical-align: baseline; 288 | } 289 | 290 | [type="number"]::-webkit-inner-spin-button, 291 | [type="number"]::-webkit-outer-spin-button { 292 | height: auto; 293 | } 294 | 295 | [type="search"] { 296 | outline-offset: -2px; 297 | -webkit-appearance: none; 298 | } 299 | 300 | [type="search"]::-webkit-search-decoration { 301 | -webkit-appearance: none; 302 | } 303 | 304 | ::-webkit-file-upload-button { 305 | font: inherit; 306 | -webkit-appearance: button; 307 | } 308 | 309 | output { 310 | display: inline-block; 311 | } 312 | 313 | summary { 314 | display: list-item; 315 | cursor: pointer; 316 | } 317 | 318 | template { 319 | display: none; 320 | } 321 | 322 | [hidden] { 323 | display: none !important; 324 | } 325 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.6.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/glyphicons/flat-ui-pro-icons-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/glyphicons/flat-ui-pro-icons-regular.eot -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/glyphicons/flat-ui-pro-icons-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/glyphicons/flat-ui-pro-icons-regular.ttf -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/glyphicons/flat-ui-pro-icons-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/glyphicons/flat-ui-pro-icons-regular.woff -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-black.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-black.eot -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-black.ttf -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-black.woff -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bold.eot -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bold.ttf -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bold.woff -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bolditalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bolditalic.eot -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bolditalic.ttf -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-bolditalic.woff -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-italic.eot -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-italic.ttf -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-italic.woff -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-light.eot -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-light.ttf -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-light.woff -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-regular.eot -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-regular.ttf -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/flat-ui/fonts/lato/lato-regular.woff -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leijinjun/sms-bomb/2d5aa6fdb4b33494e96ecc499e7edbc17d7b23df/src/main/resources/static/js/bootstrap@4.6.0/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/plugins/bootstrap-switch.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap-switch - Turn checkboxes and radio buttons into toggle switches. 3 | * 4 | * @version v3.3.5 5 | * @homepage https://bttstrp.github.io/bootstrap-switch 6 | * @author Mattia Larentis (http://larentis.eu) 7 | * @license MIT 8 | */ 9 | 10 | .bootstrap-switch{display:inline-block;direction:ltr;cursor:pointer;border-radius:4px;border:1px solid #ccc;position:relative;text-align:left;overflow:hidden;line-height:8px;z-index:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;-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;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.bootstrap-switch .bootstrap-switch-container{display:inline-block;top:0;border-radius:4px;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on,.bootstrap-switch .bootstrap-switch-label{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;cursor:pointer;display:table-cell;vertical-align:middle;padding:6px 12px;font-size:14px;line-height:20px}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on{text-align:center;z-index:1}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary{color:#fff;background:#337ab7}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info{color:#fff;background:#5bc0de}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success{color:#fff;background:#5cb85c}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning{background:#f0ad4e;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger{color:#fff;background:#d9534f}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default{color:#000;background:#eee}.bootstrap-switch .bootstrap-switch-label{text-align:center;margin-top:-1px;margin-bottom:-1px;z-index:100;color:#333;background:#fff}.bootstrap-switch span::before{content:"\200b"}.bootstrap-switch .bootstrap-switch-handle-on{border-bottom-left-radius:3px;border-top-left-radius:3px}.bootstrap-switch .bootstrap-switch-handle-off{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch input[type=radio],.bootstrap-switch input[type=checkbox]{position:absolute!important;top:0;left:0;margin:0;z-index:-1;opacity:0;filter:alpha(opacity=0);visibility:hidden}.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label{padding:1px 5px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label{padding:5px 10px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label{padding:6px 16px;font-size:18px;line-height:1.3333333}.bootstrap-switch.bootstrap-switch-disabled,.bootstrap-switch.bootstrap-switch-indeterminate,.bootstrap-switch.bootstrap-switch-readonly{cursor:default!important}.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label{opacity:.5;filter:alpha(opacity=50);cursor:default!important}.bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container{-webkit-transition:margin-left .5s;-o-transition:margin-left .5s;transition:margin-left .5s}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on{border-radius:0 3px 3px 0}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off{border-radius:3px 0 0 3px}.bootstrap-switch.bootstrap-switch-focused{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label{border-bottom-left-radius:3px;border-top-left-radius:3px} -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap@4.6.0/plugins/bootstrap-switch.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap-switch - Turn checkboxes and radio buttons into toggle switches. 3 | * 4 | * @version v3.3.5 5 | * @homepage https://bttstrp.github.io/bootstrap-switch 6 | * @author Mattia Larentis (http://larentis.eu) 7 | * @license MIT 8 | */ 9 | 10 | (function(a,b){if('function'==typeof define&&define.amd)define(['jquery'],b);else if('undefined'!=typeof exports)b(require('jquery'));else{b(a.jquery),a.bootstrapSwitch={exports:{}}.exports}})(this,function(a){'use strict';function c(x,y){if(!(x instanceof y))throw new TypeError('Cannot call a class as a function')}function d(x,y){var z=x.state,A=x.size,B=x.disabled,C=x.readonly,D=x.indeterminate,E=x.inverse;return[z?'on':'off',A,B?'disabled':void 0,C?'readonly':void 0,D?'indeterminate':void 0,E?'inverse':void 0,y?'id-'+y:void 0].filter(function(F){return null==F})}function e(){return{state:this.$element.is(':checked'),size:this.$element.data('size'),animate:this.$element.data('animate'),disabled:this.$element.is(':disabled'),readonly:this.$element.is('[readonly]'),indeterminate:this.$element.data('indeterminate'),inverse:this.$element.data('inverse'),radioAllOff:this.$element.data('radio-all-off'),onColor:this.$element.data('on-color'),offColor:this.$element.data('off-color'),onText:this.$element.data('on-text'),offText:this.$element.data('off-text'),labelText:this.$element.data('label-text'),handleWidth:this.$element.data('handle-width'),labelWidth:this.$element.data('label-width'),baseClass:this.$element.data('base-class'),wrapperClass:this.$element.data('wrapper-class')}}function f(){var x=this,y=this.$on.add(this.$off).add(this.$label).css('width',''),z='auto'===this.options.handleWidth?Math.round(Math.max(this.$on.width(),this.$off.width())):this.options.handleWidth;return y.width(z),this.$label.width(function(A,B){return'auto'===x.options.labelWidth?B-(x.privateHandleWidth/2);z=!1,x.state(x.options.inverse?!D:D)}else x.state(!x.options.state);y=!1}},'mouseleave.bootstrapSwitch':function(){x.$label.trigger('mouseup.bootstrapSwitch')}})}function n(){var x=this,y=this.$element.closest('label');y.on('click',function(z){z.preventDefault(),z.stopImmediatePropagation(),z.target===y[0]&&x.toggleState()})}function o(){function x(){return u(this).data('bootstrap-switch')}function y(){return u(this).bootstrapSwitch('state',this.checked)}var z=this.$element.closest('form');z.data('bootstrap-switch')||z.on('reset.bootstrapSwitch',function(){window.setTimeout(function(){z.find('input').filter(x).each(y)},1)}).data('bootstrap-switch',!0)}function p(x){var y=this;return u.isArray(x)?x.map(function(z){return h.call(y,z)}):[h.call(this,x)]}var r=function(x){return x&&x.__esModule?x:{default:x}}(a),s=Object.assign||function(x){for(var z,y=1;y',{class:function(){return d(z.options,z.$element.attr('id')).map(function(C){return h.call(z,C)}).concat([z.options.baseClass],p.call(z,z.options.wrapperClass)).join(' ')}}),this.$container=u('
',{class:h.call(this,'container')}),this.$on=u('',{html:this.options.onText,class:h.call(this,'handle-on')+' '+h.call(this,this.options.onColor)}),this.$off=u('',{html:this.options.offText,class:h.call(this,'handle-off')+' '+h.call(this,this.options.offColor)}),this.$label=u('',{html:this.options.labelText,class:h.call(this,'label')}),this.$element.on('init.bootstrapSwitch',function(){return z.options.onInit(y)}),this.$element.on('switchChange.bootstrapSwitch',function(){for(var B=arguments.length,C=Array(B),D=0;D').html(this.options.on).addClass(this._onstyle+" "+b),d=a('