├── .gitignore ├── .idea ├── .gitignore ├── SpringbootDemo.iml ├── compiler.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── misc.xml ├── modules.xml ├── uiDesigner.xml └── vcs.xml ├── LICENSE ├── README.md ├── elasticsearch-canal-demo ├── README.md ├── doc │ ├── es-mapping │ └── table.sql ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── fox │ │ └── es_canal │ │ ├── ElasticsearchCanalDemoApplication.java │ │ ├── config │ │ ├── ElasticsearchConfig.java │ │ └── WebMvcConfig.java │ │ ├── constant │ │ └── BlogConstants.java │ │ ├── controller │ │ ├── BlogController.java │ │ └── TestController.java │ │ ├── dto │ │ └── BlogSimpleInfoDTO.java │ │ ├── entity │ │ ├── JacksonObjectMapper.java │ │ └── Result.java │ │ └── service │ │ ├── BlogService.java │ │ └── impl │ │ └── BlogServiceImpl.java │ └── resources │ └── application.yml ├── elasticsearch-demo ├── doc │ ├── mapping.kibana │ └── table.sql ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── fox │ │ │ └── es │ │ │ ├── ElasticsearchDemoApplication.java │ │ │ ├── config │ │ │ ├── ElasticsearchConfig.java │ │ │ └── WebMvcConfig.java │ │ │ ├── controller │ │ │ ├── BlogController.java │ │ │ └── TestController.java │ │ │ ├── dto │ │ │ └── BlogSimpleInfoDTO.java │ │ │ ├── entity │ │ │ ├── JacksonObjectMapper.java │ │ │ └── Result.java │ │ │ └── service │ │ │ ├── BlogService.java │ │ │ └── impl │ │ │ └── BlogServiceImpl.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── com │ └── fox │ └── es │ └── CommonTest.java ├── minio-chunk-demo ├── README.md ├── doc │ └── table.sql ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── fox │ │ └── miniodemo │ │ ├── MinioChunkDemoApplication.java │ │ ├── config │ │ ├── MinioConfig.java │ │ └── MyBatisPlusConfig.java │ │ ├── constant │ │ └── HttpStatus.java │ │ ├── controller │ │ └── MediaFileController.java │ │ ├── dao │ │ └── MediaFileMapper.java │ │ ├── entity │ │ └── Result.java │ │ ├── handler │ │ └── MyMetaObjectHandler.java │ │ ├── po │ │ └── MediaFile.java │ │ ├── service │ │ ├── MediaFileService.java │ │ └── impl │ │ │ └── MediaFileServiceImpl.java │ │ ├── util │ │ ├── CipherUtils.java │ │ ├── FileFormatUtils.java │ │ ├── FileTypeUtils.java │ │ ├── MinioClientUtils.java │ │ └── RegexUtils.java │ │ └── vo │ │ ├── CheckChunkFileVO.java │ │ └── UploadMergeChunksVO.java │ └── resources │ └── application.yaml ├── qrcode-demo ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── zhulang │ │ └── qrcode │ │ ├── Application.java │ │ ├── controller │ │ ├── QrCodePluginController.java │ │ └── ZXingController.java │ │ └── util │ │ ├── QrCodePluginUtils.java │ │ └── ZXingUtils.java │ └── resources │ ├── application.yml │ └── static │ ├── html │ ├── qrcodePlugin │ │ ├── BgBlackWhiteCode.html │ │ ├── ColorCode.html │ │ ├── CommonBlackWhiteCode.html │ │ ├── ImgFillCode.html │ │ ├── LogoBlackWhiteCode.html │ │ └── ShapeCode.html │ └── zxing │ │ ├── CommonBlackWhite.html │ │ └── ZXingLogo.html │ └── js │ └── axios.js ├── sms-demo ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── fox │ │ └── sms │ │ ├── DirectTest │ │ └── AliSmsTest.java │ │ ├── SmsDemoApplication.java │ │ ├── component │ │ └── FastJsonRedisSerializer.java │ │ ├── config │ │ ├── RedisConfig.java │ │ └── SmsConfig.java │ │ ├── constant │ │ └── RedisConstants.java │ │ ├── controller │ │ └── SmsAliController.java │ │ ├── entity │ │ └── Result.java │ │ ├── service │ │ ├── SmsAliService.java │ │ └── impl │ │ │ └── SmsAliServiceImpl.java │ │ └── util │ │ ├── AliSmsTemplateUtils.java │ │ ├── RedisCacheUtils.java │ │ └── RegexUtils.java │ └── resources │ └── application.yml ├── xfxh-web-simple-demo ├── README.md ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── zhulang │ │ └── xfxhsimple │ │ ├── XfXhApplication.java │ │ ├── component │ │ └── XfXhStreamClient.java │ │ ├── config │ │ └── XfXhConfig.java │ │ ├── controller │ │ └── TestController.java │ │ ├── dto │ │ ├── MsgDTO.java │ │ ├── RequestDTO.java │ │ └── ResponseDTO.java │ │ └── listener │ │ └── XfXhWebSocketListener.java │ └── resources │ ├── application.yml │ └── demo-json │ ├── request.json │ └── response.json └── xfxh-web-support-context-demo ├── README.md ├── pom.xml └── src └── main ├── java └── com │ └── zhulang │ └── xfxh │ ├── Application.java │ ├── component │ ├── MemoryUserRecordSpace.java │ ├── ScheduleTask.java │ └── XfXhStreamClient.java │ ├── config │ └── XfXhConfig.java │ ├── controller │ └── TestController.java │ ├── dto │ ├── InteractMsg.java │ ├── MsgDTO.java │ ├── RecordsArray.java │ ├── RequestDTO.java │ └── ResponseDTO.java │ └── listener │ └── XfXhWebSocketListener.java └── resources ├── META-INF └── additional-spring-configuration-metadata.json ├── application.yml └── demo-json ├── request.json └── response.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java 11 | .mtj. 12 | 13 | # Package Files 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see 23 | http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | 26 | .classpath 27 | .project 28 | .settings 29 | target 30 | .idea 31 | *.iml -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/SpringbootDemo.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpringbootDemo 2 | 1.minio-chunk-demo 模块是minio分块处理服务,对应文章:[minio&前后端分离上传视频/上传大文件——前后端分离断点续传&minio分片上传实现](https://blog.csdn.net/qq_62982856/article/details/129002288) 3 | 在 minio-chunk-demo 模块下的doc文件下有数据库表的创建。 4 | 5 | 2.elasticsearch-demo 模块是es搜索服务实战代码,对应文章:[springboot整合es进行搜索](https://blog.csdn.net/qq_62982856/article/details/129722916) 6 | 7 | 3.elasticsearch-canal-demo 模块是es搜索服务+canal实现同步数据库数据到es的实战代码,对应文章:[docker环境安装mysql、canal、elasticsearch,基于binlog利用canal实现mysql的数据同步到elasticsearch中](https://blog.csdn.net/qq_62982856/article/details/129875733) 8 | 9 | 4.sms-demo 模块是手机短信服务,对应文章:[阿里云短信服务详细说明与实战开发后端代码](https://blog.csdn.net/qq_62982856/article/details/129901491) 10 | 11 | 5.qrcode-demo 模块是二维码服务,对应文章:[Java生成二维码(前后端分离项目实战)](https://blog.csdn.net/qq_62982856/article/details/132572246) 12 | 13 | 6.xfxh-web-simple-demo 模块是讯飞星火大模型后端接口服务,对应文章:[讯飞星火认知大模型Java后端接口](https://blog.csdn.net/qq_62982856/article/details/133151673) 14 | 15 | 7.xfxh-web-support-context-demo 模块是讯飞星火大模型后端接口增强服务,在 xfxh-web-simple-demo 模块接口基础上支持了基于上下文的问题回答 -------------------------------------------------------------------------------- /elasticsearch-canal-demo/README.md: -------------------------------------------------------------------------------- 1 | 1. 注意修改application.yml配置文件信息,改为你自己的ip地址 -------------------------------------------------------------------------------- /elasticsearch-canal-demo/doc/es-mapping: -------------------------------------------------------------------------------- 1 | 【PUT】 http://192.168.65.133:9200/es_demo_collect 2 | 3 | { 4 | "settings": { 5 | "number_of_shards": 1, 6 | "number_of_replicas": 0 7 | }, 8 | "mappings": { 9 | "properties": { 10 | "userId": { 11 | "type": "long" 12 | }, 13 | "username":{ 14 | "analyzer": "ik_max_word", 15 | "search_analyzer": "ik_smart", 16 | "type": "text" 17 | }, 18 | "userIcon":{ 19 | "type": "keyword", 20 | "index": false 21 | }, 22 | "title": { 23 | "analyzer": "ik_max_word", 24 | "search_analyzer": "ik_smart", 25 | "type": "text" 26 | }, 27 | "tags": { 28 | "analyzer": "ik_max_word", 29 | "search_analyzer": "ik_smart", 30 | "type": "text" 31 | }, 32 | "introduce":{ 33 | "analyzer": "ik_max_word", 34 | "search_analyzer": "ik_smart", 35 | "type": "text" 36 | }, 37 | "content":{ 38 | "analyzer": "ik_max_word", 39 | "search_analyzer": "ik_smart", 40 | "type": "text" 41 | }, 42 | "createTime":{ 43 | "format": "date_optional_time||epoch_millis", 44 | "type": "date" 45 | }, 46 | "updateTime":{ 47 | "format": "date_optional_time||epoch_millis", 48 | "type": "date" 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /elasticsearch-canal-demo/doc/table.sql: -------------------------------------------------------------------------------- 1 | # 如果存在 es_demo 数据库则删除 2 | DROP DATABASE IF EXISTS `es_demo`; 3 | 4 | # 创建新数据库 5 | CREATE DATABASE `es_demo`; 6 | 7 | # 使用数据库 8 | USE `es_demo`; 9 | 10 | # 创建一张用户表,注意自增主键id是从1000开始 11 | CREATE TABLE `user`( 12 | `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键id', 13 | `username` VARCHAR(24) NOT NULL COMMENT '用户名', 14 | `icon` VARCHAR(255) NOT NULL COMMENT '头像url', 15 | PRIMARY KEY(`id`) 16 | )ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; 17 | 18 | # 创建一张博客表,注意自增主键id是从1000开始 19 | CREATE TABLE `blog`( 20 | `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键id', 21 | `user_id` INT NOT NULL COMMENT '用户id(雪花算法生成)', 22 | `title` VARCHAR(255) NOT NULL COMMENT '标题', 23 | `tags` VARCHAR(64) NOT NULL COMMENT '标签', 24 | `introduce` VARCHAR(512) NOT NULL COMMENT '介绍', 25 | `content` TEXT NOT NULL COMMENT '文章内容', 26 | `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 27 | `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 28 | PRIMARY KEY(`id`), 29 | KEY `idx_user_create`(`user_id`,`create_time` DESC) 30 | )ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8mb4 COMMENT='博客信息表'; -------------------------------------------------------------------------------- /elasticsearch-canal-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.fox 8 | elasticsearch-canal-demo 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 8 13 | 8 14 | UTF-8 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-web 20 | 2.6.3 21 | 22 | 23 | org.projectlombok 24 | lombok 25 | 1.18.24 26 | 27 | 28 | org.elasticsearch.client 29 | elasticsearch-rest-high-level-client 30 | 7.8.0 31 | 32 | 33 | 34 | com.alibaba 35 | fastjson 36 | 1.2.33 37 | 38 | 39 | cn.hutool 40 | hutool-all 41 | 5.8.7 42 | 43 | 44 | -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/java/com/fox/es_canal/ElasticsearchCanalDemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.fox.es_canal; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author 狐狸半面添 8 | * @create 2023-03-22 18:07 9 | */ 10 | @SpringBootApplication 11 | public class ElasticsearchCanalDemoApplication { 12 | public static void main(String[] args) { 13 | SpringApplication.run(ElasticsearchCanalDemoApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/java/com/fox/es_canal/config/ElasticsearchConfig.java: -------------------------------------------------------------------------------- 1 | package com.fox.es_canal.config; 2 | 3 | import org.apache.http.HttpHost; 4 | import org.elasticsearch.client.RestClient; 5 | import org.elasticsearch.client.RestClientBuilder; 6 | import org.elasticsearch.client.RestHighLevelClient; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | /** 12 | * @author 狐狸半面添 13 | * @create 2023-03-22 17:51 14 | */ 15 | @Configuration 16 | public class ElasticsearchConfig { 17 | @Value("${elasticsearch.hostname}") 18 | private String hostname; 19 | @Value("${elasticsearch.port}") 20 | private Integer port; 21 | 22 | @Bean 23 | public RestHighLevelClient restHighLevelClient() { 24 | RestClientBuilder builder = RestClient.builder( 25 | new HttpHost(hostname, port, "http") 26 | ); 27 | return new RestHighLevelClient(builder); 28 | } 29 | } -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/java/com/fox/es_canal/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.fox.es_canal.config; 2 | 3 | import com.fox.es_canal.entity.JacksonObjectMapper; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.converter.HttpMessageConverter; 6 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * @author 狐狸半面添 13 | * @create 2023-02-11 23:25 14 | */ 15 | 16 | @Configuration 17 | public class WebMvcConfig extends WebMvcConfigurationSupport { 18 | 19 | @Override 20 | protected void extendMessageConverters(List> converters) { 21 | MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(); 22 | messageConverter.setObjectMapper(new JacksonObjectMapper()); 23 | converters.add(0, messageConverter); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/java/com/fox/es_canal/constant/BlogConstants.java: -------------------------------------------------------------------------------- 1 | package com.fox.es_canal.constant; 2 | 3 | /** 4 | * @author 狐狸半面添 5 | * @create 2023-03-31 3:41 6 | */ 7 | public class BlogConstants { 8 | /** 9 | * 每页搜索最多15条 10 | */ 11 | public static final int SEARCH_PAGE_NUM = 15; 12 | } 13 | -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/java/com/fox/es_canal/controller/BlogController.java: -------------------------------------------------------------------------------- 1 | package com.fox.es_canal.controller; 2 | 3 | import com.fox.es_canal.constant.BlogConstants; 4 | import com.fox.es_canal.entity.Result; 5 | import com.fox.es_canal.service.BlogService; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import javax.annotation.Resource; 12 | 13 | /** 14 | * @author 狐狸半面添 15 | * @create 2023-03-22 20:16 16 | */ 17 | @RestController 18 | @RequestMapping("/blog") 19 | public class BlogController { 20 | 21 | @Resource 22 | private BlogService blogService; 23 | 24 | /** 25 | * 通过关键词获取数据列表 26 | * 27 | * @param keyWords 关键词 28 | * @param pageNo 页码 29 | * @return 数据列表,按照相关性从高到低进行排序 30 | */ 31 | @GetMapping("/list") 32 | public Result list(@RequestParam("keyWords") String keyWords, 33 | @RequestParam("pageNo") Integer pageNo) { 34 | // BlogConstants是我写的一个常量类,里面定义了一个变量 SEARCH_PAGE_NUM = 15 35 | return blogService.list(keyWords, pageNo, BlogConstants.SEARCH_PAGE_NUM); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/java/com/fox/es_canal/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package com.fox.es_canal.controller; 2 | 3 | import com.fox.es_canal.entity.Result; 4 | import org.elasticsearch.client.RequestOptions; 5 | import org.elasticsearch.client.RestHighLevelClient; 6 | import org.elasticsearch.client.core.MainResponse; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import javax.annotation.Resource; 11 | import java.io.IOException; 12 | 13 | /** 14 | * @author 狐狸半面添 15 | * @create 2023-03-22 18:33 16 | */ 17 | @RestController 18 | public class TestController { 19 | @Resource 20 | private RestHighLevelClient restHighLevelClient; 21 | 22 | /** 23 | * 用于测试是否连接 es 成功 24 | * 25 | * @return 返回 es 的基本信息,等价于访问:http://192.168.65.133:9200 26 | * @throws IOException 异常信息 27 | */ 28 | @GetMapping("/getEsInfo") 29 | public Result getEsInfo() throws IOException { 30 | MainResponse info = restHighLevelClient.info(RequestOptions.DEFAULT); 31 | return Result.ok(info); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/java/com/fox/es_canal/dto/BlogSimpleInfoDTO.java: -------------------------------------------------------------------------------- 1 | package com.fox.es_canal.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | /** 10 | * @author 狐狸半面添 11 | * @create 2023-03-22 21:33 12 | */ 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class BlogSimpleInfoDTO { 17 | /** 18 | * 主键id 19 | */ 20 | private Integer id; 21 | /** 22 | * 用户id 23 | */ 24 | private Long userId; 25 | /** 26 | * 用户名 27 | */ 28 | private String username; 29 | /** 30 | * 用户头像 31 | */ 32 | private String userIcon; 33 | /** 34 | * 标题 35 | */ 36 | private String title; 37 | /** 38 | * 介绍 39 | */ 40 | private String introduce; 41 | /** 42 | * 创建时间 43 | */ 44 | private LocalDateTime createTime; 45 | /** 46 | * 修改时间 47 | */ 48 | private LocalDateTime updateTime; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/java/com/fox/es_canal/entity/JacksonObjectMapper.java: -------------------------------------------------------------------------------- 1 | package com.fox.es_canal.entity; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.module.SimpleModule; 5 | import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; 6 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; 7 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; 8 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; 9 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; 10 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; 11 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; 12 | 13 | import java.math.BigInteger; 14 | import java.time.LocalDate; 15 | import java.time.LocalDateTime; 16 | import java.time.LocalTime; 17 | import java.time.format.DateTimeFormatter; 18 | 19 | import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; 20 | 21 | /** 22 | * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象 23 | * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象] 24 | * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON] 25 | * 26 | * @author 狐狸半面添 27 | * @create 2023-01-18 20:34 28 | */ 29 | public class JacksonObjectMapper extends ObjectMapper { 30 | 31 | public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; 32 | public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; 33 | public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; 34 | 35 | public JacksonObjectMapper() { 36 | super(); 37 | //收到未知属性时不报异常 38 | this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); 39 | 40 | //反序列化时,属性不存在的兼容处理 41 | this.getDeserializationConfig().withoutFeatures(FAIL_ON_UNKNOWN_PROPERTIES); 42 | 43 | 44 | SimpleModule simpleModule = new SimpleModule() 45 | .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) 46 | .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) 47 | .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) 48 | 49 | .addSerializer(BigInteger.class, ToStringSerializer.instance) 50 | .addSerializer(Long.class, ToStringSerializer.instance) 51 | .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) 52 | .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) 53 | .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); 54 | //注册功能模块 例如,可以添加自定义序列化器和反序列化器 55 | this.registerModule(simpleModule); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/java/com/fox/es_canal/entity/Result.java: -------------------------------------------------------------------------------- 1 | package com.fox.es_canal.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Data; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | /** 9 | * 统一返回对象 10 | * 11 | * @author 狐狸半面添 12 | * @create 2023-03-22 18:34 13 | */ 14 | @Getter 15 | @Setter 16 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 17 | public class Result { 18 | private Integer code; 19 | private String msg; 20 | private Object data; 21 | 22 | private Result(Integer code, String msg, Object data) { 23 | this.code = code; 24 | this.msg = msg; 25 | this.data = data; 26 | } 27 | 28 | private Result(Integer code, String msg) { 29 | this.code = code; 30 | this.msg = msg; 31 | } 32 | 33 | private Result(){} 34 | 35 | public static Result ok() { 36 | return new Result(200, "success"); 37 | } 38 | 39 | public static Result ok(Object data) { 40 | return new Result(200, "success", data); 41 | } 42 | 43 | public static Result error(String msg) { 44 | return new Result(500, msg); 45 | } 46 | 47 | public static Result error(Integer code, String msg) { 48 | return new Result(code, msg); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/java/com/fox/es_canal/service/BlogService.java: -------------------------------------------------------------------------------- 1 | package com.fox.es_canal.service; 2 | 3 | import com.fox.es_canal.entity.Result; 4 | 5 | /** 6 | * @author 狐狸半面添 7 | * @create 2023-03-22 20:18 8 | */ 9 | public interface BlogService { 10 | 11 | /** 12 | * 通过关键词获取数据列表 13 | * 14 | * @param keyWords 关键词 15 | * @param pageNo 页码 16 | * @param pageSize 每页大小 17 | * @return 数据列表,按照相关性从高到低进行排序 18 | */ 19 | Result list(String keyWords, int pageNo, int pageSize); 20 | } 21 | -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/java/com/fox/es_canal/service/impl/BlogServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.fox.es_canal.service.impl; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.fox.es_canal.dto.BlogSimpleInfoDTO; 6 | import com.fox.es_canal.entity.Result; 7 | import com.fox.es_canal.service.BlogService; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.elasticsearch.action.search.SearchRequest; 10 | import org.elasticsearch.action.search.SearchResponse; 11 | import org.elasticsearch.client.RequestOptions; 12 | import org.elasticsearch.client.RestHighLevelClient; 13 | import org.elasticsearch.common.text.Text; 14 | import org.elasticsearch.index.query.BoolQueryBuilder; 15 | import org.elasticsearch.index.query.MultiMatchQueryBuilder; 16 | import org.elasticsearch.index.query.QueryBuilders; 17 | import org.elasticsearch.search.SearchHit; 18 | import org.elasticsearch.search.SearchHits; 19 | import org.elasticsearch.search.builder.SearchSourceBuilder; 20 | import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; 21 | import org.elasticsearch.search.fetch.subphase.highlight.HighlightField; 22 | import org.springframework.beans.factory.annotation.Value; 23 | import org.springframework.stereotype.Service; 24 | import org.springframework.util.StringUtils; 25 | 26 | import javax.annotation.Resource; 27 | import java.io.IOException; 28 | import java.time.LocalDateTime; 29 | import java.time.format.DateTimeFormatter; 30 | import java.util.ArrayList; 31 | import java.util.HashMap; 32 | import java.util.List; 33 | import java.util.Map; 34 | 35 | /** 36 | * @author 狐狸半面添 37 | * @create 2023-03-22 20:18 38 | */ 39 | @Slf4j 40 | @Service 41 | public class BlogServiceImpl implements BlogService { 42 | @Resource 43 | private RestHighLevelClient restHighLevelClient; 44 | @Value("${elasticsearch.blog.index}") 45 | private String blogIndexStore; 46 | @Value("${elasticsearch.blog.source_fields}") 47 | private String blogFields; 48 | 49 | 50 | public Result list(String keyWords, int pageNo, int pageSize) { 51 | if (pageNo <= 0) { 52 | pageNo = 1; 53 | } 54 | 55 | // 1.设置索引 - blog 56 | SearchRequest searchRequest = new SearchRequest(blogIndexStore); 57 | 58 | SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 59 | 60 | BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); 61 | // 2.source源字段过虑 62 | String[] sourceFieldsArray = blogFields.split(","); 63 | searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{}); 64 | 65 | // 3.关键字 66 | if (StringUtils.hasText(keyWords)) { 67 | // 哪些字段匹配关键字 68 | MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keyWords, "title", "tags", "username", "introduce", "content"); 69 | // 设置匹配占比(表示最少匹配的子句个数,例如有五个可选子句,最少的匹配个数为5*70%=3.5.向下取整为3,这就表示五个子句最少要匹配其中三个才能查到) 70 | multiMatchQueryBuilder.minimumShouldMatch("70%"); 71 | // 提升字段的Boost值 72 | multiMatchQueryBuilder.field("title", 15); 73 | multiMatchQueryBuilder.field("tags", 10); 74 | multiMatchQueryBuilder.field("introduce", 7); 75 | multiMatchQueryBuilder.field("content", 3); 76 | multiMatchQueryBuilder.field("username", 3); 77 | 78 | boolQueryBuilder.must(multiMatchQueryBuilder); 79 | } 80 | 81 | // 4.分页 82 | int start = (pageNo - 1) * pageSize; 83 | searchSourceBuilder.from(start); 84 | searchSourceBuilder.size(pageSize); 85 | 86 | // 布尔查询 87 | searchSourceBuilder.query(boolQueryBuilder); 88 | 89 | // 6.高亮设置 90 | HighlightBuilder highlightBuilder = new HighlightBuilder(); 91 | highlightBuilder.preTags(""); 92 | highlightBuilder.postTags(""); 93 | // 设置高亮字段 94 | ArrayList fields = new ArrayList<>(); 95 | fields.add(new HighlightBuilder.Field("title")); 96 | fields.add(new HighlightBuilder.Field("introduce")); 97 | fields.add(new HighlightBuilder.Field("username")); 98 | highlightBuilder.fields().addAll(fields); 99 | searchSourceBuilder.highlighter(highlightBuilder); 100 | 101 | // 请求搜索 102 | searchRequest.source(searchSourceBuilder); 103 | 104 | SearchResponse searchResponse; 105 | try { 106 | searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); 107 | } catch (IOException e) { 108 | log.error("博客搜索异常:{}", e.getMessage()); 109 | return Result.error(e.getMessage()); 110 | } 111 | 112 | // 结果集处理 113 | SearchHits hits = searchResponse.getHits(); 114 | SearchHit[] searchHits = hits.getHits(); 115 | // 记录总数 116 | long totalHitsCount = hits.getTotalHits().value; 117 | // 数据列表 118 | List list = new ArrayList<>(); 119 | 120 | for (SearchHit hit : searchHits) { 121 | 122 | JSONObject jsonObject = JSONObject.parseObject(hit.getSourceAsString()); 123 | BlogSimpleInfoDTO blog = new BlogSimpleInfoDTO(); 124 | blog.setId(Integer.parseInt(hit.getId())); 125 | blog.setUsername(jsonObject.getString("username")); 126 | blog.setTitle(jsonObject.getString("title")); 127 | blog.setUserId(Long.parseLong(jsonObject.getString("userId"))); 128 | blog.setUserIcon(jsonObject.getString("userIcon")); 129 | blog.setIntroduce(jsonObject.getString("introduce")); 130 | blog.setCreateTime(LocalDateTime.parse(jsonObject.getString("createTime"), DateTimeFormatter.ISO_OFFSET_DATE_TIME)); 131 | blog.setUpdateTime(LocalDateTime.parse(jsonObject.getString("updateTime"), DateTimeFormatter.ISO_OFFSET_DATE_TIME)); 132 | 133 | // 取出高亮字段内容 134 | Map highlightFields = hit.getHighlightFields(); 135 | if (highlightFields != null) { 136 | blog.setTitle(parseHighlightStr(blog.getTitle(), highlightFields.get("title"))); 137 | blog.setIntroduce(parseHighlightStr(blog.getIntroduce(), highlightFields.get("introduce"))); 138 | blog.setUsername(parseHighlightStr(blog.getUsername(), highlightFields.get("username"))); 139 | } 140 | 141 | list.add(blog); 142 | } 143 | 144 | // 封装信息返回前端 145 | HashMap resultMap = new HashMap<>(4); 146 | // 页码 147 | resultMap.put("pageNo", pageNo); 148 | // 每页记录数量 149 | resultMap.put("pageSize", pageSize); 150 | // 总记录数 151 | resultMap.put("total", totalHitsCount); 152 | // 该页信息 153 | resultMap.put("items", list); 154 | 155 | return Result.ok(resultMap); 156 | 157 | } 158 | 159 | public String parseHighlightStr(String text, HighlightField field) { 160 | if (field != null) { 161 | Text[] fragments = field.getFragments(); 162 | StringBuilder stringBuilder = new StringBuilder(); 163 | for (Text str : fragments) { 164 | stringBuilder.append(str.string()); 165 | } 166 | return stringBuilder.toString(); 167 | } else { 168 | return text; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /elasticsearch-canal-demo/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | # 服务端口 3 | port: 9999 4 | elasticsearch: 5 | # es访问ip 6 | hostname: 192.168.65.133 7 | # es访问port 8 | port: 9200 9 | blog: 10 | # 访问索引 11 | index: es_demo_collect 12 | # 搜索返回字段 13 | source_fields: userId,title,username,userIcon,introduce,createTime,updateTime -------------------------------------------------------------------------------- /elasticsearch-demo/doc/mapping.kibana: -------------------------------------------------------------------------------- 1 | PUT /blog 2 | { 3 | "settings": { 4 | "number_of_shards": 1, 5 | "number_of_replicas": 0 6 | }, 7 | "mappings": { 8 | "properties": { 9 | "id": { 10 | "type": "keyword" 11 | }, 12 | "userId": { 13 | "type": "long" 14 | }, 15 | "title": { 16 | "analyzer": "ik_max_word", 17 | "search_analyzer": "ik_smart", 18 | "type": "text" 19 | }, 20 | "tags": { 21 | "analyzer": "ik_max_word", 22 | "search_analyzer": "ik_smart", 23 | "type": "text" 24 | }, 25 | "introduce":{ 26 | "analyzer": "ik_max_word", 27 | "search_analyzer": "ik_smart", 28 | "type": "text" 29 | }, 30 | "content":{ 31 | "analyzer": "ik_max_word", 32 | "search_analyzer": "ik_smart", 33 | "type": "text" 34 | }, 35 | "createTime":{ 36 | "format": "yyyy-MM-dd HH:mm:ss", 37 | "type": "date" 38 | }, 39 | "updateTime":{ 40 | "format": "yyyy-MM-dd HH:mm:ss", 41 | "type": "date" 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /elasticsearch-demo/doc/table.sql: -------------------------------------------------------------------------------- 1 | # 如果存在 es_demo 数据库则删除 2 | DROP DATABASE IF EXISTS `es_demo`; 3 | # 创建新数据库 4 | CREATE DATABASE `es_demo`; 5 | 6 | # 创建一张博客表 7 | CREATE TABLE `blog`( 8 | `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键id', 9 | `user_id` BIGINT NOT NULL COMMENT '用户id(雪花算法生成)', 10 | `title` VARCHAR(255) NOT NULL COMMENT '标题', 11 | `tags` VARCHAR(64) NOT NULL COMMENT '标签', 12 | `introduce` VARCHAR(512) NOT NULL COMMENT '介绍', 13 | `content` TEXT NOT NULL COMMENT '文章内容', 14 | `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 15 | `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 16 | PRIMARY KEY(`id`), 17 | KEY `idx_user_create`(`user_id`,`create_time` DESC) 18 | )ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='博客信息表'; -------------------------------------------------------------------------------- /elasticsearch-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.fox 8 | elasticsearch-demo 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 8 13 | 8 14 | UTF-8 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-web 21 | 2.6.3 22 | 23 | 24 | org.projectlombok 25 | lombok 26 | 1.18.24 27 | 28 | 29 | org.elasticsearch.client 30 | elasticsearch-rest-high-level-client 31 | 7.8.0 32 | 33 | 34 | 35 | com.alibaba 36 | fastjson 37 | 1.2.33 38 | 39 | 40 | cn.hutool 41 | hutool-all 42 | 5.8.7 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /elasticsearch-demo/src/main/java/com/fox/es/ElasticsearchDemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.fox.es; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author 狐狸半面添 8 | * @create 2023-03-22 18:07 9 | */ 10 | @SpringBootApplication 11 | public class ElasticsearchDemoApplication { 12 | public static void main(String[] args) { 13 | SpringApplication.run(ElasticsearchDemoApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /elasticsearch-demo/src/main/java/com/fox/es/config/ElasticsearchConfig.java: -------------------------------------------------------------------------------- 1 | package com.fox.es.config; 2 | 3 | import org.apache.http.HttpHost; 4 | import org.elasticsearch.client.RestClient; 5 | import org.elasticsearch.client.RestClientBuilder; 6 | import org.elasticsearch.client.RestHighLevelClient; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | /** 12 | * @author 狐狸半面添 13 | * @create 2023-03-22 17:51 14 | */ 15 | @Configuration 16 | public class ElasticsearchConfig { 17 | @Value("${elasticsearch.hostname}") 18 | private String hostname; 19 | @Value("${elasticsearch.port}") 20 | private Integer port; 21 | 22 | @Bean 23 | public RestHighLevelClient restHighLevelClient() { 24 | RestClientBuilder builder = RestClient.builder( 25 | new HttpHost(hostname, port, "http") 26 | ); 27 | return new RestHighLevelClient(builder); 28 | } 29 | } -------------------------------------------------------------------------------- /elasticsearch-demo/src/main/java/com/fox/es/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.fox.es.config; 2 | 3 | import com.fox.es.entity.JacksonObjectMapper; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.converter.HttpMessageConverter; 6 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * @author 狐狸半面添 13 | * @create 2023-02-11 23:25 14 | */ 15 | 16 | @Configuration 17 | public class WebMvcConfig extends WebMvcConfigurationSupport { 18 | 19 | @Override 20 | protected void extendMessageConverters(List> converters) { 21 | MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(); 22 | messageConverter.setObjectMapper(new JacksonObjectMapper()); 23 | converters.add(0, messageConverter); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /elasticsearch-demo/src/main/java/com/fox/es/controller/BlogController.java: -------------------------------------------------------------------------------- 1 | package com.fox.es.controller; 2 | 3 | import com.fox.es.entity.Result; 4 | import com.fox.es.service.BlogService; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import javax.annotation.Resource; 11 | 12 | /** 13 | * @author 狐狸半面添 14 | * @create 2023-03-22 20:16 15 | */ 16 | @RestController 17 | @RequestMapping("/blog") 18 | public class BlogController { 19 | 20 | @Resource 21 | private BlogService blogService; 22 | 23 | /** 24 | * 通过关键词获取数据列表 25 | * 26 | * @param keyWords 关键词 27 | * @param pageNo 页码 28 | * @param pageSize 每页大小 29 | * @return 数据列表,按照相关性从高到低进行排序 30 | */ 31 | @GetMapping("/list") 32 | public Result list(@RequestParam("keyWords") String keyWords, 33 | @RequestParam("pageNo") Integer pageNo, 34 | @RequestParam("pageSize") Integer pageSize) { 35 | return blogService.list(keyWords, pageNo, pageSize); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /elasticsearch-demo/src/main/java/com/fox/es/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package com.fox.es.controller; 2 | 3 | import com.fox.es.entity.Result; 4 | import org.elasticsearch.client.RequestOptions; 5 | import org.elasticsearch.client.RestHighLevelClient; 6 | import org.elasticsearch.client.core.MainResponse; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import javax.annotation.Resource; 11 | import java.io.IOException; 12 | 13 | /** 14 | * @author 狐狸半面添 15 | * @create 2023-03-22 18:33 16 | */ 17 | @RestController 18 | public class TestController { 19 | @Resource 20 | private RestHighLevelClient restHighLevelClient; 21 | 22 | /** 23 | * 用于测试是否连接 es 成功 24 | * 25 | * @return 返回 es 的基本信息,等价于访问:http://127.0.0.1:9200 26 | * @throws IOException 异常信息 27 | */ 28 | @GetMapping("/getEsInfo") 29 | public Result getEsInfo() throws IOException { 30 | MainResponse info = restHighLevelClient.info(RequestOptions.DEFAULT); 31 | return Result.ok(info); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /elasticsearch-demo/src/main/java/com/fox/es/dto/BlogSimpleInfoDTO.java: -------------------------------------------------------------------------------- 1 | package com.fox.es.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | /** 11 | * @author 狐狸半面添 12 | * @create 2023-03-22 21:33 13 | */ 14 | @Data 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class BlogSimpleInfoDTO { 18 | /** 19 | * 主键id 20 | */ 21 | private Integer id; 22 | /** 23 | * 用户id 24 | */ 25 | private Long userId; 26 | /** 27 | * 标题 28 | */ 29 | private String title; 30 | /** 31 | * 介绍 32 | */ 33 | private String introduce; 34 | /** 35 | * 创建时间 36 | */ 37 | private LocalDateTime createTime; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /elasticsearch-demo/src/main/java/com/fox/es/entity/JacksonObjectMapper.java: -------------------------------------------------------------------------------- 1 | package com.fox.es.entity; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.module.SimpleModule; 5 | import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; 6 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; 7 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; 8 | import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; 9 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; 10 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; 11 | import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; 12 | 13 | import java.math.BigInteger; 14 | import java.time.LocalDate; 15 | import java.time.LocalDateTime; 16 | import java.time.LocalTime; 17 | import java.time.format.DateTimeFormatter; 18 | 19 | import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; 20 | 21 | /** 22 | * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象 23 | * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象] 24 | * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON] 25 | * 26 | * @author 狐狸半面添 27 | * @create 2023-01-18 20:34 28 | */ 29 | public class JacksonObjectMapper extends ObjectMapper { 30 | 31 | public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; 32 | public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; 33 | public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; 34 | 35 | public JacksonObjectMapper() { 36 | super(); 37 | //收到未知属性时不报异常 38 | this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); 39 | 40 | //反序列化时,属性不存在的兼容处理 41 | this.getDeserializationConfig().withoutFeatures(FAIL_ON_UNKNOWN_PROPERTIES); 42 | 43 | 44 | SimpleModule simpleModule = new SimpleModule() 45 | .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) 46 | .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) 47 | .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) 48 | 49 | .addSerializer(BigInteger.class, ToStringSerializer.instance) 50 | .addSerializer(Long.class, ToStringSerializer.instance) 51 | .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) 52 | .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) 53 | .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); 54 | //注册功能模块 例如,可以添加自定义序列化器和反序列化器 55 | this.registerModule(simpleModule); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /elasticsearch-demo/src/main/java/com/fox/es/entity/Result.java: -------------------------------------------------------------------------------- 1 | package com.fox.es.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Data; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | /** 10 | * 统一返回对象 11 | * 12 | * @author 狐狸半面添 13 | * @create 2023-03-22 18:34 14 | */ 15 | @Getter 16 | @Setter 17 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 18 | public class Result { 19 | private Integer code; 20 | private String msg; 21 | private Object data; 22 | 23 | private Result(Integer code, String msg, Object data) { 24 | this.code = code; 25 | this.msg = msg; 26 | this.data = data; 27 | } 28 | 29 | private Result(Integer code, String msg) { 30 | this.code = code; 31 | this.msg = msg; 32 | } 33 | 34 | private Result(){} 35 | 36 | public static Result ok() { 37 | return new Result(200, "success"); 38 | } 39 | 40 | public static Result ok(Object data) { 41 | return new Result(200, "success", data); 42 | } 43 | 44 | public static Result error(String msg) { 45 | return new Result(500, msg); 46 | } 47 | 48 | public static Result error(Integer code, String msg) { 49 | return new Result(code, msg); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /elasticsearch-demo/src/main/java/com/fox/es/service/BlogService.java: -------------------------------------------------------------------------------- 1 | package com.fox.es.service; 2 | 3 | import com.fox.es.entity.Result; 4 | 5 | /** 6 | * @author 狐狸半面添 7 | * @create 2023-03-22 20:18 8 | */ 9 | public interface BlogService { 10 | 11 | /** 12 | * 通过关键词获取数据列表 13 | * 14 | * @param keyWords 关键词 15 | * @param pageNo 页码 16 | * @param pageSize 每页大小 17 | * @return 数据列表,按照相关性从高到低进行排序 18 | */ 19 | Result list(String keyWords, int pageNo, int pageSize); 20 | } 21 | -------------------------------------------------------------------------------- /elasticsearch-demo/src/main/java/com/fox/es/service/impl/BlogServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.fox.es.service.impl; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.fox.es.dto.BlogSimpleInfoDTO; 5 | import com.fox.es.entity.Result; 6 | import com.fox.es.service.BlogService; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.elasticsearch.action.search.SearchRequest; 9 | import org.elasticsearch.action.search.SearchResponse; 10 | import org.elasticsearch.client.RequestOptions; 11 | import org.elasticsearch.client.RestHighLevelClient; 12 | import org.elasticsearch.common.text.Text; 13 | import org.elasticsearch.index.query.BoolQueryBuilder; 14 | import org.elasticsearch.index.query.MultiMatchQueryBuilder; 15 | import org.elasticsearch.index.query.QueryBuilders; 16 | import org.elasticsearch.search.SearchHit; 17 | import org.elasticsearch.search.SearchHits; 18 | import org.elasticsearch.search.builder.SearchSourceBuilder; 19 | import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; 20 | import org.elasticsearch.search.fetch.subphase.highlight.HighlightField; 21 | import org.springframework.beans.factory.annotation.Value; 22 | import org.springframework.stereotype.Service; 23 | import org.springframework.util.StringUtils; 24 | 25 | import javax.annotation.Resource; 26 | import java.io.IOException; 27 | import java.util.ArrayList; 28 | import java.util.HashMap; 29 | import java.util.List; 30 | import java.util.Map; 31 | 32 | /** 33 | * @author 狐狸半面添 34 | * @create 2023-03-22 20:18 35 | */ 36 | @Slf4j 37 | @Service 38 | public class BlogServiceImpl implements BlogService { 39 | @Resource 40 | private RestHighLevelClient restHighLevelClient; 41 | @Value("${elasticsearch.blog.index}") 42 | private String blogIndexStore; 43 | @Value("${elasticsearch.blog.source_fields}") 44 | private String blogFields; 45 | 46 | 47 | public Result list(String keyWords, int pageNo, int pageSize) { 48 | // 1.设置索引 - blog 49 | SearchRequest searchRequest = new SearchRequest(blogIndexStore); 50 | 51 | SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 52 | 53 | BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); 54 | // 2.source源字段过虑 55 | String[] sourceFieldsArray = blogFields.split(","); 56 | searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{}); 57 | 58 | // 3.关键字 59 | if (StringUtils.hasText(keyWords)) { 60 | // 哪些字段匹配关键字 61 | MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keyWords, "title", "tags", "introduce", "content"); 62 | // 设置匹配占比(表示最少匹配的子句个数,例如有五个可选子句,最少的匹配个数为5*70%=3.5.向下取整为3,这就表示五个子句最少要匹配其中三个才能查到) 63 | multiMatchQueryBuilder.minimumShouldMatch("70%"); 64 | // 提升字段的Boost值 65 | multiMatchQueryBuilder.field("title", 10); 66 | multiMatchQueryBuilder.field("tags", 7); 67 | multiMatchQueryBuilder.field("introduce", 5); 68 | 69 | boolQueryBuilder.must(multiMatchQueryBuilder); 70 | } 71 | 72 | // 4.分页 73 | int start = (pageNo - 1) * pageSize; 74 | searchSourceBuilder.from(start); 75 | searchSourceBuilder.size(pageSize); 76 | 77 | // 布尔查询 78 | searchSourceBuilder.query(boolQueryBuilder); 79 | 80 | // 6.高亮设置 81 | HighlightBuilder highlightBuilder = new HighlightBuilder(); 82 | highlightBuilder.preTags(""); 83 | highlightBuilder.postTags(""); 84 | // 设置高亮字段 85 | ArrayList fields = new ArrayList<>(); 86 | fields.add(new HighlightBuilder.Field("title")); 87 | fields.add(new HighlightBuilder.Field("introduce")); 88 | highlightBuilder.fields().addAll(fields); 89 | searchSourceBuilder.highlighter(highlightBuilder); 90 | 91 | // 请求搜索 92 | searchRequest.source(searchSourceBuilder); 93 | 94 | SearchResponse searchResponse; 95 | try { 96 | searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); 97 | } catch (IOException e) { 98 | log.error("博客搜索异常:{}", e.getMessage()); 99 | return Result.error(e.getMessage()); 100 | } 101 | 102 | // 结果集处理 103 | SearchHits hits = searchResponse.getHits(); 104 | SearchHit[] searchHits = hits.getHits(); 105 | // 记录总数 106 | long totalHitsCount = hits.getTotalHits().value; 107 | // 数据列表 108 | List list = new ArrayList<>(); 109 | 110 | for (SearchHit hit : searchHits) { 111 | 112 | String sourceAsString = hit.getSourceAsString(); 113 | // json 转 对象 114 | BlogSimpleInfoDTO blog = JSON.parseObject(sourceAsString, BlogSimpleInfoDTO.class); 115 | 116 | // 取出高亮字段内容 117 | Map highlightFields = hit.getHighlightFields(); 118 | if (highlightFields != null) { 119 | blog.setTitle(parseHighlightStr(blog.getTitle(), highlightFields.get("title"))); 120 | blog.setIntroduce(parseHighlightStr(blog.getIntroduce(), highlightFields.get("introduce"))); 121 | } 122 | 123 | list.add(blog); 124 | } 125 | 126 | // 封装信息返回前端 127 | HashMap resultMap = new HashMap<>(4); 128 | // 页码 129 | resultMap.put("pageNo", pageNo); 130 | // 每页记录数量 131 | resultMap.put("pageSize", pageSize); 132 | // 总记录数 133 | resultMap.put("total", totalHitsCount); 134 | // 该页信息 135 | resultMap.put("items", list); 136 | 137 | return Result.ok(resultMap); 138 | 139 | } 140 | 141 | public String parseHighlightStr(String text, HighlightField field) { 142 | if (field != null) { 143 | Text[] fragments = field.getFragments(); 144 | StringBuilder stringBuilder = new StringBuilder(); 145 | for (Text str : fragments) { 146 | stringBuilder.append(str.string()); 147 | } 148 | return stringBuilder.toString(); 149 | } else { 150 | return text; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /elasticsearch-demo/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | # 服务端口 3 | port: 9999 4 | elasticsearch: 5 | # es访问ip 6 | hostname: 127.0.0.1 7 | # es访问port 8 | port: 9200 9 | blog: 10 | # 访问索引 11 | index: blog 12 | # 搜索返回字段 13 | source_fields: id,userId,title,introduce,createTime -------------------------------------------------------------------------------- /elasticsearch-demo/src/test/java/com/fox/es/CommonTest.java: -------------------------------------------------------------------------------- 1 | package com.fox.es; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.fox.es.dto.BlogSimpleInfoDTO; 5 | 6 | /** 7 | * @author 狐狸半面添 8 | * @create 2023-03-23 1:04 9 | */ 10 | public class CommonTest { 11 | public static void main(String[] args) { 12 | String json = "{\"createTime\":\"2023-03-23 00:40:20\",\"introduce\":\"Java的起源\",\"id\":1000,\"title\":\"Java语言\",\"userId\":1626989073847750657}"; 13 | BlogSimpleInfoDTO blogSimpleInfoDTO = JSON.parseObject(json, BlogSimpleInfoDTO.class); 14 | System.out.println(blogSimpleInfoDTO); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /minio-chunk-demo/README.md: -------------------------------------------------------------------------------- 1 | 1.为了简单操作,这里只设计了一张表,即存放文件的表,放在了doc文件夹下,自行参考 2 | 3 | 2.代码的具体实现思路逻辑参考我的文章:[minio&前后端分离上传视频/上传大文件——前后端分离断点续传&minio分片上传实现](https://blog.csdn.net/qq_62982856/article/details/129002288) 4 | 5 | 3.MediaFileServiceImpl.java 中的uploadMergeChunks方法,里面有向数据库增加文件记录的操作,设置上传者id时我设置为了定值,实际开发中应当根据token拿到当前用户的Id。 -------------------------------------------------------------------------------- /minio-chunk-demo/doc/table.sql: -------------------------------------------------------------------------------- 1 | create database minio_demo; 2 | 3 | CREATE TABLE service_media_file 4 | ( 5 | `id` BIGINT UNSIGNED PRIMARY KEY COMMENT '主键id(雪花算法)', 6 | `file_name` VARCHAR(255) NOT NULL COMMENT '文件名称', 7 | `file_type` CHAR(2) NOT NULL COMMENT '文件类型:文本,图片,音频,视频,其它', 8 | `file_format` VARCHAR(128) NOT NULL COMMENT '文件格式', 9 | `tag` VARCHAR(32) NOT NULL COMMENT '标签', 10 | `bucket` VARCHAR(32) NOT NULL COMMENT '存储桶', 11 | `file_path` VARCHAR(512) NOT NULL COMMENT '文件存储路径', 12 | `file_md5` CHAR(32) NOT NULL UNIQUE COMMENT '文件的md5值', 13 | `file_byte_size` BIGINT UNSIGNED NOT NULL COMMENT '文件的字节大小', 14 | `file_format_size` VARCHAR(24) NOT NULL COMMENT '文件的格式大小', 15 | `user_id` BIGINT NOT NULL COMMENT '上传人id', 16 | `create_time` DATETIME NOT NULL COMMENT '创建时间(上传时间)', 17 | `update_time` DATETIME NOT NULL COMMENT '修改时间' 18 | ) ENGINE = INNODB 19 | CHARACTER SET = utf8mb4 COMMENT '第三方服务-媒资文件表'; -------------------------------------------------------------------------------- /minio-chunk-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.fox 8 | minio-chunk-demo 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | 8 14 | 8 15 | UTF-8 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-web 22 | 2.6.3 23 | 24 | 25 | org.projectlombok 26 | lombok 27 | 1.18.24 28 | 29 | 30 | com.baomidou 31 | mybatis-plus-boot-starter 32 | 3.5.2 33 | 34 | 35 | 36 | com.alibaba 37 | fastjson 38 | 1.2.33 39 | 40 | 41 | cn.hutool 42 | hutool-all 43 | 5.8.7 44 | 45 | 46 | 47 | 48 | mysql 49 | mysql-connector-java 50 | 8.0.30 51 | 52 | 53 | 54 | com.alibaba 55 | druid-spring-boot-starter 56 | 1.2.15 57 | 58 | 59 | 60 | org.aspectj 61 | aspectjweaver 62 | 1.9.7 63 | 64 | 65 | 66 | 67 | javax.validation 68 | validation-api 69 | 2.0.1.Final 70 | 71 | 72 | 73 | 74 | com.j256.simplemagic 75 | simplemagic 76 | 1.17 77 | 78 | 79 | 80 | io.minio 81 | minio 82 | 8.2.1 83 | 84 | 85 | me.tongfei 86 | progressbar 87 | 0.5.3 88 | 89 | 90 | com.squareup.okhttp3 91 | okhttp 92 | 4.8.1 93 | 94 | 95 | 96 | commons-codec 97 | commons-codec 98 | 1.13 99 | 100 | 101 | 102 | org.apache.tika 103 | tika-core 104 | 2.4.0 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/MinioChunkDemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 6 | 7 | /** 8 | * 启动类 9 | * EnableAspectJAutoProxy 暴露代理对象 10 | * 11 | * @author 狐狸半面添 12 | * @create 2023-01-16 17:03 13 | */ 14 | @SpringBootApplication 15 | @EnableAspectJAutoProxy(exposeProxy = true) 16 | public class MinioChunkDemoApplication { 17 | public static void main(String[] args) { 18 | SpringApplication.run(MinioChunkDemoApplication.class, args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/config/MinioConfig.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.config; 2 | 3 | import io.minio.MinioClient; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * @author 狐狸半面添 10 | * @create 2023-02-08 16:26 11 | */ 12 | @Configuration 13 | public class MinioConfig { 14 | 15 | /** 16 | * 连接的ip和端口 17 | */ 18 | @Value("${minio.endpoint}") 19 | private String endpoint; 20 | /** 21 | * 访问秘钥(也称用户id) 22 | */ 23 | @Value("${minio.accessKey}") 24 | private String accessKey; 25 | /** 26 | * 私有秘钥(也称密码) 27 | */ 28 | @Value("${minio.secretKey}") 29 | private String secretKey; 30 | 31 | @Bean 32 | public MinioClient minioClient() { 33 | return MinioClient.builder() 34 | .endpoint(endpoint) 35 | .credentials(accessKey, secretKey) 36 | .build(); 37 | } 38 | } -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/config/MyBatisPlusConfig.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.config; 2 | 3 | import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; 4 | import com.fox.miniodemo.handler.MyMetaObjectHandler; 5 | import org.mybatis.spring.annotation.MapperScan; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.transaction.annotation.EnableTransactionManagement; 9 | 10 | /** 11 | * 与ORM框架mybatis-plus相关的配置 12 | * EnableTransactionManagement 开启事务 13 | * 14 | * @author 狐狸半面添 15 | * @create 2023-01-15 22:40 16 | */ 17 | @Configuration 18 | @EnableTransactionManagement 19 | @MapperScan("com.fox.miniodemo.dao") 20 | public class MyBatisPlusConfig { 21 | @Bean 22 | public MetaObjectHandler metaObjectHandler(){ 23 | return new MyMetaObjectHandler(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/constant/HttpStatus.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.constant; 2 | 3 | /** 4 | * Http状态码响应常量 5 | * 6 | * @author 狐狸半面添 7 | * @create 2023-01-17 16:46 8 | */ 9 | public enum HttpStatus { 10 | /** 11 | * 请求处理成功 12 | */ 13 | HTTP_OK(200,"成功"), 14 | /** 15 | * 请求报文语法错误或传入后端的参数错误(格式,范围等) 16 | */ 17 | HTTP_BAD_REQUEST(400,"参数校验失败"), 18 | /** 19 | * 需要通过HTTP认证,或认证失败 20 | */ 21 | HTTP_UNAUTHORIZED(401,"认证失败"), 22 | /** 23 | * 请求资源被拒绝,权限不足 24 | */ 25 | HTTP_FORBIDDEN(403,"权限不足"), 26 | /** 27 | * 无法找到请求资源(服务器无理由拒绝) 28 | */ 29 | HTTP_NOT_FOUND( 404,"无法找到请求资源"), 30 | /** 31 | * 服务器故障或Web应用故障 32 | */ 33 | HTTP_INTERNAL_ERROR( 500,"服务器异常"), 34 | /** 35 | * 非法操作,对服务器的恶意请求或攻击 36 | */ 37 | HTTP_ILLEGAL_OPERATION(700,"非法操作"), 38 | /** 39 | * 操作频繁,需要稍后再试 40 | */ 41 | HTTP_TRY_AGAIN_LATER (701,"频繁操作,请稍后再试"), 42 | /** 43 | * 登录过期,需要重新登录 44 | */ 45 | HTTP_LOGIN_EXPIRE (702,"登录过期,请重新登录"), 46 | /** 47 | * 账号在其它地方登录,强制下线 48 | */ 49 | HTTP_USER_CROWDING(703,"账号在其它地方登录,您已被强制下线"), 50 | 51 | /** 52 | * 校验失败 53 | */ 54 | HTTP_VERIFY_FAIL(704,"验证失败"), 55 | 56 | /** 57 | * 未查询到相关信息 58 | */ 59 | HTTP_INFO_NOT_EXIST(705,"信息不存在"), 60 | 61 | /** 62 | * 信息被拒绝获取 63 | */ 64 | HTTP_INFO_REFUSE(706,"被拒绝获取"), 65 | /** 66 | * 操作重复,该操作之前已生效 67 | */ 68 | HTTP_REPEAT_SUCCESS_OPERATE(707,"重复已生效的操作"); 69 | 70 | private final int code; 71 | private final String value; 72 | 73 | private HttpStatus(int code, String value) { 74 | this.code = code; 75 | this.value = value; 76 | } 77 | 78 | public int getCode() { 79 | return code; 80 | } 81 | 82 | public String getValue() { 83 | return value; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/controller/MediaFileController.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.controller; 2 | 3 | 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.fox.miniodemo.entity.Result; 6 | import com.fox.miniodemo.service.MediaFileService; 7 | import com.fox.miniodemo.vo.CheckChunkFileVO; 8 | import com.fox.miniodemo.vo.UploadMergeChunksVO; 9 | import org.springframework.validation.annotation.Validated; 10 | import org.springframework.web.bind.annotation.*; 11 | import org.springframework.web.multipart.MultipartFile; 12 | 13 | import javax.annotation.Resource; 14 | 15 | /** 16 | *

17 | * 第三方服务-媒资文件表 前端控制器 18 | *

19 | * 20 | * @author 狐狸半面添 21 | * @since 2023-02-08 22 | */ 23 | @RestController 24 | @RequestMapping("/media-file") 25 | public class MediaFileController { 26 | @Resource 27 | private MediaFileService mediaFileService; 28 | 29 | 30 | /** 31 | * 文件上传前检查文件是否已存在 32 | * 33 | * @param object 需要上传的文件的md5值 34 | * @return 是否存在, false-不存在 true-存在。如果存在,则会返回文件信息。 35 | */ 36 | @PostMapping("/upload/checkFile") 37 | public Result checkFile(@RequestBody JSONObject object) { 38 | return mediaFileService.checkFile(object.getString("fileMd5")); 39 | } 40 | 41 | /** 42 | * 分块文件上传前检测分块文件是否已存在 43 | * 44 | * @param checkChunkFileVO 分块文件的源文件md5和该文件索引 45 | * @return 是否存在, false-不存在 true-存在 46 | */ 47 | @PostMapping("/upload/checkChunk") 48 | public Result checkChunk(@Validated @RequestBody CheckChunkFileVO checkChunkFileVO) { 49 | return mediaFileService.checkChunk(checkChunkFileVO.getFileMd5(), checkChunkFileVO.getChunkIndex()); 50 | } 51 | 52 | /** 53 | * 上传分块文件 54 | * 55 | * @param file 分块文件 56 | * @param fileMd5 原文件md5值 57 | * @param chunkIndex 分块文件索引 58 | * @return 上传情况 59 | */ 60 | @PostMapping("/upload/uploadChunk") 61 | public Result uploadChunk(@RequestParam("file") MultipartFile file, 62 | @RequestParam("fileMd5") String fileMd5, 63 | @RequestParam("chunkIndex") Integer chunkIndex) throws Exception { 64 | return mediaFileService.uploadChunk(fileMd5, chunkIndex, file.getBytes()); 65 | } 66 | 67 | /** 68 | * 合并分块文件 69 | * 70 | * @param uploadMergeChunksVO 文件的md5十六进制值+文件名+文件标签+分块文件总数 71 | * @return 合并与上传情况,如果成功,则返回文件信息 72 | */ 73 | @PostMapping("/upload/uploadMergeChunks") 74 | public Result uploadMergeChunks(@Validated @RequestBody UploadMergeChunksVO uploadMergeChunksVO) { 75 | 76 | return mediaFileService.uploadMergeChunks( 77 | uploadMergeChunksVO.getFileMd5(), 78 | uploadMergeChunksVO.getFileName(), 79 | uploadMergeChunksVO.getTag(), 80 | uploadMergeChunksVO.getChunkTotal() 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/dao/MediaFileMapper.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.dao; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.fox.miniodemo.po.MediaFile; 5 | 6 | /** 7 | *

8 | * 第三方服务-媒资文件表 Mapper 接口 9 | *

10 | * 11 | * @author 狐狸半面添 12 | * @since 2023-02-08 13 | */ 14 | public interface MediaFileMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/entity/Result.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.io.Serializable; 9 | 10 | import static com.fox.miniodemo.constant.HttpStatus.HTTP_INTERNAL_ERROR; 11 | import static com.fox.miniodemo.constant.HttpStatus.HTTP_OK; 12 | 13 | 14 | /** 15 | * 返回给前端的统一工具类 16 | * 17 | * @author 狐狸半面添 18 | * @create 2023-01-16 19:19 19 | */ 20 | @Getter 21 | @Setter 22 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 23 | public class Result implements Serializable { 24 | private static final long serialVersionUID = 1L; 25 | 26 | /** 27 | * 状态码 28 | */ 29 | private int code; 30 | /** 31 | * 信息 32 | */ 33 | private String msg; 34 | /** 35 | * 数据 36 | */ 37 | private Object data; 38 | 39 | private Result(int code, String msg) { 40 | this.code = code; 41 | this.msg = msg; 42 | } 43 | 44 | private Result(int code, String msg, Object data) { 45 | this.code = code; 46 | this.msg = msg; 47 | this.data = data; 48 | 49 | } 50 | private Result(){} 51 | 52 | public static Result ok() { 53 | return new Result(HTTP_OK.getCode(), HTTP_OK.getValue()); 54 | } 55 | 56 | 57 | public static Result ok(Object data) { 58 | return new Result(HTTP_OK.getCode(), HTTP_OK.getValue(), data); 59 | } 60 | 61 | public static Result ok(String msg, Object data) { 62 | return new Result(HTTP_OK.getCode(), msg, data); 63 | } 64 | 65 | public static Result error() { 66 | return new Result(HTTP_INTERNAL_ERROR.getCode(), HTTP_INTERNAL_ERROR.getValue()); 67 | } 68 | 69 | public static Result error(String msg) { 70 | return new Result(HTTP_INTERNAL_ERROR.getCode(), msg); 71 | } 72 | 73 | public static Result error(Integer code, String msg) { 74 | return new Result(code, msg); 75 | } 76 | public static Result error(String msg, Object data) { 77 | return new Result(HTTP_INTERNAL_ERROR.getCode(), msg, data); 78 | } 79 | 80 | public static Result error(int code, String msg, Object data) { 81 | return new Result(code, msg, data); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/handler/MyMetaObjectHandler.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.handler; 2 | 3 | import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; 4 | import org.apache.ibatis.reflection.MetaObject; 5 | 6 | import java.time.LocalDateTime; 7 | 8 | /** 9 | * mybatis-plus 自动填充器 10 | * 11 | * @author 狐狸半面添 12 | * @create 2023-01-18 23:14 13 | */ 14 | 15 | public class MyMetaObjectHandler implements MetaObjectHandler { 16 | 17 | @Override 18 | public void insertFill(MetaObject metaObject) { 19 | this.setFieldValByName("createTime", LocalDateTime.now(), metaObject); 20 | this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject); 21 | } 22 | 23 | @Override 24 | public void updateFill(MetaObject metaObject) { 25 | this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/po/MediaFile.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.po; 2 | 3 | import com.baomidou.mybatisplus.annotation.*; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serializable; 9 | import java.time.LocalDateTime; 10 | 11 | /** 12 | *

13 | * 第三方服务-媒资文件表 14 | *

15 | * 16 | * @author 狐狸半面添 17 | * @since 2023-02-08 18 | */ 19 | @TableName("service_media_file") 20 | @Data 21 | @AllArgsConstructor 22 | @NoArgsConstructor 23 | public class MediaFile implements Serializable { 24 | 25 | private static final long serialVersionUID = 1L; 26 | 27 | /** 28 | * 主键id(雪花算法) 29 | */ 30 | @TableId(value = "id", type = IdType.ASSIGN_ID) 31 | private Long id; 32 | 33 | /** 34 | * 文件名称 35 | */ 36 | private String fileName; 37 | 38 | /** 39 | * 文件类型:文本,图片,音频,视频,其它 40 | */ 41 | private String fileType; 42 | 43 | /** 44 | * 文件格式 45 | */ 46 | private String fileFormat; 47 | 48 | /** 49 | * 标签 50 | */ 51 | private String tag; 52 | 53 | /** 54 | * 存储桶 55 | */ 56 | private String bucket; 57 | 58 | /** 59 | * 文件存储路径 60 | */ 61 | private String filePath; 62 | 63 | /** 64 | * 文件的md5值 65 | */ 66 | private String fileMd5; 67 | 68 | /** 69 | * 文件字节大小 70 | */ 71 | private Long fileByteSize; 72 | /** 73 | * 文件格式化大小 74 | */ 75 | private String fileFormatSize; 76 | 77 | /** 78 | * 上传人id 79 | */ 80 | private Long userId; 81 | 82 | /** 83 | * 创建时间(上传时间) 84 | */ 85 | @TableField(fill = FieldFill.INSERT) 86 | private LocalDateTime createTime; 87 | 88 | /** 89 | * 修改时间 90 | */ 91 | @TableField(fill = FieldFill.INSERT_UPDATE) 92 | private LocalDateTime updateTime; 93 | 94 | } -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/service/MediaFileService.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.fox.miniodemo.entity.Result; 5 | import com.fox.miniodemo.po.MediaFile; 6 | 7 | /** 8 | *

9 | * 第三方服务-媒资文件表 服务类 10 | *

11 | * 12 | * @author 狐狸半面添 13 | * @since 2023-02-08 14 | */ 15 | public interface MediaFileService extends IService { 16 | 17 | /** 18 | * 文件上传前检查文件是否存在 19 | * 20 | * @param fileMd5 需要上传的文件的md5值 21 | * @return 是否存在, false-不存在 true-存在 22 | */ 23 | Result checkFile(String fileMd5); 24 | 25 | /** 26 | * 分块文件上传前检测分块文件是否已存在 27 | * 28 | * @param fileMd5 分块文件的源文件md5 29 | * @param chunkIndex 分块文件索引 30 | * @return 是否存在, false-不存在 true-存在 31 | */ 32 | Result checkChunk(String fileMd5, Integer chunkIndex); 33 | 34 | /** 35 | * 上传分块文件 36 | * 37 | * @param fileMd5 原文件md5值 38 | * @param chunkIndex 分块文件索引 39 | * @param bytes 分块文件的字节数组形式 40 | * @return 上传情况 41 | */ 42 | Result uploadChunk(String fileMd5, Integer chunkIndex, byte[] bytes); 43 | 44 | /** 45 | * 合并分块文件 46 | * 47 | * @param fileMd5 文件的md5十六进制值 48 | * @param fileName 文件名 49 | * @param tag 文件标签 50 | * @param chunkTotal 文件块总数 51 | * @return 合并与上传情况 52 | */ 53 | Result uploadMergeChunks(String fileMd5, String fileName, String tag, Integer chunkTotal); 54 | } 55 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/util/CipherUtils.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.util; 2 | 3 | import javax.crypto.Cipher; 4 | import javax.crypto.KeyGenerator; 5 | import javax.crypto.SecretKey; 6 | import javax.crypto.spec.SecretKeySpec; 7 | import java.security.SecureRandom; 8 | import java.util.Base64; 9 | 10 | /** 11 | * AES加密解密工具 12 | * 13 | * @author 狐狸半面添 14 | * @create 2023-01-18 20:34 15 | */ 16 | public class CipherUtils { 17 | 18 | private static final String SECRET_KEY = "tangyulang5201314"; 19 | private static final String AES = "AES"; 20 | private static final String CHARSET_NAME = "UTF-8"; 21 | 22 | /** 23 | * 生成密钥 key 24 | * 25 | * @param password 加密密码 26 | * @return 27 | * @throws Exception 28 | */ 29 | private static SecretKeySpec generateKey(String password) throws Exception { 30 | // 1.构造密钥生成器,指定为AES算法,不区分大小写 31 | KeyGenerator keyGenerator = KeyGenerator.getInstance(AES); 32 | // 2. 因为AES要求密钥的长度为128,我们需要固定的密码,因此随机源的种子需要设置为我们的密码数组 33 | // 生成一个128位的随机源, 根据传入的字节数组 34 | /* 35 | * 这种方式 windows 下正常, Linux 环境下会解密失败 36 | * keyGenerator.init(128, new SecureRandom(password.getBytes())); 37 | */ 38 | // 兼容 Linux 39 | SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); 40 | random.setSeed(password.getBytes()); 41 | keyGenerator.init(128, random); 42 | // 3.产生原始对称密钥 43 | SecretKey original_key = keyGenerator.generateKey(); 44 | // 4. 根据字节数组生成AES密钥 45 | return new SecretKeySpec(original_key.getEncoded(), AES); 46 | } 47 | 48 | /** 49 | * 加密 50 | * 51 | * @param content 加密的内容 52 | * @param password 加密密码 53 | * @return 54 | */ 55 | private static String aESEncode(String content, String password) { 56 | try { 57 | // 根据指定算法AES自成密码器 58 | Cipher cipher = Cipher.getInstance(AES); 59 | // 基于加密模式和密钥初始化Cipher 60 | cipher.init(Cipher.ENCRYPT_MODE, generateKey(password)); 61 | // 单部分加密结束, 重置Cipher, 获取加密内容的字节数组(这里要设置为UTF-8)防止解密为乱码 62 | byte[] bytes = cipher.doFinal(content.getBytes(CHARSET_NAME)); 63 | // 将加密后的字节数组转为字符串返回 64 | return Base64.getUrlEncoder().encodeToString(bytes); 65 | } catch (Exception e) { 66 | // 如果有错就返回 null 67 | return null; 68 | } 69 | } 70 | 71 | /** 72 | * 解密 73 | * 74 | * @param content 解密内容 75 | * @param password 解密密码 76 | * @return 77 | */ 78 | private static String AESDecode(String content, String password) { 79 | try { 80 | // 将加密并编码后的内容解码成字节数组 81 | byte[] bytes = Base64.getUrlDecoder().decode(content); 82 | // 这里指定了算法为AES 83 | Cipher cipher = Cipher.getInstance(AES); 84 | // 基于解密模式和密钥初始化Cipher 85 | cipher.init(Cipher.DECRYPT_MODE, generateKey(password)); 86 | // 单部分加密结束,重置Cipher 87 | byte[] result = cipher.doFinal(bytes); 88 | // 将解密后的字节数组转成 UTF-8 编码的字符串返回 89 | return new String(result, CHARSET_NAME); 90 | } catch (Exception e) { 91 | // 如果有错就返回 null 92 | return null; 93 | } 94 | 95 | 96 | } 97 | 98 | /** 99 | * 加密 100 | * 101 | * @param content 加密内容 102 | * @return 加密结果 103 | */ 104 | public static String encrypt(String content) { 105 | return aESEncode(content, SECRET_KEY); 106 | } 107 | 108 | /** 109 | * 解密 110 | * 111 | * @param content 解密内容 112 | * @return 解密结果 113 | */ 114 | public static String decrypt(String content) { 115 | try { 116 | return AESDecode(content, SECRET_KEY); 117 | } catch (Exception e) { 118 | return null; 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/util/FileFormatUtils.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.util; 2 | 3 | import java.text.DecimalFormat; 4 | 5 | /** 6 | * 文件格式工具 7 | * 8 | * @author 狐狸半面添 9 | * @create 2023-02-09 19:43 10 | */ 11 | public class FileFormatUtils { 12 | /** 13 | * 将字节单位的文件大小转为格式化的文件大小表示 14 | * 15 | * @param fileLength 文件字节大小 16 | * @return 格式化文件大小表示 17 | */ 18 | public static String formatFileSize(long fileLength) { 19 | DecimalFormat df = new DecimalFormat("#.00"); 20 | String fileSizeString = ""; 21 | String wrongSize = "0B"; 22 | if (fileLength == 0) { 23 | return wrongSize; 24 | } 25 | if (fileLength < 1024) { 26 | fileSizeString = df.format((double) fileLength) + " B"; 27 | } else if (fileLength < 1048576) { 28 | fileSizeString = df.format((double) fileLength / 1024) + " KB"; 29 | } else if (fileLength < 1073741824) { 30 | fileSizeString = df.format((double) fileLength / 1048576) + " MB"; 31 | } else { 32 | fileSizeString = df.format((double) fileLength / 1073741824) + " GB"; 33 | } 34 | return fileSizeString; 35 | } 36 | } -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/util/FileTypeUtils.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.util; 2 | 3 | import org.apache.tika.metadata.HttpHeaders; 4 | import org.apache.tika.metadata.Metadata; 5 | import org.apache.tika.metadata.TikaCoreProperties; 6 | import org.apache.tika.mime.MediaType; 7 | import org.apache.tika.parser.AutoDetectParser; 8 | import org.apache.tika.parser.ParseContext; 9 | import org.apache.tika.parser.Parser; 10 | import org.xml.sax.helpers.DefaultHandler; 11 | 12 | import java.io.File; 13 | import java.io.InputStream; 14 | import java.nio.file.Files; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | /** 19 | * 文件类型工具类 20 | * 21 | * @author 狐狸半面添 22 | * @create 2023-02-09 0:13 23 | */ 24 | public class FileTypeUtils { 25 | private static final Map contentType = new HashMap<>(); 26 | 27 | /** 28 | * 获取文件的 mime 类型 29 | * 30 | * @param file 文件 31 | * @return mime类型 32 | */ 33 | public static String getMimeType(File file) { 34 | AutoDetectParser parser = new AutoDetectParser(); 35 | parser.setParsers(new HashMap()); 36 | Metadata metadata = new Metadata(); 37 | metadata.add(TikaCoreProperties.RESOURCE_NAME_KEY, file.getName()); 38 | try (InputStream stream = Files.newInputStream(file.toPath())) { 39 | parser.parse(stream, new DefaultHandler(), metadata, new ParseContext()); 40 | } catch (Exception e) { 41 | throw new RuntimeException(); 42 | } 43 | return metadata.get(HttpHeaders.CONTENT_TYPE); 44 | } 45 | 46 | /** 47 | * 根据 mimetype 获取文件的简单类型 48 | * 49 | * @param mimeType mime类型 50 | * @return 简单类型:文本,图片,音频,视频,其它 51 | */ 52 | public static String getSimpleType(String mimeType) { 53 | String simpleType = mimeType.split("/")[0]; 54 | switch (simpleType) { 55 | case "text": 56 | return "文本"; 57 | case "image": 58 | return "图片"; 59 | case "audio": 60 | return "音频"; 61 | case "video": 62 | return "视频"; 63 | case "application": 64 | return "其它"; 65 | default: 66 | throw new RuntimeException("mimeType格式错误"); 67 | } 68 | } 69 | 70 | // 测试 71 | public static void main(String[] args) { 72 | File file = new File("D:\\location语法规则.docx"); 73 | String mimeType = getMimeType(file); 74 | System.out.println(mimeType); 75 | System.out.println(getSimpleType(mimeType)); 76 | } 77 | } -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/util/MinioClientUtils.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.util; 2 | 3 | import com.j256.simplemagic.ContentInfo; 4 | import com.j256.simplemagic.ContentInfoUtil; 5 | import io.minio.GetObjectArgs; 6 | import io.minio.MinioClient; 7 | import io.minio.PutObjectArgs; 8 | import io.minio.UploadObjectArgs; 9 | import org.apache.tomcat.util.http.fileupload.IOUtils; 10 | import org.springframework.http.MediaType; 11 | 12 | import java.io.*; 13 | 14 | /** 15 | * 操作minio的工具类 16 | * 17 | * @author 狐狸半面添 18 | * @create 2023-02-08 22:08 19 | */ 20 | public class MinioClientUtils { 21 | private final MinioClient minioClient; 22 | 23 | public MinioClientUtils(MinioClient minioClient) { 24 | this.minioClient = minioClient; 25 | } 26 | 27 | /** 28 | * 获取minio文件的输入流对象 29 | * 30 | * @param bucket 桶 31 | * @param filePath 文件路径 32 | * @return 输入流 33 | * @throws Exception 异常 34 | */ 35 | public InputStream getObject(String bucket, String filePath) throws Exception { 36 | return minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(filePath).build()); 37 | } 38 | 39 | /** 40 | * 将分块文件上传到分布式文件系统 41 | * 42 | * @param bytes 文件的字节数组 43 | * @param bucket 桶 44 | * @param filePath 存储在桶中的文件路径 45 | */ 46 | public void uploadChunkFile(byte[] bytes, String bucket, String filePath) throws Exception { 47 | // 1.指定资源的媒体类型为未知二进制流,以分片形式上传至minio 48 | try ( 49 | ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes) 50 | ) { 51 | minioClient.putObject( 52 | PutObjectArgs.builder() 53 | .bucket(bucket) 54 | .object(filePath) 55 | // InputStream stream, long objectSize 对象大小, long partSize 分片大小(-1表示5M,最大不要超过5T,最多10000) 56 | .stream(byteArrayInputStream, byteArrayInputStream.available(), -1) 57 | .contentType(MediaType.APPLICATION_OCTET_STREAM_VALUE) 58 | .build() 59 | ); 60 | } catch (Exception e) { 61 | throw new RuntimeException(e); 62 | } 63 | } 64 | 65 | /** 66 | * 将文件上传到分布式文件系统 67 | * 68 | * @param bytes 文件的字节数组 69 | * @param bucket 桶 70 | * @param filePath 存储在桶中的文件路径 71 | */ 72 | public void uploadFile(byte[] bytes, String bucket, String filePath) throws Exception { 73 | // 1.指定资源的媒体类型,默认未知二进制流 74 | String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; 75 | 76 | // 2.判断是否有后缀,有后缀则根据后缀推算出文件类型,否则使用默认的未知二进制流 77 | if (filePath.contains(".")) { 78 | // 取objectName中的扩展名 79 | String extension = filePath.substring(filePath.lastIndexOf(".")); 80 | ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension); 81 | if (extensionMatch != null) { 82 | contentType = extensionMatch.getMimeType(); 83 | } 84 | } 85 | 86 | // 3.以分片形式上传至minio 87 | ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); 88 | 89 | PutObjectArgs putObjectArgs = PutObjectArgs.builder() 90 | .bucket(bucket) 91 | .object(filePath) 92 | // InputStream stream, long objectSize 对象大小, long partSize 分片大小(-1表示5M,最大不要超过5T,最多10000) 93 | .stream(byteArrayInputStream, byteArrayInputStream.available(), -1) 94 | .contentType(contentType) 95 | .build(); 96 | // 上传 97 | minioClient.putObject(putObjectArgs); 98 | } 99 | 100 | 101 | /** 102 | * 根据文件路径将文件上传到文件系统 103 | * 104 | * @param naiveFilePath 本地文件路径 105 | * @param bucket 桶 106 | * @param minioFilePath 保存到minio的文件路径位置 107 | * @throws Exception 异常 108 | */ 109 | public void uploadChunkFile(String naiveFilePath, String bucket, String minioFilePath) throws Exception { 110 | UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder() 111 | .bucket(bucket) 112 | .object(minioFilePath) 113 | .filename(naiveFilePath) 114 | .build(); 115 | minioClient.uploadObject(uploadObjectArgs); 116 | } 117 | 118 | /** 119 | * 下载文件保存至本地临时文件中 120 | * 121 | * @param tempFilePrefix 临时文件的前缀 122 | * @param tempFileSuffix 临时文件的后缀 123 | * @param bucket 桶 124 | * @param filePath 文件路径 125 | * @return 携带数据的临时文件 126 | * @throws Exception 异常信息 127 | */ 128 | public File downloadFile(String tempFilePrefix, String tempFileSuffix, String bucket, String filePath) throws Exception { 129 | // 1.创建空文件,临时保存下载下来的分块文件数据 130 | File tempFile = File.createTempFile(tempFilePrefix, tempFileSuffix); 131 | try ( 132 | // 2.获取目标文件的输入流对象 133 | InputStream inputStream = getObject(bucket, filePath); 134 | // 3.获取临时空文件的输出流对象 135 | FileOutputStream outputStream = new FileOutputStream(tempFile); 136 | ) { 137 | // 4.进行数据拷贝 138 | IOUtils.copy(inputStream, outputStream); 139 | // 5.返回保存了数据的临时文件 140 | return tempFile; 141 | } catch (Exception e) { 142 | throw new RuntimeException(e.getMessage()); 143 | } 144 | } 145 | 146 | // public File downloadFile(String tempFilePrefix, String tempFileSuffix, String bucket, String filePath) throws Exception { 147 | // // 1.创建空文件,临时保存下载下来的分块文件数据 148 | // File tempFile = File.createTempFile(tempFilePrefix, tempFileSuffix); 149 | // try { 150 | // Long start = System.currentTimeMillis(); 151 | // minioClient.downloadObject( 152 | // DownloadObjectArgs.builder() 153 | // // 指定 bucket 存储桶 154 | // .bucket(bucket) 155 | // // 指定 哪个文件 156 | // .object(filePath) 157 | // // 指定存放位置与名称 158 | // .filename(tempFile.getPath()) 159 | // .build()); 160 | // Long end = System.currentTimeMillis(); 161 | // System.out.println("下载分块时间:"+(end-start)+"ms"); 162 | // // 5.返回保存了数据的临时文件 163 | // return tempFile; 164 | // } catch (Exception e) { 165 | // throw new RuntimeException(e.getMessage()); 166 | // } 167 | // } 168 | } 169 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/util/RegexUtils.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.util; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | 5 | /** 6 | * 校验格式工具类 7 | * 8 | * @author 狐狸半面添 9 | * @create 2023-01-16 22:17 10 | */ 11 | public class RegexUtils { 12 | 13 | /** 14 | * 正则表达式模板 15 | * 16 | * @author 狐狸半面添 17 | * @create 2023-01-16 22:39 18 | */ 19 | public static class RegexPatterns { 20 | 21 | /** 22 | * md5十六进制正则:32个字符 23 | */ 24 | public static final String MD5_HEX_REGEX = "^[0-9abcdef]{32}$"; 25 | 26 | /** 27 | * 文件名称正则:最多255个字符 28 | */ 29 | public static final String FILE_NAME_REGEX = "^.{1,255}$"; 30 | 31 | /** 32 | * 文件标签正则:最多32个字符 33 | */ 34 | public static final String FILE_TAG_REGEX = "^.{1,32}$"; 35 | 36 | } 37 | 38 | 39 | /** 40 | * 校验是否不符合正则格式 41 | * 42 | * @param str 字符串 43 | * @param regex 正则表达式 44 | * @return true:符合 false:不符合 45 | */ 46 | private static boolean mismatch(String str, String regex) { 47 | if (StrUtil.isBlank(str)) { 48 | return true; 49 | } 50 | return !str.matches(regex); 51 | } 52 | 53 | 54 | /** 55 | * 是否是无效十六进制md5格式 56 | * 57 | * @param md5Hex md5的十六进制 58 | * @return true:符合,false:不符合 59 | */ 60 | public static boolean isMd5HexInvalid(String md5Hex){ 61 | return mismatch(md5Hex, RegexPatterns.MD5_HEX_REGEX); 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/vo/CheckChunkFileVO.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.vo; 2 | 3 | import com.fox.miniodemo.util.RegexUtils; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.validation.constraints.Min; 9 | import javax.validation.constraints.NotBlank; 10 | import javax.validation.constraints.NotNull; 11 | import javax.validation.constraints.Pattern; 12 | 13 | /** 14 | * 分块文件上传检查封装类 15 | * 16 | * @author 狐狸半面添 17 | * @create 2023-02-08 16:54 18 | */ 19 | @Data 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | public class CheckChunkFileVO { 23 | /** 24 | * 需要上传的文件的md5值 25 | */ 26 | @NotBlank(message = "文件md5不能为空") 27 | @Pattern(regexp = RegexUtils.RegexPatterns.MD5_HEX_REGEX, message = "文件md5格式错误") 28 | private String fileMd5; 29 | /** 30 | * 该分块文件的索引 31 | */ 32 | @NotNull(message = "分块文件索引不能为空") 33 | @Min(value = 0, message = "分块文件索引必须是大于等于0的整数") 34 | private Integer chunkIndex; 35 | } 36 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/java/com/fox/miniodemo/vo/UploadMergeChunksVO.java: -------------------------------------------------------------------------------- 1 | package com.fox.miniodemo.vo; 2 | 3 | import com.fox.miniodemo.util.RegexUtils; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.validation.constraints.Min; 9 | import javax.validation.constraints.NotBlank; 10 | import javax.validation.constraints.NotNull; 11 | import javax.validation.constraints.Pattern; 12 | 13 | /** 14 | * @author 狐狸半面添 15 | * @create 2023-02-09 22:56 16 | */ 17 | @Data 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class UploadMergeChunksVO { 21 | @NotBlank(message = "文件md5不能为空") 22 | @Pattern(regexp = RegexUtils.RegexPatterns.MD5_HEX_REGEX, message = "文件md5格式错误") 23 | private String fileMd5; 24 | 25 | @NotBlank(message = "文件名不能为空") 26 | @Pattern(regexp = RegexUtils.RegexPatterns.FILE_NAME_REGEX, message = "文件名最多255个字符") 27 | private String fileName; 28 | 29 | @NotBlank(message = "文件标签不能为空") 30 | @Pattern(regexp = RegexUtils.RegexPatterns.FILE_TAG_REGEX, message = "文件标签最多32个字符") 31 | private String tag; 32 | 33 | @NotNull(message = "分块文件数不能为空") 34 | @Min(value = 1, message = "块总数必须大于等于1") 35 | private Integer chunkTotal; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /minio-chunk-demo/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 60000 3 | spring: 4 | main: 5 | allow-circular-references: true # 允许循环依赖 6 | servlet: 7 | multipart: 8 | max-file-size: 3MB 9 | max-request-size: 5MB 10 | datasource: 11 | username: root 12 | password: 123456 13 | url: jdbc:mysql://127.0.0.1:3306/minio_demo?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 14 | driver-class-name: com.mysql.cj.jdbc.Driver 15 | # 指定数据源 16 | type: com.alibaba.druid.pool.DruidDataSource 17 | # Spring Boot 默认是不注入这些属性值的,需要自己绑定 18 | # druid 数据源专有配置 19 | initialSize: 5 20 | minIdle: 5 21 | maxActive: 20 22 | maxWait: 60000 23 | timeBetweenEvictionRunsMillis: 60000 24 | minEvictableIdleTimeMillis: 300000 25 | validationQuery: SELECT 1 FROM DUAL 26 | testWhileIdle: true 27 | testOnBorrow: false 28 | testOnReturn: false 29 | poolPreparedStatements: true 30 | mybatis-plus: 31 | mapper-locations: classpath:/mapper/**/*.xml 32 | global-config: 33 | db-config: 34 | id-type: assign_id # 使用雪花算法生成id 35 | logic-delete-value: 1 # 逻辑已删除值 36 | logic-not-delete-value: 0 # 逻辑未删除值 37 | configuration: 38 | # 这里我们配置出底层的sql,可以输出sql日志信息,方便我们观察 39 | log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 40 | minio: 41 | # 指定连接的ip和端口 42 | endpoint: http://192.168.65.129:9000 43 | # 指定 访问秘钥(也称用户id) 44 | accessKey: minioadmin 45 | # 指定 私有秘钥(也称密码) 46 | secretKey: minioadmin -------------------------------------------------------------------------------- /qrcode-demo/README.md: -------------------------------------------------------------------------------- 1 | 1.具体说明文档参考:[Java生成二维码(前后端分离项目实战)](https://blog.csdn.net/qq_62982856/article/details/132572246) -------------------------------------------------------------------------------- /qrcode-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.1.2 11 | 12 | 13 | 14 | com.zhulang 15 | qrcode-demo 16 | 1.0-SNAPSHOT 17 | 18 | 19 | 17 20 | 17 21 | UTF-8 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | 31 | 32 | com.google.zxing 33 | javase 34 | 3.1.0 35 | 36 | 37 | 38 | 39 | commons-lang 40 | commons-lang 41 | 2.6 42 | 43 | 44 | 45 | com.github.liuyueyi.media 46 | qrcode-plugin 47 | 3.0.0 48 | 49 | 50 | 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-maven-plugin 56 | 57 | 58 | 59 | org.projectlombok 60 | lombok 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/java/com/zhulang/qrcode/Application.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.qrcode; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author 狐狸半面添 8 | * @create 2023-08-28 21:26 9 | */ 10 | @SpringBootApplication 11 | public class Application { 12 | public static void main(String[] args) { 13 | SpringApplication.run(Application.class,args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/java/com/zhulang/qrcode/controller/QrCodePluginController.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.qrcode.controller; 2 | 3 | import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper; 4 | import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions; 5 | import com.google.zxing.WriterException; 6 | import com.zhulang.qrcode.util.QrCodePluginUtils; 7 | import com.zhulang.qrcode.util.ZXingUtils; 8 | import org.springframework.web.bind.annotation.*; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | import java.awt.*; 12 | import java.awt.image.BufferedImage; 13 | import java.io.IOException; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * @author 狐狸半面添 19 | * @create 2023-08-29 21:37 20 | */ 21 | @CrossOrigin 22 | @RestController 23 | @RequestMapping("/qrcode-plugin") 24 | public class QrCodePluginController { 25 | 26 | /** 27 | * 生成普通黑白二维码 28 | * 29 | * @param content 文本内容 30 | * @return 图片base64编码 31 | */ 32 | @GetMapping("/getCommonBlackWhiteCode") 33 | public Map getCommonBlackWhiteCode(@RequestParam("content") String content) throws IOException, WriterException { 34 | Map map = new HashMap<>(1); 35 | map.put("imgEncode", QrCodePluginUtils.generateBlackWhiteCode(content)); 36 | return map; 37 | } 38 | 39 | /** 40 | * 生成带 logo 的黑白二维码 41 | * 42 | * @param content 文本内容 43 | * @param logo logo文件 44 | * @return 图片base64编码 45 | */ 46 | @PostMapping("/getLogoBlackWhiteCode") 47 | public Map getLogoBlackWhiteCode(@RequestParam("content") String content, @RequestParam("logo") MultipartFile logo) throws IOException, WriterException { 48 | Map map = new HashMap<>(1); 49 | map.put("imgEncode", QrCodePluginUtils.generateLogoBlackWhiteCode(content, logo)); 50 | return map; 51 | } 52 | 53 | /** 54 | * 生成彩色二维码 55 | * 56 | * @param content 文本内容 57 | * @return 图片base64编码 58 | */ 59 | @GetMapping("/getColorCode") 60 | public Map getColorCode(@RequestParam("content") String content) throws IOException, WriterException { 61 | Map map = new HashMap<>(1); 62 | // 二维码颜色可以由前端传过来进行指定 63 | map.put("imgEncode", QrCodePluginUtils.generateColorCode(content, Color.BLUE)); 64 | return map; 65 | } 66 | 67 | /** 68 | * 生成带背景图片的黑白二维码 69 | * 70 | * @param content 文本内容 71 | * @param backgroundImage 背景图文件 72 | * @return 图片base64编码 73 | */ 74 | @PostMapping("/getBgBlackWhiteCode") 75 | public Map getBgBlackWhiteCode(@RequestParam("content") String content, @RequestParam("backgroundImage") MultipartFile backgroundImage) throws IOException, WriterException { 76 | Map map = new HashMap<>(1); 77 | map.put("imgEncode", QrCodePluginUtils.generateBgBlackWhiteCode(content, backgroundImage)); 78 | return map; 79 | } 80 | 81 | /** 82 | * 生成带特殊形状的二维码 83 | * 84 | * @param content 文本内容 85 | * @return 图片base64编码 86 | */ 87 | @GetMapping("/getShapeCode") 88 | public Map getShapeCode(@RequestParam("content") String content) throws IOException, WriterException { 89 | Map map = new HashMap<>(1); 90 | // 绘制样式可以由前端传过来进行指定,这里设定为 钻石 91 | map.put("imgEncode", QrCodePluginUtils.generateShapeCode(content, QrCodeOptions.DrawStyle.DIAMOND)); 92 | return map; 93 | } 94 | 95 | /** 96 | * 生成图片填充二维码 97 | * 98 | * @param content 文本内容 99 | * @param fillImg 填充图片 100 | * @return 图片base64编码 101 | */ 102 | @PostMapping("/getImgFillCode") 103 | public Map getImgFillCode(@RequestParam("content") String content, @RequestParam("fillImg") MultipartFile fillImg) throws IOException, WriterException { 104 | Map map = new HashMap<>(1); 105 | map.put("imgEncode", QrCodePluginUtils.generateImgFillCode(content, fillImg)); 106 | return map; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/java/com/zhulang/qrcode/controller/ZXingController.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.qrcode.controller; 2 | 3 | import com.google.zxing.WriterException; 4 | import com.zhulang.qrcode.util.ZXingUtils; 5 | import org.springframework.web.bind.annotation.*; 6 | import org.springframework.web.multipart.MultipartFile; 7 | 8 | import java.io.IOException; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * @author 狐狸半面添 14 | * @create 2023-08-28 21:35 15 | */ 16 | @CrossOrigin 17 | @RestController 18 | @RequestMapping("/zxing") 19 | public class ZXingController { 20 | @GetMapping("/getCommonBlackWhite") 21 | public Map getCommonBlackWhite(@RequestParam("content") String content) throws IOException, WriterException { 22 | Map map = new HashMap<>(1); 23 | map.put("imgEncode", ZXingUtils.generateBlackWhiteCode(content)); 24 | return map; 25 | } 26 | 27 | @PostMapping("/getLogo") 28 | public Map getLogo(@RequestParam("content") String content, @RequestParam("logo") MultipartFile logo) throws IOException, WriterException { 29 | Map map = new HashMap<>(1); 30 | map.put("imgEncode", ZXingUtils.generateLogoCode(content, logo)); 31 | return map; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/java/com/zhulang/qrcode/util/QrCodePluginUtils.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.qrcode.util; 2 | 3 | import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeGenWrapper; 4 | import com.github.hui.quick.plugin.qrcode.wrapper.QrCodeOptions; 5 | import com.google.zxing.WriterException; 6 | import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; 7 | import org.apache.tomcat.util.codec.binary.Base64; 8 | import org.springframework.util.FastByteArrayOutputStream; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | import javax.imageio.ImageIO; 12 | import java.awt.*; 13 | import java.awt.image.BufferedImage; 14 | import java.io.IOException; 15 | 16 | /** 17 | * @author 狐狸半面添 18 | * @create 2023-08-29 21:41 19 | */ 20 | public class QrCodePluginUtils { 21 | 22 | private static String imageParseBase64(BufferedImage image) throws IOException { 23 | FastByteArrayOutputStream fos = new FastByteArrayOutputStream(); 24 | ImageIO.write(image, "png", fos); 25 | // 获取二维码图片的 base64 编码 26 | String imgEncode = Base64.encodeBase64String(fos.toByteArray()); 27 | fos.flush(); 28 | return imgEncode; 29 | } 30 | 31 | /** 32 | * 生成普通黑白二维码 33 | * 34 | * @param content 文本内容 35 | * @return 图片base64编码 36 | */ 37 | public static String generateBlackWhiteCode(String content) throws IOException, WriterException { 38 | return imageParseBase64(QrCodeGenWrapper.of(content).asBufferedImage()); 39 | } 40 | 41 | /** 42 | * 生成带 logo 的黑白二维码 43 | * 44 | * @param content 文本内容 45 | * @param logo logo文件 46 | * @return 图片base64编码 47 | */ 48 | public static String generateLogoBlackWhiteCode(String content, MultipartFile logo) throws IOException, WriterException { 49 | BufferedImage image = QrCodeGenWrapper.of(content) 50 | .setLogo(logo.getInputStream()) 51 | // 设置 logo 图片与二维码之间的比例,10 表示 logo 的宽度等于二维码的 1/10 52 | .setLogoRate(10) 53 | // 设置 logo 图片的样式,将 logo 的边框形状设置为圆形 54 | .setLogoStyle(QrCodeOptions.LogoStyle.ROUND) 55 | .asBufferedImage(); 56 | return imageParseBase64(image); 57 | } 58 | 59 | /** 60 | * 生成彩色二维码 61 | * 62 | * @param content 文本内容 63 | * @param color 颜色 64 | * @return 图片base64编码 65 | */ 66 | public static String generateColorCode(String content, Color color) throws IOException, WriterException { 67 | BufferedImage image = QrCodeGenWrapper.of(content) 68 | // 指定画笔颜色 69 | .setDrawPreColor(color) 70 | .asBufferedImage(); 71 | return imageParseBase64(image); 72 | } 73 | 74 | /** 75 | * 生成带背景图片的黑白二维码 76 | * 77 | * @param content 文本内容 78 | * @param backgroundImage 背景图文件 79 | * @return 图片base64编码 80 | */ 81 | public static String generateBgBlackWhiteCode(String content, MultipartFile backgroundImage) throws IOException, WriterException { 82 | BufferedImage image = QrCodeGenWrapper.of(content) 83 | // 设置背景图 84 | .setBgImg(backgroundImage.getInputStream()) 85 | // 设置背景图透明度 86 | .setBgOpacity(0.7F) 87 | .asBufferedImage(); 88 | return imageParseBase64(image); 89 | } 90 | 91 | /** 92 | * 生成带特殊形状的二维码 93 | * 94 | * @param content 文本内容 95 | * @param drawStyle 绘制样式 96 | * @return 图片base64编码 97 | */ 98 | public static String generateShapeCode(String content, QrCodeOptions.DrawStyle drawStyle) throws IOException, WriterException { 99 | BufferedImage image = QrCodeGenWrapper.of(content) 100 | // 启用二维码绘制时的缩放功能 101 | .setDrawEnableScale(true) 102 | // 指定绘制样式 103 | .setDrawStyle(drawStyle) 104 | .asBufferedImage(); 105 | return imageParseBase64(image); 106 | } 107 | 108 | /** 109 | * 生成图片填充二维码 110 | * 111 | * @param content 文本内容 112 | * @param fillImg 填充图片 113 | * @return 图片base64编码 114 | */ 115 | public static String generateImgFillCode(String content, MultipartFile fillImg) throws IOException, WriterException { 116 | BufferedImage image = QrCodeGenWrapper.of(content) 117 | // 设置二维码的错误纠正级别 118 | .setErrorCorrection(ErrorCorrectionLevel.H) 119 | // 绘制二维码时采用图片填充 120 | .setDrawStyle(QrCodeOptions.DrawStyle.IMAGE) 121 | // 设置填充的图片 122 | .addImg(1, 1, fillImg.getInputStream()) 123 | .asBufferedImage(); 124 | return imageParseBase64(image); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/java/com/zhulang/qrcode/util/ZXingUtils.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.qrcode.util; 2 | 3 | import com.google.zxing.BarcodeFormat; 4 | import com.google.zxing.EncodeHintType; 5 | import com.google.zxing.MultiFormatWriter; 6 | import com.google.zxing.WriterException; 7 | import com.google.zxing.common.BitMatrix; 8 | import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; 9 | import org.apache.tomcat.util.codec.binary.Base64; 10 | import org.springframework.util.FastByteArrayOutputStream; 11 | import org.springframework.web.multipart.MultipartFile; 12 | 13 | import javax.imageio.ImageIO; 14 | import java.awt.*; 15 | import java.awt.geom.RoundRectangle2D; 16 | import java.awt.image.BufferedImage; 17 | import java.io.IOException; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | /** 22 | * @author 狐狸半面添 23 | * @create 2023-08-28 21:35 24 | */ 25 | public class ZXingUtils { 26 | public static String generateBlackWhiteCode(String content) throws WriterException, IOException { 27 | // 使用 Google 提供的 zxing 开源库,生成带 Logo 的黑白二维码 28 | 29 | // 需要创建一个 Map 集合,使用这个 Map 集合存储二维码相关的属性(参数) 30 | Map map = new HashMap<>(3); 31 | 32 | // 设置二维码的误差校正级别 33 | map.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); 34 | // 设置二维码的字符集 35 | map.put(EncodeHintType.CHARACTER_SET, "utf-8"); 36 | // 设置二维码四周的留白,单位为 px 37 | map.put(EncodeHintType.MARGIN, 1); 38 | 39 | // 创建 zxing 的核心对象,MultiFormatWriter(多格式写入器) 40 | // 通过 MultiFormatWriter 对象来生成二维码 41 | MultiFormatWriter writer = new MultiFormatWriter(); 42 | 43 | // writer.encode(内容, 什么格式的二维码, 二维码宽度, 二维码高度, 二维码参数) 44 | // 位矩阵对象(位矩阵对象内部实际上是一个二维数组,二维数组中每一个元素是 boolean 类型,true 代表黑色,false 代表白色) 45 | BitMatrix bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, 300, 300, map); 46 | 47 | // 获取矩阵的宽度 48 | int width = bitMatrix.getWidth(); 49 | // 获取矩阵的高度 50 | int height = bitMatrix.getHeight(); 51 | 52 | // 生成二维码图片 53 | BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 54 | 55 | // 编写一个嵌套循环,遍历二维数组的一个循环,遍历位矩阵对象 56 | for (int x = 0; x < width; x++) { 57 | for (int y = 0; y < height; y++) { 58 | image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF); 59 | } 60 | } 61 | 62 | FastByteArrayOutputStream fos = new FastByteArrayOutputStream(); 63 | ImageIO.write(image, "png", fos); 64 | // 获取二维码图片的 base64 编码 65 | String imgEncode = Base64.encodeBase64String(fos.toByteArray()); 66 | fos.flush(); 67 | 68 | // 返回 base64 编码 69 | return imgEncode; 70 | } 71 | 72 | public static String generateLogoCode(String content, MultipartFile logo) throws WriterException, IOException { 73 | // 使用 Google 提供的 zxing 开源库,生成普通的黑白二维码 74 | 75 | // 需要创建一个 Map 集合,使用这个 Map 集合存储二维码相关的属性(参数) 76 | Map map = new HashMap<>(3); 77 | 78 | // 设置二维码的误差校正级别 79 | map.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); 80 | // 设置二维码的字符集 81 | map.put(EncodeHintType.CHARACTER_SET, "utf-8"); 82 | // 设置二维码四周的留白,单位为 px 83 | map.put(EncodeHintType.MARGIN, 1); 84 | 85 | // 创建 zxing 的核心对象,MultiFormatWriter(多格式写入器) 86 | // 通过 MultiFormatWriter 对象来生成二维码 87 | MultiFormatWriter writer = new MultiFormatWriter(); 88 | 89 | // writer.encode(内容, 什么格式的二维码, 二维码宽度, 二维码高度, 二维码参数) 90 | // 位矩阵对象(位矩阵对象内部实际上是一个二维数组,二维数组中每一个元素是 boolean 类型,true 代表黑色,false 代表白色) 91 | BitMatrix bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, 300, 300, map); 92 | 93 | // 获取矩阵的宽度 94 | int width = bitMatrix.getWidth(); 95 | // 获取矩阵的高度 96 | int height = bitMatrix.getHeight(); 97 | 98 | // 生成二维码图片 99 | BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 100 | 101 | // 编写一个嵌套循环,遍历二维数组的一个循环,遍历位矩阵对象 102 | for (int x = 0; x < width; x++) { 103 | for (int y = 0; y < height; y++) { 104 | image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF); 105 | } 106 | } 107 | 108 | // 给二维码添加 Logo 109 | // 1.获取 logo 图片:通过 ImageIO 的read方法,从输入流中读取,从而获得 logo 图片 110 | BufferedImage logoImage = ImageIO.read(logo.getInputStream()); 111 | // 2.设置 logo 的宽度和高度 112 | int logoWidth = Math.min(logoImage.getWidth(), 60); 113 | int logoHeight = Math.min(logoImage.getHeight(), 60); 114 | // 3.将 logo 缩放:使用平滑缩放算法对原 logo 图像进行缩放得到一个全新的图像 115 | Image scaledLogo = logoImage.getScaledInstance(logoWidth, logoHeight, Image.SCALE_SMOOTH); 116 | // 4.将缩放后的 logo 画到黑白二维码上 117 | // 4.1 获取一个 2D 的画笔 118 | Graphics2D graphics2D = image.createGraphics(); 119 | // 4.2 指定开始的坐标 x,y 120 | int x = (300 - logoWidth) / 2; 121 | int y = (300 - logoHeight) / 2; 122 | // 4.3 将缩放后的 logo 画上去 123 | graphics2D.drawImage(scaledLogo,x,y,null); 124 | // 4.4 创建一个具有指定位置、宽度、高度和圆角半径的圆角矩形,这个圆角矩形是用来绘制边框的 125 | Shape shape = new RoundRectangle2D.Float(x,y,logoWidth,logoHeight,10,10); 126 | // 4.5 使用一个宽度为 4px 的基本笔触 127 | graphics2D.setStroke(new BasicStroke(4f)); 128 | // 4.6 给 logo 画圆角矩形 129 | graphics2D.draw(shape); 130 | // 4.7 释放画笔 131 | graphics2D.dispose(); 132 | 133 | FastByteArrayOutputStream fos = new FastByteArrayOutputStream(); 134 | ImageIO.write(image, "png", fos); 135 | // 获取二维码图片的 base64 编码 136 | String imgEncode = Base64.encodeBase64String(fos.toByteArray()); 137 | fos.flush(); 138 | 139 | // 返回 base64 编码 140 | return imgEncode; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | servlet: 3 | multipart: 4 | max-file-size: 10MB 5 | file-size-threshold: 2MB 6 | max-request-size: 15MB -------------------------------------------------------------------------------- /qrcode-demo/src/main/resources/static/html/qrcodePlugin/BgBlackWhiteCode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 生成带背景图的黑白二维码 6 | 7 | 8 | 请输入文本内容:
9 | 请选择图片:
10 | 11 |
12 | 13 | 14 | 15 | 40 | 41 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/resources/static/html/qrcodePlugin/ColorCode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 生成彩色二维码 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 30 | 31 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/resources/static/html/qrcodePlugin/CommonBlackWhiteCode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 生成普通黑白二维码 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 30 | 31 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/resources/static/html/qrcodePlugin/ImgFillCode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 生成图片填充二维码 6 | 7 | 8 | 请输入文本内容:
9 | 请选择图片:
10 | 11 |
12 | 13 | 14 | 15 | 40 | 41 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/resources/static/html/qrcodePlugin/LogoBlackWhiteCode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 生成带 logo 的二维码 6 | 7 | 8 | 请输入文本内容:
9 | 请选择图片:
10 | 11 |
12 | 13 | 14 | 15 | 40 | 41 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/resources/static/html/qrcodePlugin/ShapeCode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 生成特殊形状的二维码 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 30 | 31 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/resources/static/html/zxing/CommonBlackWhite.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 生成普通黑白二维码 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 30 | 31 | -------------------------------------------------------------------------------- /qrcode-demo/src/main/resources/static/html/zxing/ZXingLogo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ZXing 生成带 logo 的二维码 6 | 7 | 8 | 请输入文本内容:
9 | 请选择图片:
10 | 11 |
12 | 13 | 14 | 15 | 40 | 41 | -------------------------------------------------------------------------------- /sms-demo/README.md: -------------------------------------------------------------------------------- 1 | ## 1.application.yml 文件中的配置需要进行修改。 2 | 3 | - redis 地址 4 | - redis 密码 5 | - 短信平台信息 6 | 7 | ## 2.ubuntu安装docker的步骤 8 | 9 | 1. 安装需要的包 10 | 11 | ```shell 12 | sudo apt-get update 13 | ``` 14 | 15 | 2. 安装依赖包 16 | 17 | ```shell 18 | sudo apt-get install \ 19 | apt-transport-https \ 20 | ca-certificates \ 21 | curl \ 22 | gnupg-agent \ 23 | software-properties-common 24 | ``` 25 | 26 | 3. 添加 Docker 的官方 GPG 密钥 27 | 28 | ```shell 29 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 30 | ``` 31 | 32 | 4. 设置远程仓库 33 | 34 | ```shell 35 | sudo add-apt-repository \ 36 | "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ 37 | $(lsb_release -cs) \ 38 | stable" 39 | ``` 40 | 41 | 5. 安装 Docker-CE 42 | 43 | ```shell 44 | sudo apt-get update 45 | 46 | sudo apt-get install docker-ce docker-ce-cli containerd.io 47 | ``` 48 | 49 | 6. 验证是否成功 50 | 51 | ```shell 52 | sudo docker run hello-world 53 | ``` 54 | 55 | ## 3.使用 docker 安装redis并设置密码的步骤 56 | 57 | ```shell 58 | # 拉取redis镜像 59 | docker pull redis 60 | 61 | # 启动容器的时候,并为其设置密码 62 | docker run -d --name myredis -p 6379:6379 redis --requirepass "123456" 63 | ``` 64 | 65 | ## 其它注意事项 66 | 67 | 注意需要将防火墙6379端口打开。 -------------------------------------------------------------------------------- /sms-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.fox 8 | sms-demo 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 8 13 | 8 14 | UTF-8 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-web 20 | 2.6.3 21 | 22 | 23 | org.projectlombok 24 | lombok 25 | 1.18.24 26 | 27 | 28 | 29 | com.alibaba 30 | fastjson 31 | 2.0.25 32 | 33 | 34 | cn.hutool 35 | hutool-all 36 | 5.8.15 37 | 38 | 39 | 40 | com.aliyun 41 | alibabacloud-dysmsapi20170525 42 | 2.0.23 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-data-redis 48 | 2.6.3 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/DirectTest/AliSmsTest.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.DirectTest; 2 | 3 | import com.aliyun.auth.credentials.Credential; 4 | import com.aliyun.auth.credentials.provider.StaticCredentialProvider; 5 | import com.aliyun.sdk.service.dysmsapi20170525.AsyncClient; 6 | import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsRequest; 7 | import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsResponse; 8 | import com.google.gson.Gson; 9 | import darabonba.core.client.ClientOverrideConfiguration; 10 | 11 | import java.util.concurrent.CompletableFuture; 12 | 13 | /** 14 | * 用于直接运行测试 15 | * 16 | * @author 狐狸半面添 17 | * @create 2023-04-01 18:47 18 | */ 19 | public class AliSmsTest { 20 | public static void main(String[] args) throws Exception { 21 | StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder() 22 | .accessKeyId("LTAI5tFhzvktt9U5j4ak2637") 23 | .accessKeySecret("hYLiytNltwB1pOetnBiNcXPmXqfvkx") 24 | //.securityToken("") // use STS token 25 | .build()); 26 | 27 | // Configure the Client 28 | AsyncClient client = AsyncClient.builder() 29 | .region("cn-wulanchabu") // Region ID 30 | //.httpClient(httpClient) // Use the configured HttpClient, otherwise use the default HttpClient (Apache HttpClient) 31 | .credentialsProvider(provider) 32 | //.serviceConfiguration(Configuration.create()) // Service-level configuration 33 | // Client-level configuration rewrite, can set Endpoint, Http request parameters, etc. 34 | .overrideConfiguration( 35 | ClientOverrideConfiguration.create() 36 | .setEndpointOverride("dysmsapi.aliyuncs.com") 37 | //.setConnectTimeout(Duration.ofSeconds(30)) 38 | ) 39 | .build(); 40 | 41 | // Parameter settings for API request 42 | SendSmsRequest sendSmsRequest = SendSmsRequest.builder() 43 | .signName("逐浪教育") 44 | .templateCode("SMS_275395309") 45 | .phoneNumbers("15675229376") 46 | .templateParam("{\"code\":\"123456\"}") 47 | // Request-level configuration rewrite, can set Http request parameters, etc. 48 | // .requestConfiguration(RequestConfiguration.create().setHttpHeaders(new HttpHeaders())) 49 | .build(); 50 | 51 | // Asynchronously get the return value of the API request 52 | CompletableFuture response = client.sendSms(sendSmsRequest); 53 | // Synchronously get the return value of the API request 54 | SendSmsResponse resp = response.get(); 55 | System.out.println(new Gson().toJson(resp)); 56 | // Asynchronous processing of return values 57 | /*response.thenAccept(resp -> { 58 | System.out.println(new Gson().toJson(resp)); 59 | }).exceptionally(throwable -> { // Handling exceptions 60 | System.out.println(throwable.getMessage()); 61 | return null; 62 | });*/ 63 | 64 | // Finally, close the client 65 | client.close(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/SmsDemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author 狐狸半面添 8 | * @create 2023-03-22 18:07 9 | */ 10 | @SpringBootApplication 11 | public class SmsDemoApplication { 12 | public static void main(String[] args) { 13 | SpringApplication.run(SmsDemoApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/component/FastJsonRedisSerializer.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.component; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.parser.ParserConfig; 5 | import com.alibaba.fastjson.serializer.SerializerFeature; 6 | import com.fasterxml.jackson.databind.JavaType; 7 | import com.fasterxml.jackson.databind.type.TypeFactory; 8 | import org.springframework.data.redis.serializer.RedisSerializer; 9 | import org.springframework.data.redis.serializer.SerializationException; 10 | 11 | import java.nio.charset.Charset; 12 | import java.nio.charset.StandardCharsets; 13 | 14 | /** 15 | * Redis使用FastJson序列化 16 | * 17 | * @author 狐狸半面添 18 | * @create 2023-01-17 21:30 19 | */ 20 | public class FastJsonRedisSerializer implements RedisSerializer { 21 | 22 | public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; 23 | 24 | private Class clazz; 25 | 26 | static { 27 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true); 28 | } 29 | 30 | public FastJsonRedisSerializer(Class clazz) { 31 | super(); 32 | this.clazz = clazz; 33 | } 34 | 35 | @Override 36 | public byte[] serialize(T t) throws SerializationException { 37 | if (t == null) { 38 | return new byte[0]; 39 | } 40 | return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); 41 | } 42 | 43 | @Override 44 | public T deserialize(byte[] bytes) throws SerializationException { 45 | if (bytes == null || bytes.length <= 0) { 46 | return null; 47 | } 48 | String str = new String(bytes, DEFAULT_CHARSET); 49 | 50 | return JSON.parseObject(str, clazz); 51 | } 52 | 53 | 54 | protected JavaType getJavaType(Class clazz) { 55 | return TypeFactory.defaultInstance().constructType(clazz); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.config; 2 | 3 | import com.fox.sms.component.FastJsonRedisSerializer; 4 | import com.fox.sms.util.RedisCacheUtils; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.data.redis.connection.RedisConnectionFactory; 8 | import org.springframework.data.redis.core.RedisTemplate; 9 | import org.springframework.data.redis.serializer.StringRedisSerializer; 10 | 11 | /** 12 | * @author 狐狸半面添 13 | * @create 2023-01-17 21:00 14 | */ 15 | @Configuration 16 | public class RedisConfig { 17 | 18 | @Bean 19 | @SuppressWarnings(value = { "unchecked", "rawtypes" }) 20 | public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) 21 | { 22 | RedisTemplate template = new RedisTemplate<>(); 23 | template.setConnectionFactory(connectionFactory); 24 | 25 | FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class); 26 | 27 | // 使用StringRedisSerializer来序列化和反序列化redis的key值 28 | template.setKeySerializer(new StringRedisSerializer()); 29 | template.setValueSerializer(serializer); 30 | 31 | // Hash的key也采用StringRedisSerializer的序列化方式 32 | template.setHashKeySerializer(new StringRedisSerializer()); 33 | template.setHashValueSerializer(serializer); 34 | 35 | template.afterPropertiesSet(); 36 | return template; 37 | } 38 | 39 | @Bean 40 | public RedisCacheUtils redisCacheUtils(RedisTemplate redisTemplate){ 41 | return new RedisCacheUtils(redisTemplate); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/config/SmsConfig.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.config; 2 | 3 | import com.fox.sms.util.AliSmsTemplateUtils; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * @author 狐狸半面添 10 | * @create 2023-04-01 15:46 11 | */ 12 | @Configuration 13 | public class SmsConfig { 14 | 15 | /** 16 | * 配置阿里短信发送工具类 17 | */ 18 | @Bean 19 | @ConfigurationProperties(prefix = "sms.ali") 20 | public AliSmsTemplateUtils aliSmsTemplateUtils(){ 21 | return new AliSmsTemplateUtils(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/constant/RedisConstants.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.constant; 2 | 3 | /** 4 | * 定义 redis 缓存的常量前缀 5 | * 6 | * @author 狐狸半面添 7 | * @create 2023-04-01 16:37 8 | */ 9 | public class RedisConstants { 10 | /** 11 | * 用户登录&注册手机验证码 12 | * 有效期:5分钟 13 | * 剩余时长大于 4分钟 则无法再次发送 14 | */ 15 | public static final String LOGIN_USER_CODE_KEY = "login:user:code:"; 16 | public static final Long LOGIN_USER_CODE_TTL = 60 * 5L; 17 | public static final Long LOGIN_USER_CODE_AGAIN_TTL = 60 * 4L; 18 | } 19 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/controller/SmsAliController.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.controller; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.fox.sms.entity.Result; 5 | import com.fox.sms.service.SmsAliService; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import javax.annotation.Resource; 12 | 13 | /** 14 | * @author 狐狸半面添 15 | * @create 2023-04-01 15:45 16 | */ 17 | @RestController 18 | @RequestMapping("/sms-ali") 19 | public class SmsAliController { 20 | @Resource 21 | private SmsAliService smsAliService; 22 | 23 | /** 24 | * 发送用于登录的验证码 25 | * 26 | * @param object 手机号的json对象 27 | * @return 发送情况 28 | */ 29 | @PostMapping("/sendLoginCode") 30 | public Result sendLoginCode(@RequestBody JSONObject object){ 31 | String phone = object.getString("phone"); 32 | return smsAliService.sendLoginCode(phone); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/entity/Result.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | /** 8 | * 统一返回对象 9 | * 10 | * @author 狐狸半面添 11 | * @create 2023-03-22 18:34 12 | */ 13 | @Getter 14 | @Setter 15 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 16 | public class Result { 17 | private Integer code; 18 | private String msg; 19 | private Object data; 20 | 21 | private Result(Integer code, String msg, Object data) { 22 | this.code = code; 23 | this.msg = msg; 24 | this.data = data; 25 | } 26 | 27 | private Result(Integer code, String msg) { 28 | this.code = code; 29 | this.msg = msg; 30 | } 31 | 32 | private Result(){} 33 | 34 | public static Result ok() { 35 | return new Result(200, "success"); 36 | } 37 | 38 | public static Result ok(Object data) { 39 | return new Result(200, "success", data); 40 | } 41 | 42 | public static Result error(String msg) { 43 | return new Result(500, msg); 44 | } 45 | 46 | public static Result error(Integer code, String msg) { 47 | return new Result(code, msg); 48 | } 49 | 50 | public static Result error(){ 51 | return new Result(500,"服务器异常"); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/service/SmsAliService.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.service; 2 | 3 | import com.fox.sms.entity.Result; 4 | 5 | /** 6 | * @author 狐狸半面添 7 | * @create 2023-04-01 16:26 8 | */ 9 | public interface SmsAliService { 10 | /** 11 | * 发送用于登录与注册的验证码 12 | * 13 | * @param phone 手机号 14 | * @return 发送情况 15 | */ 16 | Result sendLoginCode(String phone); 17 | } 18 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/service/impl/SmsAliServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.service.impl; 2 | 3 | import cn.hutool.core.util.RandomUtil; 4 | import cn.hutool.http.HttpStatus; 5 | import com.fox.sms.constant.RedisConstants; 6 | import com.fox.sms.entity.Result; 7 | import com.fox.sms.service.SmsAliService; 8 | import com.fox.sms.util.AliSmsTemplateUtils; 9 | import com.fox.sms.util.RedisCacheUtils; 10 | import com.fox.sms.util.RegexUtils; 11 | import org.springframework.stereotype.Service; 12 | 13 | import javax.annotation.Resource; 14 | 15 | /** 16 | * @author 狐狸半面添 17 | * @create 2023-04-01 16:25 18 | */ 19 | @Service 20 | public class SmsAliServiceImpl implements SmsAliService { 21 | @Resource 22 | private AliSmsTemplateUtils aliSmsTemplateUtils; 23 | @Resource 24 | private RedisCacheUtils redisCacheUtils; 25 | 26 | @Override 27 | public Result sendLoginCode(String phone) { 28 | // 1.校验手机号格式 29 | if (RegexUtils.isPhoneInvalid(phone)) { 30 | return Result.error(HttpStatus.HTTP_BAD_REQUEST, "手机号格式错误"); 31 | } 32 | String key = RedisConstants.LOGIN_USER_CODE_KEY + phone; 33 | 34 | // 2.查看缓存中是否已经存在,得到剩余TTL 35 | Long expire = redisCacheUtils.getExpire(key); 36 | 37 | // 3.存在并且剩余时长大于4分钟则不可再次发送验证码 38 | if(expire > RedisConstants.LOGIN_USER_CODE_AGAIN_TTL){ 39 | return Result.error(701,"发送失败,验证码仍在有效期内"); 40 | } 41 | 42 | // 4.验证码不存在或者剩余时长小于四分钟,则可以继续发送验证码 --> 先生成六位随机数 43 | String code = RandomUtil.randomNumbers(6); 44 | 45 | // 关于恶意并发的问题,在短信云平台已经自动做了处理,这里就无需处理 46 | 47 | // 5.先存储到 redis,附带该手机号已经验证的次数,初始化为0,进行校验时分割得到 code 和 count。 48 | redisCacheUtils.setCacheObject(key,code+",0",RedisConstants.LOGIN_USER_CODE_TTL); 49 | 50 | // 6.发送短信验证到手机 51 | boolean success = aliSmsTemplateUtils.sendLoginCode(phone, code); 52 | 53 | // 7.返回 54 | if (success){ 55 | return Result.ok(); 56 | }else{ 57 | // 移除redis中的缓存记录 58 | redisCacheUtils.deleteObject(RedisConstants.LOGIN_USER_CODE_KEY + phone); 59 | return Result.error(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/util/AliSmsTemplateUtils.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.util; 2 | 3 | import cn.hutool.json.JSONUtil; 4 | import com.aliyun.auth.credentials.Credential; 5 | import com.aliyun.auth.credentials.provider.StaticCredentialProvider; 6 | import com.aliyun.sdk.service.dysmsapi20170525.AsyncClient; 7 | import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsRequest; 8 | import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsResponse; 9 | import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsResponseBody; 10 | import darabonba.core.client.ClientOverrideConfiguration; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Data; 13 | import lombok.NoArgsConstructor; 14 | import lombok.extern.slf4j.Slf4j; 15 | 16 | import java.time.Duration; 17 | import java.util.HashMap; 18 | import java.util.concurrent.CompletableFuture; 19 | 20 | /** 21 | * 阿里云发送短信工具类,需要注入容器 22 | * 23 | * @author 狐狸半面添 24 | * @create 2023-04-01 15:44 25 | */ 26 | @Data 27 | @NoArgsConstructor 28 | @AllArgsConstructor 29 | @Slf4j 30 | public class AliSmsTemplateUtils { 31 | 32 | /** 33 | * 子用户的访问键 34 | */ 35 | private String accessKeyId; 36 | /** 37 | * 子用户的访问密钥 38 | */ 39 | private String accessKeySecret; 40 | /** 41 | * 签名名称 42 | */ 43 | private String signName; 44 | /** 45 | * 登录短信模板的code 46 | */ 47 | private String loginTemplateCode; 48 | 49 | 50 | 51 | /** 52 | * 发送登录验证码 53 | * 54 | * @param phone 手机号 55 | * @return true-发送成功,false-发送失败 56 | */ 57 | public boolean sendLoginCode(String phone, String code){ 58 | // 配置凭据身份验证信息,包括 accessKeyId 与 accessKeySecret 59 | StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder() 60 | .accessKeyId(accessKeyId) 61 | .accessKeySecret(accessKeySecret) 62 | .build()); 63 | 64 | // 客户端配置 65 | AsyncClient client = AsyncClient.builder() 66 | // 地域id,这里是乌兰察布 67 | .region("cn-wulanchabu") 68 | .credentialsProvider(provider) 69 | .overrideConfiguration( 70 | ClientOverrideConfiguration.create() 71 | // 访问的域名,不要修改 72 | .setEndpointOverride("dysmsapi.aliyuncs.com") 73 | // 设置超时时长 74 | .setConnectTimeout(Duration.ofSeconds(30)) 75 | ) 76 | .build(); 77 | 78 | // 请求参数设置 79 | HashMap contentParam = new HashMap<>(); 80 | contentParam.put("code",code); 81 | SendSmsRequest sendSmsRequest = SendSmsRequest.builder() 82 | .signName(signName) 83 | .templateCode(loginTemplateCode) 84 | .phoneNumbers(phone) 85 | .templateParam(JSONUtil.toJsonStr(contentParam)) 86 | .build(); 87 | 88 | CompletableFuture response = null; 89 | 90 | try { 91 | // 异步获取API请求的返回值 92 | response = client.sendSms(sendSmsRequest); 93 | 94 | // 同步获取API请求的返回值 95 | SendSmsResponseBody body = response.get().getBody(); 96 | // 判断是否发送成功 97 | if ("OK".equalsIgnoreCase(body.getCode())){ 98 | return true; 99 | }else{ 100 | return false; 101 | } 102 | } catch (Exception e) { 103 | // 日志记录 104 | log.error("发送登录短信验证码发生异常:{}",e.getMessage()); 105 | // 存入 mysql 106 | return false; 107 | } finally { 108 | // 关闭客户端 109 | client.close(); 110 | } 111 | 112 | // 异步处理返回值 113 | // response.thenAccept(resp -> { 114 | // JSONObject respInfo = JSONUtil.parseObj(resp.getBody()); 115 | // if (!"OK".equals(respInfo.getStr("code"))){ 116 | // // 说明发送失败 117 | // log.error("发送登录短信验证码发生异常:状态码-{},状态描述-{}",respInfo.getStr("code"),respInfo.getStr("message")); 118 | // // 存入 mysql 119 | // } 120 | // System.out.println(); 121 | // }).exceptionally(throwable -> { 122 | // // 1.处理异常,日志记录 123 | // log.error("发送登录短信验证码发生异常:{}",throwable.getMessage()); 124 | // // 2.存入 mysql 125 | // return null; 126 | // }); 127 | // 6.1 发送失败,则移除 redis 中的验证码缓存信息,并返回 128 | 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/util/RedisCacheUtils.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.util; 2 | 3 | import cn.hutool.core.util.BooleanUtil; 4 | import org.springframework.data.redis.core.BoundSetOperations; 5 | import org.springframework.data.redis.core.HashOperations; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.core.ValueOperations; 8 | 9 | import java.util.*; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * Redis缓存工具 14 | * 15 | * @author 狐狸半面添 16 | * @create 2023-04-01 16:39 17 | */ 18 | @SuppressWarnings(value = {"unchecked", "rawtypes"}) 19 | public class RedisCacheUtils { 20 | private final RedisTemplate redisTemplate; 21 | 22 | public RedisCacheUtils(RedisTemplate redisTemplate){ 23 | this.redisTemplate = redisTemplate; 24 | } 25 | 26 | /** 27 | * 获取剩余TTL有效期 28 | * 29 | * @param key 缓存的键值 30 | * @return 剩余有效期,单位:s 31 | */ 32 | public Long getExpire(final String key){ 33 | return redisTemplate.getExpire(key); 34 | } 35 | 36 | /** 37 | * 缓存基本的对象,Long、String、实体类等 38 | * 39 | * @param key 缓存的键值 40 | * @param value 缓存的值 41 | */ 42 | public void setCacheObject(final String key, final T value) { 43 | redisTemplate.opsForValue().set(key, value); 44 | } 45 | 46 | /** 47 | * 缓存基本的对象,Long、String、实体类等 48 | * 49 | * @param key 缓存的键值 50 | * @param value 缓存的值 51 | * @param timeout 时间 52 | * @param timeUnit 时间颗粒度 53 | */ 54 | public void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit) { 55 | redisTemplate.opsForValue().set(key, value, timeout, timeUnit); 56 | } 57 | 58 | /** 59 | * 缓存基本的对象,Long、String、实体类等 60 | * 61 | * @param key 缓存的键值 62 | * @param value 缓存的值 63 | * @param timeout 时间 64 | */ 65 | public void setCacheObject(final String key, final T value, final Long timeout) { 66 | redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); 67 | } 68 | 69 | /** 70 | * 设置有效时间 71 | * 72 | * @param key Redis键 73 | * @param timeout 超时时间 74 | * @return true=设置成功;false=设置失败 75 | */ 76 | public boolean expire(final String key, final long timeout) { 77 | return expire(key, timeout, TimeUnit.SECONDS); 78 | } 79 | 80 | /** 81 | * 设置有效时间 82 | * 83 | * @param key Redis键 84 | * @param timeout 超时时间 85 | * @param unit 时间单位 86 | * @return true=设置成功;false=设置失败 87 | */ 88 | public boolean expire(final String key, final long timeout, final TimeUnit unit) { 89 | return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, unit)); 90 | } 91 | 92 | /** 93 | * 获得缓存的基本对象。 94 | * 95 | * @param key 缓存键值 96 | * @return 缓存键值对应的数据 97 | */ 98 | public T getCacheObject(final String key) { 99 | ValueOperations operation = redisTemplate.opsForValue(); 100 | return operation.get(key); 101 | } 102 | 103 | /** 104 | * 删除单个对象 105 | * 106 | * @param key 107 | */ 108 | public boolean deleteObject(final String key) { 109 | return Boolean.TRUE.equals(redisTemplate.delete(key)); 110 | } 111 | 112 | /** 113 | * 删除集合对象 114 | * 115 | * @param collection 多个对象 116 | * @return 117 | */ 118 | public long deleteObject(final Collection collection) { 119 | return redisTemplate.delete(collection); 120 | } 121 | 122 | /** 123 | * 缓存List数据 124 | * 125 | * @param key 缓存的键值 126 | * @param dataList 待缓存的List数据 127 | * @return 缓存的对象 128 | */ 129 | public long setCacheList(final String key, final List dataList) { 130 | Long count = redisTemplate.opsForList().rightPushAll(key, dataList); 131 | return count == null ? 0 : count; 132 | } 133 | 134 | /** 135 | * 获得缓存的list对象 136 | * 137 | * @param key 缓存的键值 138 | * @return 缓存键值对应的数据 139 | */ 140 | public List getCacheList(final String key) { 141 | return redisTemplate.opsForList().range(key, 0, -1); 142 | } 143 | 144 | /** 145 | * 缓存Set 146 | * 147 | * @param key 缓存键值 148 | * @param dataSet 缓存的数据 149 | * @return 缓存数据的对象 150 | */ 151 | public BoundSetOperations setCacheSet(final String key, final Set dataSet) { 152 | BoundSetOperations setOperation = redisTemplate.boundSetOps(key); 153 | Iterator it = dataSet.iterator(); 154 | while (it.hasNext()) { 155 | setOperation.add(it.next()); 156 | } 157 | return setOperation; 158 | } 159 | 160 | /** 161 | * 获得缓存的set 162 | * 163 | * @param key 164 | * @return 165 | */ 166 | public Set getCacheSet(final String key) { 167 | return redisTemplate.opsForSet().members(key); 168 | } 169 | 170 | /** 171 | * 缓存Map 172 | * 173 | * @param key 174 | * @param dataMap 175 | */ 176 | public void setCacheMap(final String key, final Map dataMap) { 177 | if (dataMap != null) { 178 | redisTemplate.opsForHash().putAll(key, dataMap); 179 | } 180 | } 181 | 182 | /** 183 | * 获得缓存的Map 184 | * 185 | * @param key 186 | * @return 187 | */ 188 | public Map getCacheMap(final String key) { 189 | return redisTemplate.opsForHash().entries(key); 190 | } 191 | 192 | /** 193 | * 往Hash中存入数据 194 | * 195 | * @param key Redis键 196 | * @param hKey Hash键 197 | * @param value 值 198 | */ 199 | public void setCacheMapValue(final String key, final String hKey, final T value) { 200 | redisTemplate.opsForHash().put(key, hKey, value); 201 | } 202 | 203 | /** 204 | * 获取Hash中的数据 205 | * 206 | * @param key Redis键 207 | * @param hKey Hash键 208 | * @return Hash中的对象 209 | */ 210 | public T getCacheMapValue(final String key, final String hKey) { 211 | HashOperations opsForHash = redisTemplate.opsForHash(); 212 | return opsForHash.get(key, hKey); 213 | } 214 | 215 | /** 216 | * 删除Hash中的数据 217 | * 218 | * @param key 219 | * @param hKey 220 | */ 221 | public void delCacheMapValue(final String key, final String hKey) { 222 | HashOperations hashOperations = redisTemplate.opsForHash(); 223 | hashOperations.delete(key, hKey); 224 | } 225 | 226 | /** 227 | * 获取多个Hash中的数据 228 | * 229 | * @param key Redis键 230 | * @param hKeys Hash键集合 231 | * @return Hash对象集合 232 | */ 233 | public List getMultiCacheMapValue(final String key, final Collection hKeys) { 234 | return redisTemplate.opsForHash().multiGet(key, hKeys); 235 | } 236 | 237 | /** 238 | * 获得缓存的基本对象列表 239 | * 240 | * @param pattern 字符串前缀 241 | * @return 对象列表 242 | */ 243 | public Collection keys(final String pattern) { 244 | return redisTemplate.keys(pattern); 245 | } 246 | 247 | /** 248 | * 判断某个键是否存在 249 | * 250 | * @param key 键 251 | * @return true-存在 false-不存在 252 | */ 253 | public boolean existKey(final String key){ 254 | return BooleanUtil.isTrue(redisTemplate.hasKey(key)); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /sms-demo/src/main/java/com/fox/sms/util/RegexUtils.java: -------------------------------------------------------------------------------- 1 | package com.fox.sms.util; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | 5 | /** 6 | * 校验格式工具类 7 | * 8 | * @author 狐狸半面添 9 | * @create 2023-04-01 16:32 10 | */ 11 | public class RegexUtils { 12 | 13 | 14 | /** 15 | * 正则表达式模板 16 | * 17 | * @author 狐狸半面添 18 | * @create 2023-01-16 22:39 19 | */ 20 | public static class RegexPatterns { 21 | /** 22 | * 手机号正则 23 | */ 24 | public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$"; 25 | } 26 | 27 | /** 28 | * 是否是无效手机格式 29 | * 30 | * @param phone 要校验的手机号 31 | * @return true:符合,false:不符合 32 | */ 33 | public static boolean isPhoneInvalid(String phone) { 34 | return mismatch(phone, RegexPatterns.PHONE_REGEX); 35 | } 36 | 37 | /** 38 | * 校验是否不符合正则格式 39 | * 40 | * @param str 字符串 41 | * @param regex 正则表达式 42 | * @return true:符合 false:不符合 43 | */ 44 | private static boolean mismatch(String str, String regex) { 45 | if (StrUtil.isBlank(str)) { 46 | return true; 47 | } 48 | return !str.matches(regex); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sms-demo/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 10505 3 | spring: 4 | redis: 5 | host: 8.130.97.145 6 | port: 6379 7 | password: 123456 8 | lettuce: 9 | pool: 10 | max-active: 10 11 | max-idle: 10 12 | min-idle: 1 13 | time-between-eviction-runs: 10s 14 | database: 0 15 | sms: 16 | ali: 17 | # 子用户的访问键 18 | accessKeyId: LTAI5tFhzvktt9U5j4ak**** 19 | # 子用户的访问密钥 20 | accessKeySecret: hYLiytNltwB1pOetnBiNcXPmXq**** 21 | # 签名名称 22 | signName: 逐浪教育 23 | # 登录短信模板的code 24 | loginTemplateCode: SMS_275395309 25 | -------------------------------------------------------------------------------- /xfxh-web-simple-demo/README.md: -------------------------------------------------------------------------------- 1 | # 讯飞星火大模型后端接口 2 | 3 | 启动项目后,直接使用 GET 请求访问 http://localhost:8080/test/sendQuestion?question=hello 4 | 5 | 讯飞官方web文档:https://www.xfyun.cn/doc/spark/Web.html 6 | 7 | # 说明 8 | 该后端接口的大致实现逻辑: 9 | 10 | 1. 以 GET 方式访问 SpringBoot 后端接口; 11 | 2. 根据你的配置信息生成通用**鉴权URL**,并携带 question 建立 **websocket 连接**; 12 | 3. 星火大模型**流式返回**生成的回答; 13 | 4. 当大模型返回给后端的响应中出现 **已返回全部回答的标识status** 后,后端关闭 websocket 连接; 14 | 5. 后端将生成的完整回答响应给接口调用者。 15 | 16 | 如果你想了解更详细的与星火大模型之间的参数说明,请参考 [星火认知大模型Web文档](https://www.xfyun.cn/doc/spark/Web.html) 17 | 18 | 该项目后端接口的实现功能: 19 | 20 | - 能回答单个问题,但不支持上下文; 21 | - 对星火大模型限制的 QPS 做了处理; 22 | - 通过配置文件可以规定大模型回复问题的最大响应时长; 23 | 24 | 如果想要使用支持上下文的接口,只需要找到 **xfxh-web-support-context-demo** 模块,它在 **xfxh-web-simple-demo** 模块基础上实现了基于上下文的回答,该增强模块的后端接口说明: 25 | 26 | - 将上下文内容信息保存到了内存中,可以通过配置文件设置保存的上下文内容条数以及用户信息数; 27 | - 一份交互记录指的是两条上下文内容,分别是用户的问题和大模型的回答; 28 | - 支持了唯一标识的用户必须先等他的上一条问题的回答生成才能发送新的问题; 29 | - 由于信息存储在内存中,因此设置了定时任务检查用户是否过期并移除,这个过期时间可以在配置文件中设置。 30 | 31 | 代码还是易懂的,如果想了解如何实现的,建议先看完有完整注释的 **xfxh-web-simple-demo** 模块,再去看 **xfxh-web-support-context-demo** 模块。**xfxh-web-support-context-demo** 模块只是在 **xfxh-web-simple-demo** 模块进行了补充/增强。 32 | 33 | -------------------------------------------------------------------------------- /xfxh-web-simple-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 2.7.0 11 | 12 | 13 | 14 | com.zhulang 15 | xfxh-web-simple-demo 16 | 1.0-SNAPSHOT 17 | 18 | 19 | 8 20 | 8 21 | UTF-8 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-configuration-processor 28 | true 29 | 30 | 31 | cn.hutool 32 | hutool-all 33 | 5.8.18 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-web 38 | 39 | 40 | com.alibaba 41 | fastjson 42 | 1.2.67 43 | 44 | 45 | org.java-websocket 46 | Java-WebSocket 47 | 1.3.8 48 | 49 | 50 | com.squareup.okhttp3 51 | okhttp 52 | 4.10.0 53 | 54 | 55 | org.projectlombok 56 | lombok 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /xfxh-web-simple-demo/src/main/java/com/zhulang/xfxhsimple/XfXhApplication.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxhsimple; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author 狐狸半面添 8 | * @create 2023-09-20 1:42 9 | */ 10 | @SpringBootApplication 11 | public class XfXhApplication { 12 | public static void main(String[] args) { 13 | SpringApplication.run(XfXhApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /xfxh-web-simple-demo/src/main/java/com/zhulang/xfxhsimple/component/XfXhStreamClient.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxhsimple.component; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.zhulang.xfxhsimple.config.XfXhConfig; 5 | import com.zhulang.xfxhsimple.dto.MsgDTO; 6 | import com.zhulang.xfxhsimple.dto.RequestDTO; 7 | import lombok.extern.slf4j.Slf4j; 8 | import okhttp3.*; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.annotation.Resource; 13 | import javax.crypto.Mac; 14 | import javax.crypto.spec.SecretKeySpec; 15 | import java.net.URL; 16 | import java.nio.charset.StandardCharsets; 17 | import java.text.SimpleDateFormat; 18 | import java.util.*; 19 | 20 | /** 21 | * @author 狐狸半面添 22 | * @create 2023-09-15 1:10 23 | */ 24 | @Component 25 | @Slf4j 26 | public class XfXhStreamClient { 27 | @Resource 28 | private XfXhConfig xfXhConfig; 29 | 30 | @Value("${xfxh.QPS}") 31 | private int connectionTokenCount; 32 | 33 | /** 34 | * 获取令牌 35 | */ 36 | public static int GET_TOKEN_STATUS = 0; 37 | /** 38 | * 归还令牌 39 | */ 40 | public static int BACK_TOKEN_STATUS = 1; 41 | 42 | /** 43 | * 操作令牌 44 | * 45 | * @param status 0-获取令牌 1-归还令牌 46 | * @return 是否操作成功 47 | */ 48 | public synchronized boolean operateToken(int status) { 49 | if (status == GET_TOKEN_STATUS) { 50 | // 获取令牌 51 | if (connectionTokenCount != 0) { 52 | // 说明还有令牌,将令牌数减一 53 | connectionTokenCount -= 1; 54 | return true; 55 | } else { 56 | return false; 57 | } 58 | } else { 59 | // 放回令牌 60 | connectionTokenCount += 1; 61 | return true; 62 | } 63 | } 64 | 65 | /** 66 | * 发送消息 67 | * 68 | * @param uid 每个用户的id,用于区分不同用户 69 | * @param msgList 发送给大模型的消息,可以包含上下文内容 70 | * @return 获取websocket连接,以便于我们在获取完整大模型回复后手动关闭连接 71 | */ 72 | public WebSocket sendMsg(String uid, List msgList, WebSocketListener listener) { 73 | // 获取鉴权url 74 | String authUrl = this.getAuthUrl(); 75 | // 鉴权方法生成失败,直接返回 null 76 | if (authUrl == null) { 77 | return null; 78 | } 79 | OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); 80 | // 将 https/http 连接替换为 ws/wss 连接 81 | String url = authUrl.replace("http://", "ws://").replace("https://", "wss://"); 82 | Request request = new Request.Builder().url(url).build(); 83 | // 建立 wss 连接 84 | WebSocket webSocket = okHttpClient.newWebSocket(request, listener); 85 | // 组装请求参数 86 | RequestDTO requestDTO = getRequestParam(uid, msgList); 87 | // 发送请求 88 | webSocket.send(JSONObject.toJSONString(requestDTO)); 89 | return webSocket; 90 | } 91 | 92 | /** 93 | * 生成鉴权方法,具体实现不用关心,这是讯飞官方定义的鉴权方式 94 | * 95 | * @return 鉴权访问大模型的路径 96 | */ 97 | public String getAuthUrl() { 98 | try { 99 | URL url = new URL(xfXhConfig.getHostUrl()); 100 | // 时间 101 | SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 102 | format.setTimeZone(TimeZone.getTimeZone("GMT")); 103 | String date = format.format(new Date()); 104 | // 拼接 105 | String preStr = "host: " + url.getHost() + "\n" + 106 | "date: " + date + "\n" + 107 | "GET " + url.getPath() + " HTTP/1.1"; 108 | // SHA256加密 109 | Mac mac = Mac.getInstance("hmacsha256"); 110 | SecretKeySpec spec = new SecretKeySpec(xfXhConfig.getApiSecret().getBytes(StandardCharsets.UTF_8), "hmacsha256"); 111 | mac.init(spec); 112 | 113 | byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8)); 114 | // Base64加密 115 | String sha = Base64.getEncoder().encodeToString(hexDigits); 116 | // 拼接 117 | String authorizationOrigin = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", xfXhConfig.getApiKey(), "hmac-sha256", "host date request-line", sha); 118 | // 拼接地址 119 | HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder(). 120 | addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorizationOrigin.getBytes(StandardCharsets.UTF_8))). 121 | addQueryParameter("date", date). 122 | addQueryParameter("host", url.getHost()). 123 | build(); 124 | 125 | return httpUrl.toString(); 126 | } catch (Exception e) { 127 | log.error("鉴权方法中发生错误:" + e.getMessage()); 128 | return null; 129 | } 130 | } 131 | 132 | /** 133 | * 获取请求参数 134 | * 135 | * @param uid 每个用户的id,用于区分不同用户 136 | * @param msgList 发送给大模型的消息,可以包含上下文内容 137 | * @return 请求DTO,该 DTO 转 json 字符串后生成的格式参考 resources/demo-json/request.json 138 | */ 139 | public RequestDTO getRequestParam(String uid, List msgList) { 140 | RequestDTO requestDTO = new RequestDTO(); 141 | requestDTO.setHeader(new RequestDTO.HeaderDTO(xfXhConfig.getAppId(), uid)); 142 | requestDTO.setParameter(new RequestDTO.ParameterDTO(new RequestDTO.ParameterDTO.ChatDTO(xfXhConfig.getDomain(), xfXhConfig.getTemperature(), xfXhConfig.getMaxTokens()))); 143 | requestDTO.setPayload(new RequestDTO.PayloadDTO(new RequestDTO.PayloadDTO.MessageDTO(msgList))); 144 | return requestDTO; 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /xfxh-web-simple-demo/src/main/java/com/zhulang/xfxhsimple/config/XfXhConfig.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxhsimple.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * @author 狐狸半面添 9 | * @create 2023-09-15 0:46 10 | */ 11 | @Configuration 12 | @ConfigurationProperties(prefix = "xfxh") 13 | @Data 14 | public class XfXhConfig { 15 | /** 16 | * 服务引擎使用 讯飞星火认知大模型V2.0,如果使用 V1.5 需要将 hostUrl 修改为 https://spark-api.xf-yun.com/v1.1/chat 17 | */ 18 | private String hostUrl; 19 | /** 20 | * 发送请求时指定的访问领域,如果是 V1.5版本 设置为 general,如果是 V2版本 设置为 generalv2 21 | */ 22 | private String domain; 23 | /** 24 | * 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高。取值 [0,1] 25 | */ 26 | private Float temperature; 27 | /** 28 | * 模型回答的tokens的最大长度,V1.5取值为[1,4096],V2.0取值为[1,8192]。 29 | */ 30 | private Integer maxTokens; 31 | /** 32 | * 大模型回复问题的最大响应时长,单位 s 33 | */ 34 | private Integer maxResponseTime; 35 | /** 36 | * 用于权限验证,从服务接口认证信息中获取 37 | */ 38 | private String appId; 39 | /** 40 | * 用于权限验证,从服务接口认证信息中获取 41 | */ 42 | private String apiKey; 43 | /** 44 | * 用于权限验证,从服务接口认证信息中获取 45 | */ 46 | private String apiSecret; 47 | 48 | } 49 | -------------------------------------------------------------------------------- /xfxh-web-simple-demo/src/main/java/com/zhulang/xfxhsimple/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxhsimple.controller; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.zhulang.xfxhsimple.component.XfXhStreamClient; 5 | import com.zhulang.xfxhsimple.config.XfXhConfig; 6 | import com.zhulang.xfxhsimple.dto.MsgDTO; 7 | import com.zhulang.xfxhsimple.listener.XfXhWebSocketListener; 8 | import lombok.extern.slf4j.Slf4j; 9 | import okhttp3.WebSocket; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import javax.annotation.Resource; 16 | import java.util.Collections; 17 | import java.util.UUID; 18 | 19 | /** 20 | * @author 狐狸半面添 21 | * @create 2023-09-20 1:42 22 | */ 23 | @RestController 24 | @RequestMapping("/test") 25 | @Slf4j 26 | public class TestController { 27 | 28 | @Resource 29 | private XfXhStreamClient xfXhStreamClient; 30 | 31 | @Resource 32 | private XfXhConfig xfXhConfig; 33 | 34 | 35 | /** 36 | * 发送问题 37 | * 38 | * @param question 问题 39 | * @return 星火大模型的回答 40 | */ 41 | @GetMapping("/sendQuestion") 42 | public String sendQuestion(@RequestParam("question") String question) { 43 | // 如果是无效字符串,则不对大模型进行请求 44 | if (StrUtil.isBlank(question)) { 45 | return "无效问题,请重新输入"; 46 | } 47 | // 获取连接令牌 48 | if (!xfXhStreamClient.operateToken(XfXhStreamClient.GET_TOKEN_STATUS)) { 49 | return "当前大模型连接数过多,请稍后再试"; 50 | } 51 | 52 | // 创建消息对象 53 | MsgDTO msgDTO = MsgDTO.createUserMsg(question); 54 | // 创建监听器 55 | XfXhWebSocketListener listener = new XfXhWebSocketListener(); 56 | // 发送问题给大模型,生成 websocket 连接 57 | WebSocket webSocket = xfXhStreamClient.sendMsg(UUID.randomUUID().toString().substring(0, 10), Collections.singletonList(msgDTO), listener); 58 | if (webSocket == null) { 59 | // 归还令牌 60 | xfXhStreamClient.operateToken(XfXhStreamClient.BACK_TOKEN_STATUS); 61 | return "系统内部错误,请联系管理员"; 62 | } 63 | try { 64 | int count = 0; 65 | // 为了避免死循环,设置循环次数来定义超时时长 66 | int maxCount = xfXhConfig.getMaxResponseTime() * 5; 67 | while (count <= maxCount) { 68 | Thread.sleep(200); 69 | if (listener.isWsCloseFlag()) { 70 | break; 71 | } 72 | count++; 73 | } 74 | if (count > maxCount) { 75 | return "大模型响应超时,请联系管理员"; 76 | } 77 | // 响应大模型的答案 78 | return listener.getAnswer().toString(); 79 | } catch (InterruptedException e) { 80 | log.error("错误:" + e.getMessage()); 81 | return "系统内部错误,请联系管理员"; 82 | } finally { 83 | // 关闭 websocket 连接 84 | webSocket.close(1000, ""); 85 | // 归还令牌 86 | xfXhStreamClient.operateToken(XfXhStreamClient.BACK_TOKEN_STATUS); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /xfxh-web-simple-demo/src/main/java/com/zhulang/xfxhsimple/dto/MsgDTO.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxhsimple.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * 消息对象 10 | * 11 | * @author 狐狸半面添 12 | * @create 2023-09-15 0:42 13 | */ 14 | @Data 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | @JsonInclude(JsonInclude.Include.NON_NULL) 18 | public class MsgDTO { 19 | /** 20 | * 角色 21 | */ 22 | private String role; 23 | /** 24 | * 消息内容 25 | */ 26 | private String content; 27 | /** 28 | * 响应结果字段:结果序号,取值为[0,10]; 当前为保留字段,开发者可忽略 29 | */ 30 | private Integer index; 31 | 32 | public static final String ROLE_USER = "user"; 33 | public static final String ROLE_ASSISTANT = "assistant"; 34 | 35 | public static MsgDTO createUserMsg(String content) { 36 | return new MsgDTO(ROLE_USER, content, null); 37 | } 38 | 39 | public static MsgDTO createAssistantMsg(String content) { 40 | return new MsgDTO(ROLE_ASSISTANT, content, null); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /xfxh-web-simple-demo/src/main/java/com/zhulang/xfxhsimple/dto/RequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxhsimple.dto; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * 请求参数 13 | * 对应生成的 JSON 结构参考 resources/demo-json/request.json 14 | * 15 | * @author 狐狸半面添 16 | * @create 2023-09-15 0:42 17 | */ 18 | @NoArgsConstructor 19 | @Data 20 | public class RequestDTO { 21 | 22 | @JsonProperty("header") 23 | private HeaderDTO header; 24 | @JsonProperty("parameter") 25 | private ParameterDTO parameter; 26 | @JsonProperty("payload") 27 | private PayloadDTO payload; 28 | 29 | @NoArgsConstructor 30 | @Data 31 | @AllArgsConstructor 32 | public static class HeaderDTO { 33 | /** 34 | * 应用appid,从开放平台控制台创建的应用中获取 35 | */ 36 | @JSONField(name = "app_id") 37 | private String appId; 38 | /** 39 | * 每个用户的id,用于区分不同用户,最大长度32 40 | */ 41 | @JSONField(name = "uid") 42 | private String uid; 43 | } 44 | 45 | @NoArgsConstructor 46 | @Data 47 | @AllArgsConstructor 48 | public static class ParameterDTO { 49 | private ChatDTO chat; 50 | 51 | @NoArgsConstructor 52 | @Data 53 | @AllArgsConstructor 54 | public static class ChatDTO { 55 | /** 56 | * 指定访问的领域,general指向V1.5版本 generalv2指向V2版本。注意:不同的取值对应的url也不一样! 57 | */ 58 | @JsonProperty("domain") 59 | private String domain; 60 | /** 61 | * 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高 62 | */ 63 | @JsonProperty("temperature") 64 | private Float temperature; 65 | /** 66 | * 模型回答的tokens的最大长度 67 | */ 68 | @JSONField(name = "max_tokens") 69 | private Integer maxTokens; 70 | } 71 | } 72 | 73 | @NoArgsConstructor 74 | @Data 75 | @AllArgsConstructor 76 | public static class PayloadDTO { 77 | @JsonProperty("message") 78 | private MessageDTO message; 79 | 80 | @NoArgsConstructor 81 | @Data 82 | @AllArgsConstructor 83 | public static class MessageDTO { 84 | @JsonProperty("text") 85 | private List text; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /xfxh-web-simple-demo/src/main/java/com/zhulang/xfxhsimple/dto/ResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxhsimple.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * 返回参数 11 | * 对应生成的 JSON 结构参考 resources/demo-json/response.json 12 | * 13 | * @author 狐狸半面添 14 | * @create 2023-09-15 0:42 15 | */ 16 | @NoArgsConstructor 17 | @Data 18 | public class ResponseDTO { 19 | 20 | @JsonProperty("header") 21 | private HeaderDTO header; 22 | @JsonProperty("payload") 23 | private PayloadDTO payload; 24 | 25 | @NoArgsConstructor 26 | @Data 27 | public static class HeaderDTO { 28 | /** 29 | * 错误码,0表示正常,非0表示出错 30 | */ 31 | @JsonProperty("code") 32 | private Integer code; 33 | /** 34 | * 会话是否成功的描述信息 35 | */ 36 | @JsonProperty("message") 37 | private String message; 38 | /** 39 | * 会话的唯一id,用于讯飞技术人员查询服务端会话日志使用,出现调用错误时建议留存该字段 40 | */ 41 | @JsonProperty("sid") 42 | private String sid; 43 | /** 44 | * 会话状态,取值为[0,1,2];0代表首次结果;1代表中间结果;2代表最后一个结果 45 | */ 46 | @JsonProperty("status") 47 | private Integer status; 48 | } 49 | 50 | @NoArgsConstructor 51 | @Data 52 | public static class PayloadDTO { 53 | @JsonProperty("choices") 54 | private ChoicesDTO choices; 55 | /** 56 | * 在最后一次结果返回 57 | */ 58 | @JsonProperty("usage") 59 | private UsageDTO usage; 60 | 61 | @NoArgsConstructor 62 | @Data 63 | public static class ChoicesDTO { 64 | /** 65 | * 文本响应状态,取值为[0,1,2]; 0代表首个文本结果;1代表中间文本结果;2代表最后一个文本结果 66 | */ 67 | @JsonProperty("status") 68 | private Integer status; 69 | /** 70 | * 返回的数据序号,取值为[0,9999999] 71 | */ 72 | @JsonProperty("seq") 73 | private Integer seq; 74 | /** 75 | * 响应文本 76 | */ 77 | @JsonProperty("text") 78 | private List text; 79 | 80 | } 81 | 82 | @NoArgsConstructor 83 | @Data 84 | public static class UsageDTO { 85 | @JsonProperty("text") 86 | private TextDTO text; 87 | 88 | @NoArgsConstructor 89 | @Data 90 | public static class TextDTO { 91 | /** 92 | * 保留字段,可忽略 93 | */ 94 | @JsonProperty("question_tokens") 95 | private Integer questionTokens; 96 | /** 97 | * 包含历史问题的总tokens大小 98 | */ 99 | @JsonProperty("prompt_tokens") 100 | private Integer promptTokens; 101 | /** 102 | * 回答的tokens大小 103 | */ 104 | @JsonProperty("completion_tokens") 105 | private Integer completionTokens; 106 | /** 107 | * prompt_tokens和completion_tokens的和,也是本次交互计费的tokens大小 108 | */ 109 | @JsonProperty("total_tokens") 110 | private Integer totalTokens; 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /xfxh-web-simple-demo/src/main/java/com/zhulang/xfxhsimple/listener/XfXhWebSocketListener.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxhsimple.listener; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.zhulang.xfxhsimple.dto.MsgDTO; 5 | import com.zhulang.xfxhsimple.dto.ResponseDTO; 6 | import lombok.extern.slf4j.Slf4j; 7 | import okhttp3.Response; 8 | import okhttp3.WebSocket; 9 | import okhttp3.WebSocketListener; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.springframework.lang.Nullable; 12 | 13 | /** 14 | * @author 狐狸半面添 15 | * @create 2023-09-15 1:11 16 | */ 17 | @Slf4j 18 | public class XfXhWebSocketListener extends WebSocketListener { 19 | private StringBuilder answer = new StringBuilder(); 20 | 21 | private boolean wsCloseFlag = false; 22 | 23 | public StringBuilder getAnswer() { 24 | return answer; 25 | } 26 | 27 | public boolean isWsCloseFlag() { 28 | return wsCloseFlag; 29 | } 30 | 31 | @Override 32 | public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { 33 | super.onOpen(webSocket, response); 34 | } 35 | 36 | @Override 37 | public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { 38 | super.onMessage(webSocket, text); 39 | // 将大模型回复的 JSON 文本转为 ResponseDTO 对象 40 | ResponseDTO responseData = JSONObject.parseObject(text, ResponseDTO.class); 41 | // 如果响应数据中的 header 的 code 值不为 0,则表示响应错误 42 | if (responseData.getHeader().getCode() != 0) { 43 | // 日志记录 44 | log.error("发生错误,错误码为:" + responseData.getHeader().getCode() + "; " + "信息:" + responseData.getHeader().getMessage()); 45 | // 设置回答 46 | this.answer = new StringBuilder("大模型响应错误,请稍后再试"); 47 | // 关闭连接标识 48 | wsCloseFlag = true; 49 | return; 50 | } 51 | // 将回答进行拼接 52 | for (MsgDTO msgDTO : responseData.getPayload().getChoices().getText()) { 53 | this.answer.append(msgDTO.getContent()); 54 | } 55 | // 对最后一个文本结果进行处理 56 | if (2 == responseData.getHeader().getStatus()) { 57 | wsCloseFlag = true; 58 | } 59 | } 60 | 61 | @Override 62 | public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) { 63 | super.onFailure(webSocket, t, response); 64 | } 65 | 66 | @Override 67 | public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { 68 | super.onClosed(webSocket, code, reason); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /xfxh-web-simple-demo/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | xfxh: 2 | # 服务引擎使用 讯飞星火认知大模型V2.0,如果使用 V1.5 需要将 hostUrl 修改为 https://spark-api.xf-yun.com/v1.1/chat 3 | hostUrl: https://spark-api.xf-yun.com/v2.1/chat 4 | # 发送请求时指定的访问领域,如果是 V1.5版本 设置为 general,如果是 V2版本 设置为 generalv2 5 | domain: generalv2 6 | # 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高。取值 [0,1] 7 | temperature: 0.5 8 | # 模型回答的tokens的最大长度,V1.5取值为[1,4096],V2.0取值为[1,8192]。 9 | maxTokens: 2048 10 | # 大模型回复问题的最大响应时长,单位 s 11 | maxResponseTime: 30 12 | # 允许同时连接大模型的 websocket 数,如果是普通(免费)用户为 2,超过这个数连接响应会报错,具体参考官网。 13 | QPS: 2 14 | # 用于权限验证,从服务接口认证信息中获取 15 | appId: 16 | # 用于权限验证,从服务接口认证信息中获取 17 | apiKey: 18 | # 用于权限验证,从服务接口认证信息中获取 19 | apiSecret: -------------------------------------------------------------------------------- /xfxh-web-simple-demo/src/main/resources/demo-json/request.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "app_id": "12345", 4 | "uid": "12345" 5 | }, 6 | "parameter": { 7 | "chat": { 8 | "domain": "general", 9 | "temperature": 0.5, 10 | "max_tokens": 1024 11 | } 12 | }, 13 | "payload": { 14 | "message": { 15 | "text": [ 16 | { 17 | "role": "user", 18 | "content": "你是谁" 19 | } 20 | ] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /xfxh-web-simple-demo/src/main/resources/demo-json/response.json: -------------------------------------------------------------------------------- 1 | // 接口为流式返回,此示例为最后一次返回结果,开发者需要将接口多次返回的结果进行拼接展示 2 | 3 | { 4 | "header": { 5 | "code": 0, 6 | "message": "Success", 7 | "sid": "cht000cb087@dx18793cd421fb894542", 8 | "status": 2 9 | }, 10 | "payload": { 11 | "choices": { 12 | "status": 1, 13 | "seq": 0, 14 | "text": [ 15 | { 16 | "content": "我有什么可", 17 | "role": "assistant", 18 | "index": 0 19 | } 20 | ] 21 | }, 22 | "usage": { 23 | "text": { 24 | "question_tokens": 4, 25 | "prompt_tokens": 5, 26 | "completion_tokens": 9, 27 | "total_tokens": 14 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/README.md: -------------------------------------------------------------------------------- 1 | # 讯飞星火大模型后端接口 2 | 3 | 启动项目后,直接使用 GET 请求访问 http://localhost:8080/test/sendQuestion?question=hello&id=2 4 | 5 | 讯飞官方web文档:https://www.xfyun.cn/doc/spark/Web.html -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 2.7.0 11 | 12 | 13 | 14 | com.zhulang 15 | xfxh-web-support-context-demo 16 | 1.0-SNAPSHOT 17 | 18 | 19 | 8 20 | 8 21 | UTF-8 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-configuration-processor 28 | true 29 | 30 | 31 | cn.hutool 32 | hutool-all 33 | 5.8.18 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-web 38 | 39 | 40 | com.alibaba 41 | fastjson 42 | 1.2.67 43 | 44 | 45 | org.java-websocket 46 | Java-WebSocket 47 | 1.3.8 48 | 49 | 50 | com.squareup.okhttp3 51 | okhttp 52 | 4.10.0 53 | 54 | 55 | org.projectlombok 56 | lombok 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/java/com/zhulang/xfxh/Application.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxh; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author 狐狸半面添 8 | * @create 2023-09-15 0:43 9 | */ 10 | @SpringBootApplication 11 | public class Application { 12 | public static void main(String[] args) { 13 | SpringApplication.run(Application.class,args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/java/com/zhulang/xfxh/component/MemoryUserRecordSpace.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxh.component; 2 | 3 | import com.zhulang.xfxh.config.XfXhConfig; 4 | import com.zhulang.xfxh.dto.InteractMsg; 5 | import com.zhulang.xfxh.dto.MsgDTO; 6 | import com.zhulang.xfxh.dto.RecordsArray; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.Resource; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | 15 | /** 16 | * @author 狐狸半面添 17 | * @create 2023-09-17 1:38 18 | */ 19 | @Component 20 | public class MemoryUserRecordSpace { 21 | @Resource 22 | private XfXhConfig xfXhConfig; 23 | 24 | private final ConcurrentHashMap userRecordMap = new ConcurrentHashMap<>(); 25 | 26 | public ConcurrentHashMap getUserRecordMap() { 27 | return userRecordMap; 28 | } 29 | 30 | public List getAllInteractMsg(Long id) { 31 | RecordsArray recordsArray = userRecordMap.get(id); 32 | if (recordsArray == null) { 33 | return new ArrayList<>(1); 34 | } 35 | return recordsArray.getAllInteractMsg(); 36 | } 37 | 38 | /** 39 | * 尝试加锁 40 | * 41 | * @param id 42 | * @return true-加锁成功,false-加锁失败 43 | */ 44 | public boolean tryLock(Long id) { 45 | synchronized (id.toString().intern()) { 46 | RecordsArray recordsArray = userRecordMap.get(id); 47 | // 如果查不到用户或者没有加锁,则允许操作 48 | if (recordsArray == null) { 49 | // 用户信息保存到内存 50 | RecordsArray newRecordsArray = storeUserRecord(id); 51 | // 加锁 52 | newRecordsArray.setLock(true); 53 | return true; 54 | } else if (!recordsArray.isLock()) { 55 | // 加锁 56 | recordsArray.setLock(true); 57 | // 返回 58 | return true; 59 | } else { 60 | return false; 61 | } 62 | } 63 | } 64 | 65 | 66 | /** 67 | * 释放锁 68 | * 69 | * @param id 70 | */ 71 | public void unLock(Long id) { 72 | RecordsArray recordsArray = userRecordMap.get(id); 73 | if (recordsArray != null) { 74 | recordsArray.setLock(false); 75 | } 76 | } 77 | 78 | public void storeInteractMsg(Long id, InteractMsg interactMsg) { 79 | RecordsArray recordsArray = userRecordMap.get(id); 80 | // 为空说明不存在该用户的记录 81 | if (recordsArray == null) { 82 | storeUserRecord(id,interactMsg); 83 | return; 84 | } 85 | 86 | // 不为空的处理 87 | recordsArray.addInteractMsg(interactMsg); 88 | // 刷新状态为最新 89 | recordsArray.setStatus(0); 90 | } 91 | 92 | private RecordsArray storeUserRecord(Long id, InteractMsg interactMsg) { 93 | // 判断是否满了 94 | while (userRecordMap.size() >= xfXhConfig.getMaxUserCount()) { 95 | // 需要移除状态最差(status 最高)的用户 96 | int maxStatus = -1; 97 | Long userId = 0L; 98 | for (Map.Entry entry : userRecordMap.entrySet()) { 99 | RecordsArray recordsArray = entry.getValue(); 100 | // 已锁用户不进行处理 101 | if (!recordsArray.isLock()) { 102 | // 获取当前用户的状态 103 | int userStatus = recordsArray.getStatus(); 104 | if (maxStatus < userStatus) { 105 | maxStatus = userStatus; 106 | userId = entry.getKey(); 107 | } 108 | } 109 | } 110 | userRecordMap.remove(userId); 111 | } 112 | RecordsArray newRecordArray = new RecordsArray(xfXhConfig.getMaxInteractCount()); 113 | if (interactMsg != null) { 114 | newRecordArray.addInteractMsg(interactMsg); 115 | } 116 | userRecordMap.put(id, newRecordArray); 117 | return newRecordArray; 118 | } 119 | 120 | private RecordsArray storeUserRecord(Long id){ 121 | return storeUserRecord(id,null); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/java/com/zhulang/xfxh/component/ScheduleTask.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxh.component; 2 | 3 | import com.zhulang.xfxh.config.XfXhConfig; 4 | import com.zhulang.xfxh.dto.RecordsArray; 5 | import org.springframework.scheduling.annotation.Async; 6 | import org.springframework.scheduling.annotation.EnableAsync; 7 | import org.springframework.scheduling.annotation.EnableScheduling; 8 | import org.springframework.scheduling.annotation.Scheduled; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.annotation.Resource; 12 | import java.util.Map; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | 15 | 16 | /** 17 | * @author 狐狸半面添 18 | * @create 2023-09-18 1:07 19 | */ 20 | @EnableScheduling // 1.开启定时任务 21 | @EnableAsync // 2.开启多线程 22 | @Component 23 | public class ScheduleTask { 24 | @Resource 25 | private MemoryUserRecordSpace memoryUserRecordSpace; 26 | 27 | @Resource 28 | private XfXhConfig xfXhConfig; 29 | 30 | @Scheduled(initialDelayString = "${xfxh.scheduled.updateUserStatusFixedDelay}" 31 | ,fixedDelayString = "${xfxh.scheduled.updateUserStatusFixedDelay}") 32 | @Async 33 | public void updateUserStatusTask(){ 34 | ConcurrentHashMap userRecordMap = memoryUserRecordSpace.getUserRecordMap(); 35 | for (Map.Entry userRecord : userRecordMap.entrySet()) { 36 | RecordsArray recordsArray = userRecord.getValue(); 37 | // 如果已经加锁,说明正在交互消息中,直接跳过 38 | if (recordsArray.isLock()){ 39 | continue; 40 | } 41 | // 将状态+1,当超过 xfXhConfig.userRecordMaxStatus 则从空间中移除该用户交互信息 42 | recordsArray.setStatus(recordsArray.getStatus()+1); 43 | if (recordsArray.getStatus()>xfXhConfig.getUserRecordMaxStatus()){ 44 | userRecordMap.remove(userRecord.getKey()); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/java/com/zhulang/xfxh/component/XfXhStreamClient.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxh.component; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.zhulang.xfxh.config.XfXhConfig; 5 | import com.zhulang.xfxh.dto.MsgDTO; 6 | import com.zhulang.xfxh.dto.RequestDTO; 7 | import lombok.extern.slf4j.Slf4j; 8 | import okhttp3.*; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.annotation.Resource; 13 | import javax.crypto.Mac; 14 | import javax.crypto.spec.SecretKeySpec; 15 | import java.net.URL; 16 | import java.nio.charset.StandardCharsets; 17 | import java.text.SimpleDateFormat; 18 | import java.util.*; 19 | 20 | /** 21 | * @author 狐狸半面添 22 | * @create 2023-09-15 1:10 23 | */ 24 | @Component 25 | @Slf4j 26 | public class XfXhStreamClient { 27 | @Resource 28 | private XfXhConfig xfXhConfig; 29 | 30 | @Value("${xfxh.QPS}") 31 | private int connectionTokenCount; 32 | 33 | /** 34 | * 获取令牌 35 | */ 36 | public static int GET_TOKEN_STATUS = 0; 37 | /** 38 | * 归还令牌 39 | */ 40 | public static int BACK_TOKEN_STATUS = 1; 41 | 42 | /** 43 | * 操作令牌 44 | * 45 | * @param status 0-获取令牌 1-归还令牌 46 | * @return 是否操作成功 47 | */ 48 | public synchronized boolean operateToken(int status) { 49 | if (status == GET_TOKEN_STATUS) { 50 | // 获取令牌 51 | if (connectionTokenCount != 0) { 52 | // 说明还有令牌,将令牌数减一 53 | connectionTokenCount-=1; 54 | return true; 55 | }else{ 56 | return false; 57 | } 58 | } else { 59 | // 放回令牌 60 | connectionTokenCount += 1; 61 | return true; 62 | } 63 | } 64 | 65 | /** 66 | * 发送消息 67 | * 68 | * @param uid 每个用户的id,用于区分不同用户 69 | * @param msgList 发送给大模型的消息,可以包含上下文内容 70 | * @return 获取websocket连接,以便于我们在获取完整大模型回复后手动关闭连接 71 | */ 72 | public WebSocket sendMsg(String uid, List msgList, WebSocketListener listener) { 73 | // 获取鉴权url 74 | String authUrl = this.getAuthUrl(); 75 | // 鉴权方法生成失败,直接返回 null 76 | if (authUrl == null) { 77 | return null; 78 | } 79 | OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); 80 | // 将 https/http 连接替换为 ws/wss 连接 81 | String url = authUrl.replace("http://", "ws://").replace("https://", "wss://"); 82 | Request request = new Request.Builder().url(url).build(); 83 | // 建立 wss 连接 84 | WebSocket webSocket = okHttpClient.newWebSocket(request, listener); 85 | // 组装请求参数 86 | RequestDTO requestDTO = getRequestParam(uid, msgList); 87 | // 发送请求 88 | webSocket.send(JSONObject.toJSONString(requestDTO)); 89 | return webSocket; 90 | } 91 | 92 | /** 93 | * 生成鉴权方法,具体实现不用关心,这是讯飞官方定义的鉴权方式 94 | * 95 | * @return 鉴权访问大模型的路径 96 | */ 97 | public String getAuthUrl() { 98 | try { 99 | URL url = new URL(xfXhConfig.getHostUrl()); 100 | // 时间 101 | SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 102 | format.setTimeZone(TimeZone.getTimeZone("GMT")); 103 | String date = format.format(new Date()); 104 | // 拼接 105 | String preStr = "host: " + url.getHost() + "\n" + 106 | "date: " + date + "\n" + 107 | "GET " + url.getPath() + " HTTP/1.1"; 108 | // SHA256加密 109 | Mac mac = Mac.getInstance("hmacsha256"); 110 | SecretKeySpec spec = new SecretKeySpec(xfXhConfig.getApiSecret().getBytes(StandardCharsets.UTF_8), "hmacsha256"); 111 | mac.init(spec); 112 | 113 | byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8)); 114 | // Base64加密 115 | String sha = Base64.getEncoder().encodeToString(hexDigits); 116 | // 拼接 117 | String authorizationOrigin = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", xfXhConfig.getApiKey(), "hmac-sha256", "host date request-line", sha); 118 | // 拼接地址 119 | HttpUrl httpUrl = Objects.requireNonNull(HttpUrl.parse("https://" + url.getHost() + url.getPath())).newBuilder(). 120 | addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorizationOrigin.getBytes(StandardCharsets.UTF_8))). 121 | addQueryParameter("date", date). 122 | addQueryParameter("host", url.getHost()). 123 | build(); 124 | 125 | return httpUrl.toString(); 126 | } catch (Exception e) { 127 | log.error("鉴权方法中发生错误:" + e.getMessage()); 128 | return null; 129 | } 130 | } 131 | 132 | /** 133 | * 获取请求参数 134 | * 135 | * @param uid 每个用户的id,用于区分不同用户 136 | * @param msgList 发送给大模型的消息,可以包含上下文内容 137 | * @return 请求DTO,该 DTO 转 json 字符串后生成的格式参考 resources/demo-json/request.json 138 | */ 139 | public RequestDTO getRequestParam(String uid, List msgList) { 140 | RequestDTO requestDTO = new RequestDTO(); 141 | requestDTO.setHeader(new RequestDTO.HeaderDTO(xfXhConfig.getAppId(), uid)); 142 | requestDTO.setParameter(new RequestDTO.ParameterDTO(new RequestDTO.ParameterDTO.ChatDTO(xfXhConfig.getDomain(), xfXhConfig.getTemperature(), xfXhConfig.getMaxTokens()))); 143 | requestDTO.setPayload(new RequestDTO.PayloadDTO(new RequestDTO.PayloadDTO.MessageDTO(msgList))); 144 | return requestDTO; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/java/com/zhulang/xfxh/config/XfXhConfig.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxh.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * @author 狐狸半面添 9 | * @create 2023-09-15 0:46 10 | */ 11 | @Configuration 12 | @ConfigurationProperties(prefix = "xfxh") 13 | @Data 14 | public class XfXhConfig { 15 | private String hostUrl; 16 | private String domain; 17 | private Float temperature; 18 | private Integer maxTokens; 19 | private String appId; 20 | private String apiKey; 21 | private String apiSecret; 22 | private Integer maxInteractCount; 23 | private Integer maxUserCount; 24 | private Integer userRecordMaxStatus; 25 | private Integer maxResponseTime; 26 | } 27 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/java/com/zhulang/xfxh/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxh.controller; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.zhulang.xfxh.config.XfXhConfig; 6 | import com.zhulang.xfxh.dto.InteractMsg; 7 | import com.zhulang.xfxh.dto.MsgDTO; 8 | import com.zhulang.xfxh.dto.RecordsArray; 9 | import com.zhulang.xfxh.listener.XfXhWebSocketListener; 10 | import com.zhulang.xfxh.component.MemoryUserRecordSpace; 11 | import com.zhulang.xfxh.component.XfXhStreamClient; 12 | import okhttp3.WebSocket; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RequestParam; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import javax.annotation.Resource; 19 | import java.util.*; 20 | import java.util.concurrent.ConcurrentHashMap; 21 | 22 | /** 23 | * @author 狐狸半面添 24 | * @create 2023-09-15 0:42 25 | */ 26 | @RestController 27 | @RequestMapping("/test") 28 | public class TestController { 29 | @Resource 30 | private XfXhConfig xfXhConfig; 31 | 32 | @Resource 33 | private XfXhStreamClient xfXhStreamClient; 34 | 35 | @Resource 36 | private MemoryUserRecordSpace memoryUserRecordSpace; 37 | 38 | // 使用 id 作为唯一用户的标识(区分不同用户) 39 | @GetMapping("/sendQuestion") 40 | public String question(@RequestParam("id") Long id, @RequestParam("question") String question) throws InterruptedException { 41 | if (StrUtil.isBlank(question)) { 42 | return "无效问题,请重新输入"; 43 | } 44 | 45 | // 尝试锁定用户 46 | if (!memoryUserRecordSpace.tryLock(id)) { 47 | return "正在处理上次问题,请稍后再试"; 48 | } 49 | 50 | // 获取连接令牌 51 | if(!xfXhStreamClient.operateToken(XfXhStreamClient.GET_TOKEN_STATUS)){ 52 | // 释放锁 53 | memoryUserRecordSpace.unLock(id); 54 | return "当前大模型连接数过多,请稍后再试"; 55 | } 56 | 57 | MsgDTO msgDTO = MsgDTO.createUserMsg(question); 58 | XfXhWebSocketListener listener = new XfXhWebSocketListener(); 59 | // 组装上下文内容发送 60 | List msgList = memoryUserRecordSpace.getAllInteractMsg(id); 61 | 62 | msgList.add(msgDTO); 63 | WebSocket webSocket = xfXhStreamClient.sendMsg(UUID.randomUUID().toString().substring(0, 10), msgList, listener); 64 | if (webSocket == null) { 65 | // 归还令牌 66 | xfXhStreamClient.operateToken(XfXhStreamClient.BACK_TOKEN_STATUS); 67 | // 释放锁 68 | memoryUserRecordSpace.unLock(id); 69 | return "系统内部错误,请联系管理员"; 70 | } 71 | try { 72 | int count = 0; 73 | // 为了避免死循环,设置循环次数来定义超时时长 74 | int maxCount = xfXhConfig.getMaxResponseTime() * 5; 75 | while (count <= maxCount) { 76 | Thread.sleep(200); 77 | if (listener.isWsCloseFlag()) { 78 | break; 79 | } 80 | count++; 81 | } 82 | if (count > maxCount) { 83 | return "大模型响应超时,请联系管理员"; 84 | } 85 | // 将记录添加到 memoryUserRecordSpace 86 | String answer = listener.getAnswer().toString(); 87 | memoryUserRecordSpace.storeInteractMsg(id, new InteractMsg(MsgDTO.createUserMsg(question), MsgDTO.createAssistantMsg(answer))); 88 | return answer; 89 | } finally { 90 | // 关闭连接 91 | webSocket.close(1000, ""); 92 | // 释放锁 93 | memoryUserRecordSpace.unLock(id); 94 | // 归还令牌 95 | xfXhStreamClient.operateToken(XfXhStreamClient.BACK_TOKEN_STATUS); 96 | } 97 | } 98 | 99 | // 测试使用,查看内存空间中所有的用户记录信息 100 | @GetMapping("/spaceInfo") 101 | public List spaceInfo(){ 102 | ConcurrentHashMap userRecordMap = memoryUserRecordSpace.getUserRecordMap(); 103 | ArrayList infoList = new ArrayList<>(userRecordMap.size()); 104 | for (Map.Entry entry : userRecordMap.entrySet()) { 105 | RecordsArray recordsArray = entry.getValue(); 106 | JSONObject data = new JSONObject(); 107 | data.put("id",entry.getKey()); 108 | data.put("allInteractMsg",recordsArray.getAllInteractMsg()); 109 | data.put("status",recordsArray.getStatus()); 110 | data.put("lock",recordsArray.isLock()); 111 | infoList.add(data); 112 | } 113 | 114 | return infoList; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/java/com/zhulang/xfxh/dto/InteractMsg.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxh.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * @author 狐狸半面添 9 | * @create 2023-09-17 0:53 10 | */ 11 | @Data 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class InteractMsg { 15 | private MsgDTO userMsg; 16 | private MsgDTO assistantMsg; 17 | } 18 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/java/com/zhulang/xfxh/dto/MsgDTO.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxh.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.*; 5 | 6 | /** 7 | * 消息对象 8 | * 9 | * @author 狐狸半面添 10 | * @create 2023-09-15 0:42 11 | */ 12 | @Data 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | @JsonInclude(JsonInclude.Include.NON_NULL) 16 | public class MsgDTO { 17 | /** 18 | * 角色 19 | */ 20 | private String role; 21 | /** 22 | * 消息内容 23 | */ 24 | private String content; 25 | /** 26 | * 响应结果字段:结果序号,取值为[0,10]; 当前为保留字段,开发者可忽略 27 | */ 28 | private Integer index; 29 | 30 | public static final String ROLE_USER = "user"; 31 | public static final String ROLE_ASSISTANT = "assistant"; 32 | 33 | public static MsgDTO createUserMsg(String content){ 34 | return new MsgDTO(ROLE_USER,content,null); 35 | } 36 | 37 | public static MsgDTO createAssistantMsg(String content){ 38 | return new MsgDTO(ROLE_ASSISTANT,content,null); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/java/com/zhulang/xfxh/dto/RecordsArray.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxh.dto; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * @author 狐狸半面添 8 | * @create 2023-09-17 0:49 9 | */ 10 | public class RecordsArray { 11 | 12 | /** 13 | * 操作锁,默认 false 即未锁 14 | * true-已锁 15 | * false-未锁 16 | */ 17 | private boolean lock; 18 | 19 | /** 20 | * 记录状态,默认为 0 21 | * 0: 最新记录 22 | * 1: 最近一次交互在 10 分钟前 23 | * 2: 最近一次交互在 20 分钟前 24 | * ... 25 | * 设置最大在 60 分钟后消息被销毁 26 | */ 27 | private int status; 28 | 29 | /** 30 | * 表示数组的最大容量,始终留一个空间作为 rear 标记点,实际的允许的交互记录大小为 arraySize-1 31 | */ 32 | private final int arrayMaxSize; 33 | 34 | /** 35 | * front 初始值为 0,指向队列的第一个元素, 也就是说 interactMsgArr[front] 就是队列的第一个元素 36 | */ 37 | private int front; 38 | 39 | /** 40 | * 队列尾,rear的初始值 = 0,指向队列的最后一个元素的后一个位置,因为希望空出一个空间做为约定 41 | */ 42 | private int rear; 43 | 44 | /** 45 | * 存放交互记录,模拟队列 46 | */ 47 | private final InteractMsg[] interactMsgArr; 48 | 49 | public RecordsArray(int maxInteractCount) { 50 | // +1 留出一个空间作为 rear 标记点 51 | this.arrayMaxSize = maxInteractCount + 1; 52 | this.interactMsgArr = new InteractMsg[arrayMaxSize]; 53 | } 54 | 55 | /** 56 | * 判断队列是否满 57 | * 58 | * @return true-队满,false-未满 59 | */ 60 | public boolean isFull() { 61 | return (rear + 1) % arrayMaxSize == front; 62 | } 63 | 64 | 65 | /** 66 | * 添加交互消息 67 | */ 68 | public void addInteractMsg(InteractMsg interactMsg) { 69 | // 判断队列是否满 70 | if (isFull()) { 71 | // 队满,front 向后移动相当于将最旧的数据出队列 72 | front = (front + 1) % arrayMaxSize; 73 | } 74 | // 队列未满,无需将最旧的数据出队列,即 front 无需移动 75 | 76 | // 无论是否队满,新的交互信息还是要添加的,因此 rear 处设置为最新的交互消息,并向后移动 77 | interactMsgArr[rear] = interactMsg; 78 | rear = (rear + 1) % arrayMaxSize; 79 | } 80 | 81 | /** 82 | * 获取存储的交互消息 83 | */ 84 | public List getAllInteractMsg() { 85 | int realSize = size(); 86 | ArrayList msgList = new ArrayList<>(realSize * 2); 87 | for (int i = front; i < front + realSize; i++) { 88 | InteractMsg interactMsg = interactMsgArr[i % arrayMaxSize]; 89 | if (interactMsg.getUserMsg() != null) { 90 | msgList.add(interactMsg.getUserMsg()); 91 | } 92 | if (interactMsg.getAssistantMsg() != null) { 93 | msgList.add(interactMsg.getAssistantMsg()); 94 | } 95 | } 96 | return msgList; 97 | } 98 | 99 | /** 100 | * 求出当前队列有效数据的个数 101 | * 102 | * @return 有效个数 103 | */ 104 | public int size() { 105 | //当rear>=front时,有效个数为rear-front 106 | //当rear text; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/java/com/zhulang/xfxh/dto/ResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxh.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * 返回参数 11 | * 12 | * @author 狐狸半面添 13 | * @create 2023-09-15 0:42 14 | */ 15 | @NoArgsConstructor 16 | @Data 17 | public class ResponseDTO { 18 | 19 | @JsonProperty("header") 20 | private HeaderDTO header; 21 | @JsonProperty("payload") 22 | private PayloadDTO payload; 23 | 24 | @NoArgsConstructor 25 | @Data 26 | public static class HeaderDTO { 27 | /** 28 | * 错误码,0表示正常,非0表示出错 29 | */ 30 | @JsonProperty("code") 31 | private Integer code; 32 | /** 33 | * 会话是否成功的描述信息 34 | */ 35 | @JsonProperty("message") 36 | private String message; 37 | /** 38 | * 会话的唯一id,用于讯飞技术人员查询服务端会话日志使用,出现调用错误时建议留存该字段 39 | */ 40 | @JsonProperty("sid") 41 | private String sid; 42 | /** 43 | * 会话状态,取值为[0,1,2];0代表首次结果;1代表中间结果;2代表最后一个结果 44 | */ 45 | @JsonProperty("status") 46 | private Integer status; 47 | } 48 | 49 | @NoArgsConstructor 50 | @Data 51 | public static class PayloadDTO { 52 | @JsonProperty("choices") 53 | private ChoicesDTO choices; 54 | /** 55 | * 在最后一次结果返回 56 | */ 57 | @JsonProperty("usage") 58 | private UsageDTO usage; 59 | 60 | @NoArgsConstructor 61 | @Data 62 | public static class ChoicesDTO { 63 | /** 64 | * 文本响应状态,取值为[0,1,2]; 0代表首个文本结果;1代表中间文本结果;2代表最后一个文本结果 65 | */ 66 | @JsonProperty("status") 67 | private Integer status; 68 | /** 69 | * 返回的数据序号,取值为[0,9999999] 70 | */ 71 | @JsonProperty("seq") 72 | private Integer seq; 73 | /** 74 | * 响应文本 75 | */ 76 | @JsonProperty("text") 77 | private List text; 78 | 79 | } 80 | 81 | @NoArgsConstructor 82 | @Data 83 | public static class UsageDTO { 84 | @JsonProperty("text") 85 | private TextDTO text; 86 | 87 | @NoArgsConstructor 88 | @Data 89 | public static class TextDTO { 90 | /** 91 | * 保留字段,可忽略 92 | */ 93 | @JsonProperty("question_tokens") 94 | private Integer questionTokens; 95 | /** 96 | * 包含历史问题的总tokens大小 97 | */ 98 | @JsonProperty("prompt_tokens") 99 | private Integer promptTokens; 100 | /** 101 | * 回答的tokens大小 102 | */ 103 | @JsonProperty("completion_tokens") 104 | private Integer completionTokens; 105 | /** 106 | * prompt_tokens和completion_tokens的和,也是本次交互计费的tokens大小 107 | */ 108 | @JsonProperty("total_tokens") 109 | private Integer totalTokens; 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/java/com/zhulang/xfxh/listener/XfXhWebSocketListener.java: -------------------------------------------------------------------------------- 1 | package com.zhulang.xfxh.listener; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.zhulang.xfxh.dto.MsgDTO; 5 | import com.zhulang.xfxh.dto.ResponseDTO; 6 | import lombok.Data; 7 | import lombok.extern.slf4j.Slf4j; 8 | import okhttp3.Response; 9 | import okhttp3.WebSocket; 10 | import okhttp3.WebSocketListener; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.springframework.lang.Nullable; 13 | 14 | /** 15 | * @author 狐狸半面添 16 | * @create 2023-09-15 1:11 17 | */ 18 | @Data 19 | @Slf4j 20 | public class XfXhWebSocketListener extends WebSocketListener { 21 | private StringBuilder answer = new StringBuilder(); 22 | 23 | private boolean wsCloseFlag = false; 24 | 25 | @Override 26 | public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { 27 | super.onOpen(webSocket, response); 28 | } 29 | 30 | @Override 31 | public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { 32 | super.onMessage(webSocket, text); 33 | ResponseDTO responseData = JSONObject.parseObject(text, ResponseDTO.class); 34 | if (responseData.getHeader().getCode() != 0) { 35 | // 日志记录 36 | log.error("发生错误,错误码为:" + responseData.getHeader().getCode() + "; " + "信息:" + responseData.getHeader().getMessage()); 37 | // 记录信息 38 | this.answer = new StringBuilder("大模型响应错误,请稍后再试"); 39 | wsCloseFlag = true; 40 | return; 41 | } 42 | // 将回答进行拼接 43 | for (MsgDTO msgDTO : responseData.getPayload().getChoices().getText()) { 44 | this.answer.append(msgDTO.getContent()); 45 | } 46 | // 对最后一个文本结果进行处理 47 | if (2 == responseData.getHeader().getStatus()) { 48 | wsCloseFlag = true; 49 | } 50 | } 51 | 52 | @Override 53 | public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) { 54 | super.onFailure(webSocket, t, response); 55 | } 56 | 57 | @Override 58 | public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { 59 | super.onClosed(webSocket, code, reason); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "xfxh.hostUrl", 5 | "type": "java.lang.String", 6 | "description": "Description for xfxh.hostUrl." 7 | } 8 | ] } -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | xfxh: 2 | # 服务引擎使用 讯飞星火认知大模型V2.0,如果使用 V1.5 需要将 hostUrl 修改为 https://spark-api.xf-yun.com/v1.1/chat 3 | hostUrl: https://spark-api.xf-yun.com/v2.1/chat 4 | # 发送请求时指定的访问领域,如果是 V1.5版本 设置为 general,如果是 V2版本 设置为 generalv2 5 | domain: generalv2 6 | # 核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高。取值 [0,1] 7 | temperature: 0.5 8 | # 模型回答的tokens的最大长度,V1.5取值为[1,4096],V2.0取值为[1,8192]。 9 | maxTokens: 2048 10 | # 用于权限验证,从服务接口认证信息中获取 11 | appId: 12 | # 用于权限验证,从服务接口认证信息中获取 13 | apiKey: 14 | # 用于权限验证,从服务接口认证信息中获取 15 | apiSecret: 16 | # 每个 id 用户能保存的最大交互记录数,需要注意所有累计tokens需控制8192以内 17 | maxInteractCount: 3 18 | # 内存中能够保存的最大用户数量 19 | maxUserCount: 15 20 | # 用户记录最大状态为 6,状态超过 userRecordMaxStatus 会从记录空间中移除该用户 21 | userRecordMaxStatus: 6 22 | # 大模型回复问题的最大响应时长,单位 s 23 | maxResponseTime: 40 24 | # 允许同时连接大模型的 websocket 数,如果是普通(免费)用户为 2,超过这个数连接响应会报错,具体参考官网。 25 | QPS: 2 26 | scheduled: 27 | # 单位 ms,600000 表示每隔十分钟用户记录状态+1,直到状态超过 userRecordMaxStatus 会从记录空间中移除该用户 28 | updateUserStatusFixedDelay: 600000 -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/resources/demo-json/request.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "app_id": "12345", 4 | "uid": "12345" 5 | }, 6 | "parameter": { 7 | "chat": { 8 | "domain": "general", 9 | "temperature": 0.5, 10 | "max_tokens": 1024 11 | } 12 | }, 13 | "payload": { 14 | "message": { 15 | "text": [ 16 | { 17 | "role": "user", 18 | "content": "你是谁" 19 | } 20 | ] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /xfxh-web-support-context-demo/src/main/resources/demo-json/response.json: -------------------------------------------------------------------------------- 1 | // 接口为流式返回,此示例为最后一次返回结果,开发者需要将接口多次返回的结果进行拼接展示 2 | 3 | { 4 | "header": { 5 | "code": 0, 6 | "message": "Success", 7 | "sid": "cht000cb087@dx18793cd421fb894542", 8 | "status": 2 9 | }, 10 | "payload": { 11 | "choices": { 12 | "status": 1, 13 | "seq": 0, 14 | "text": [ 15 | { 16 | "content": "我有什么可", 17 | "role": "assistant", 18 | "index": 0 19 | } 20 | ] 21 | }, 22 | "usage": { 23 | "text": { 24 | "question_tokens": 4, 25 | "prompt_tokens": 5, 26 | "completion_tokens": 9, 27 | "total_tokens": 14 28 | } 29 | } 30 | } 31 | } --------------------------------------------------------------------------------