diff --git a/86C111T1K131FJQU.dat b/86C111T1K131FJQU.dat new file mode 100644 index 0000000..09ca61a --- /dev/null +++ b/86C111T1K131FJQU.dat @@ -0,0 +1 @@ +EWDRGAHMADDPERCSGJDUHABIENHHJEEAFRFSATFTEFDSGZEQCYEOCOIRABDOEMEQAYAAABHEBBDWCMGEARHUJOGHBAJUFJJNFDGFAHDBJMGUBCIZBIEHIKDWBRAZIAINIGBFFFEKGQBUJGELGZIYBRGTHAEHHZCUEQJBAOGJAZBLACAFDFJKALCECDGKCQIYDJDODSDNDTIDHQJSDFFADLJLBNFAAAIODQEEILGGGVGEEKFSJRACFZDZIQEOILBLDXCRCPAOEBEDIPDDBNHCDJCKEGEJBHHBFKDNAOFOCCIQIBGVBSGPAJCDGTEMHGBCHYGLJOCPDYGWIFGIDXBUIHEKETDIFEGQHVGGFJAKAVHNJLEKBXJAFXDNHBGQAZDRHZGOGXHAALCXEZCNGLGHGOIDDKGCIYDSFJFNGIAQGMAHCXAFAVDVBPFMHSJSCODLHACXBHGUJEJLGLEREPBOEFCJDQDIDOIEFIHLCICOBWBTCAHOJTENCYDSBDGLFQBPIWCBGJDDIRCCDNEJITEJFHIWJEGLBGDMJRDGCFIFJCENCGCXHUCJGZGXAWAJCYDFIKJSIPDHFPBHGLCLGVFOEUAUIKFRISFQEXEPGEDVIFFGDOBNBFIMAUJOHJJNJAEQAMHBJVDTIUDDHBAQILFXHOGEFXIPBMDUCKCTCWGRFZIYFOENDJHPCTIYIXDXAEDGHNHTJQCWCQHAEVBDGLAZEQDPIQGIEVHNDUCUHMBMIVJKDNDJDJFGBAADHDIWJEGJJFDAEKALBMDOHTAQFFIDIBJVHQDNBXIABPIJCLAIBHHNIOGKDUCUCSAXDUCMDMGYAMBUEUBOAEATCMGBBJAOJKFRCYEXCJIZDKFAFCAJAGDYJGGPDKCJAPDWFIGAITHCEOFTIJEQGUGQHKAAHGFFEWGHDDEMDCHSAYJNCGIAJUHGDLAKFGHCGLEHHEAPENDVBAIOFEGOBKIKERHNFSIRCCAHHUBMGXDYATBYADEZCLCBDEDUHCJRESHMJCJVFTGLJJDYDYAFIMBCJKAOHEAHBMHMASIHCMGEGREQEUBSHFEECBJOJUBKDXGWJVADHLECJHHAADBKGCFQGIAYEHAJBFCVBTILJBIKBPAYFFFPCQEWJB \ No newline at end of file diff --git a/ArcFacePro64.dat b/ArcFacePro64.dat new file mode 100644 index 0000000..33dd326 --- /dev/null +++ b/ArcFacePro64.dat @@ -0,0 +1 @@ +EWEPEPEOGMGTELIZJUGECKIUJDBCJTCNISGPBNHLJTJUBHEWGNAKGEGAIOHJDQAJGNCFDRFZJEDMJTGFGBEAAWGLBZAUCNHCCPBZCIIKBJATGYARHRAZHXFRBIEBCHIXAFDLFQBZFVJQGTCEHDIMIVGQEJAJIYHXHTISIVETBACMCCGFDPEKDQHYGGAPFXIOCJAAGOHYIFHNHZHWIGCZIGHHDMDXHQGTFLJOHGAYIVBGIHIQHHHJDJFWHMCRHJAMHZESGWGGAUJRGDHAGHIQITIJIUAXAEIZGBGZCCHJAHASGNCVIIJAIYFOEQGFELEIEDECJTCXBCAPIKHTFTHBAGJTERHQGSAFEUDIHLDDITDPIMACAOGTIIHFEMHLHIHYJTFDFDGWAKHJEIEIJFGABQJCIHIADYCXAHIMJTHOASFLGFFIBJJEHDHLHOCMDIGGDOJDHNBQCJENFUBAGOAZITIRJSFBBUCUHABLHRFVIUBWCADJCMDXDPATBPJSJRJLBRABGVFTDNFOAQDOARDEBRHJAQGYHIGMBDCJCSJKFBBLGECAEFEYCVCHAEAZJRIOFEHLCJILEHJVGYIVCWGHCMGJGLBTFMHFCAEAAUJQJLAEARDHDFHDBJJGALEHFNGSAIHOJUBOEAJDDFFYFTINITHTBNIJFDHLEAFGBFHFFQGHGFGREVFHFDCZGYEVBWAZDSCAGLDMIAAEFOAXIXFECSFQDWHFFHCFASFSGAHVJSDBBZJQAZBXARILBAJFJEHCANAIABBMECBJJFIOGYGHBXCUCVBDJOCYBZDZAJEXAXEPFRFOGVHQAOJLCYBOHFEKJFIJBDHDDCEAAUJVDRIGGGGCJOFVECAHAQFSBSGYJVGKCQDDHPGUCIARFAIEJGGDITAUDIIVBBJUEFCIDTGJGYJODRDEJPBMFNCXAKCPAIGOJQGHBZHQJUCOBKCKDPJSGGCVCAIWFVHIIEAJJMEFGRHZDEDACFBQJODGDVJBAUBXGKGCFZERAHIOALGKGAILDNHQGGAZDEIGATBTCWHMDKGSIWFMHAIZHREBJBEFENDFBRBLGLCMERJAEOBXCNDBHVCSJBDMEHCLJLCFFOGVGWATBOJBFJEQETHGESEXFDIIFDAGJPDNHEDSFNBRIVFMFPGOEEIHEFCOCKJGJAIZJIFTIGAWITGWDXGBEFDTJHFXBF \ No newline at end of file diff --git a/docs/rtc.md b/docs/rtc.md new file mode 100644 index 0000000..5cb3ede --- /dev/null +++ b/docs/rtc.md @@ -0,0 +1,83 @@ +很久以前,人类以为只有神仙可以听到、看到千里之外的声音和景象,称之为千里眼和顺风耳,短短几百年里,人类的技术革命实现了质的飞跃。 + +1876 年,贝尔电话的发明,使人类可以听到千里之外声音的梦想终于成真。 + +此后,音视频技术不断发展。一方面,视频压缩技术从 H.261 到 H.264,再到现在的 H.265 及AV1,视频压缩率越来越高;音频压缩技术也从电话使用的 G.711、G.722 等窄带音频压缩技术,发展到现代的 AAC、OPUS 等宽带音频压缩技术。 + +另一方面,从中国 3G 网络正式商用开始,移动网络也发生了翻天覆地的变化。从 3G 到 4G ,再到马上要落地的 5G,移动网络的带宽和质量越来越高,为音视频数据传输打下了坚实的基础。 + +尤其是 2011 年 Google 推出 WebRTC 技术后,大大降低了音视频技术的门槛,可以在浏览器上快速开发出各种音视频应用。 + +如今,在疫情的三年里,视频会议,远程会诊,线上教学等需求将RTC技术推向高潮,成为影响社会发展不可或缺的技术之一。 + +2023年,从用3W法学习解构RTC开始,笔者也开启了RTC分享之路。 + +1.什么是RTC? +RTC是Real-Time Communication的缩写,译为实时通信,目的是在设备端实时的转发音视频多媒体数据,让用户能实时的进行音频和视频的会话,即基于 IP 技术实现的实时交互的音视频通信技术。 + +具体涵义如下: + +▪ 实时:音视频数据传输的延迟要达到“实时”的标准,也就是说延时要小于400ms,能够实现低延时和无卡顿,在正常通信过程中基本感受不到延迟的存在。 + +▪ 音视频:音视频数据传输,实时音视频通信通过服务端为中转节点,即时采集、渲染、处理、传输终端用户的图像、视频、音频数据进行,实现音视频流数据在终端节点间完成通信的过程。 + +▪ 实时音视频服务商一般以SDK的形式提供一整套解决方案。 + +2.为什么选择RTC技术? +痛点: + +基础音视频流程复杂且广泛:涵盖音视频收集、音视频压缩/解码、数据传输、终端适配、视频分发等系列环节,每个环节展开,都是复杂技术点。企业若想打造自主实时音视频方案,不仅要养一定规模软硬研发团队,还要花费一定时间沉淀,对于该企业来说,成本太高。 + +RTC技术优势: + +高音质 +基于专有回声消除&降噪技术,可在嘈杂的环境下实现高音质通话,让对话里语音听得比较清晰,没有回声、啸叫的状况出现。 + +高画质 +视频支持超高清晰度画面,一路视频提供多种分辨率,大屏幕可订阅更高分辨率提升视频通话体验,分分钟感受面对面交流感。 + +低延迟 +全球通信节点支持,支持实时性更好的UDP协议,端到端延时低 + +抗弱网 +自动增益控制&弱网丢包补偿技术 ,在丢包下保持音视频通话流畅。 + +当然,基于一些特定行业的应用场景,比如多人数直播时的高并发,医疗行业的网闸透传,成熟RTC服务商都有着良好的技术沉淀,让越来越多的企业使用这项技术。 + +3.RTC有哪些使用场景? +随着移动互联网的普及和智能终端设备的广泛应用,实时音视频正逐渐成为主流互动方式,已在在线教育、社交娱乐、互动电商等热门领域得到广泛应用,也赋能于更多创新场景,如金融、政企服务、loT、医疗等,帮助人们享受更便捷和更人性化的生活服务。 + +协同办公-视频会议 +丰富的会议场景,轻松实现远程办公系统,打通团队沟通渠道,帮助企业充分挖掘和整合隐形资源。 + +典型应用:Zoom,腾讯会议 + +社交沟通-聊天室 +支持 1v1 通话或群聊功能,频道内用户可自由发言,适用于语音通话、语音群聊、语音聊天室等场景。 + +典型应用:微信语音通话,YY语音 + +游戏&娱乐 +玩家可通过语音/视频聊天推进游戏进程,团战作战、协同作战,及时分享游戏信息,一起连麦开黑,拉近玩家距离。 + +典型应用:网易游戏,虎牙直播 + +电商直播 +通过IM+音视频拓展多样化电商直播玩法,增强购物体验,提升获客率,让购物更有趣,促进电商平台交易转化,实现全球购物零距离。 + +典型应用:淘宝直播 + +在线教育 +视频面对面教学,真实还原线下教学场景,支持1V1教学、1对多教学、双师课堂等多种互动教学模式。 + +典型应用:小鹅通 + +远程医疗 +基于IM及实时音视频RTC,通过实施互动技术,实现优质医疗资源和知识共享,满足远程会诊、手术示教多种场景需求。 + +典型应用:微医 + +视频双录 +根据金融监管要求,为客户提供多场景的双录服务,提供柜面双录、远程双录、移动双录、AI自助双录,帮助金融机构实现业务回溯。 + +典型应用:招商银行app \ No newline at end of file diff --git a/pom.xml b/pom.xml index d3b1568..7841d8d 100644 --- a/pom.xml +++ b/pom.xml @@ -244,16 +244,33 @@ </dependency> <!-- Elasticsearch --> +<!-- <dependency>--> +<!-- <groupId>org.elasticsearch.client</groupId>--> +<!-- <artifactId>elasticsearch-rest-high-level-client</artifactId>--> +<!-- <version>7.17.9</version>--> +<!-- </dependency>--> +<!-- <dependency>--> +<!-- <groupId>org.elasticsearch</groupId>--> +<!-- <artifactId>elasticsearch</artifactId>--> +<!-- <version>7.17.9</version>--> +<!-- </dependency>--> + <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> - <version>7.17.9</version> + <version>7.14.0</version> </dependency> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> - <version>7.17.9</version> + <version>7.14.0</version> </dependency> + <dependency> + <groupId>org.elasticsearch.client</groupId> + <artifactId>elasticsearch-rest-client</artifactId> + <version>7.14.0</version> + </dependency> + </dependencies> diff --git a/src/main/java/com/guwan/backend/client/Go2RTCClient.java b/src/main/java/com/guwan/backend/client/Go2RTCClient.java new file mode 100644 index 0000000..eaf09ed --- /dev/null +++ b/src/main/java/com/guwan/backend/client/Go2RTCClient.java @@ -0,0 +1,84 @@ +package com.guwan.backend.client; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Component +@RequiredArgsConstructor +public class Go2RTCClient { + + private final RestTemplate restTemplate; + + @Value("${go2rtc.api.url}") + private String apiUrl; + + /** + * 创建流 + */ + public void createStream(String streamId, String sourceUrl) { + String url = apiUrl + "/api/streams/" + streamId; + StreamConfig config = new StreamConfig(sourceUrl); + + try { + restTemplate.postForEntity(url, config, String.class); + } catch (Exception e) { + log.error("创建流失败: {}", e.getMessage()); + throw new RuntimeException("创建流失败", e); + } + } + + /** + * 删除流 + */ + public void deleteStream(String streamId) { + String url = apiUrl + "/api/streams/" + streamId; + + try { + restTemplate.delete(url); + } catch (Exception e) { + log.error("删除流失败: {}", e.getMessage()); + throw new RuntimeException("删除流失败", e); + } + } + + /** + * 获取WebRTC Offer + */ + public String getOffer(String streamId, String sdp) { + String url = apiUrl + "/api/stream/" + streamId + "/webrtc"; + WebRTCRequest request = new WebRTCRequest(sdp); + + try { + ResponseEntity<WebRTCResponse> response = + restTemplate.postForEntity(url, request, WebRTCResponse.class); + return response.getBody().getSdp(); + } catch (Exception e) { + log.error("获取WebRTC Offer失败: {}", e.getMessage()); + throw new RuntimeException("获取WebRTC Offer失败", e); + } + } + + @Data + @AllArgsConstructor + static class StreamConfig { + private String input; + } + + @Data + @AllArgsConstructor + static class WebRTCRequest { + private String sdp; + } + + @Data + static class WebRTCResponse { + private String sdp; + } +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/config/EasyEsConfig.java b/src/main/java/com/guwan/backend/config/EasyEsConfig.java index f63ad93..37ab97d 100644 --- a/src/main/java/com/guwan/backend/config/EasyEsConfig.java +++ b/src/main/java/com/guwan/backend/config/EasyEsConfig.java @@ -1,9 +1,19 @@ package com.guwan.backend.config; +import cn.easyes.starter.factory.IndexStrategyFactory; import cn.easyes.starter.register.EsMapperScan; +import cn.easyes.starter.config.EasyEsConfigProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @EsMapperScan("com.guwan.backend.es.mapper") +@EnableConfigurationProperties(EasyEsConfigProperties.class) public class EasyEsConfig { + + @Bean + public IndexStrategyFactory indexStrategyFactory() { + return new IndexStrategyFactory(); + } } \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/config/ElasticsearchConfig.java b/src/main/java/com/guwan/backend/config/ElasticsearchConfig.java index 12d1e32..dd0f699 100644 --- a/src/main/java/com/guwan/backend/config/ElasticsearchConfig.java +++ b/src/main/java/com/guwan/backend/config/ElasticsearchConfig.java @@ -15,12 +15,6 @@ public class ElasticsearchConfig { @Value("${easy-es.address}") private String address; - @Value("${easy-es.username:}") - private String username; - - @Value("${easy-es.password:}") - private String password; - @Bean public RestHighLevelClient restHighLevelClient() { String[] parts = address.split(":"); diff --git a/src/main/java/com/guwan/backend/config/RestTemplateConfig.java b/src/main/java/com/guwan/backend/config/RestTemplateConfig.java new file mode 100644 index 0000000..e2399a2 --- /dev/null +++ b/src/main/java/com/guwan/backend/config/RestTemplateConfig.java @@ -0,0 +1,19 @@ +package com.guwan.backend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; +import java.time.Duration; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder + .setConnectTimeout(Duration.ofSeconds(10)) + .setReadTimeout(Duration.ofSeconds(30)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/constant/SecurityConstants.java b/src/main/java/com/guwan/backend/constant/SecurityConstants.java index 108c2be..8bd8738 100644 --- a/src/main/java/com/guwan/backend/constant/SecurityConstants.java +++ b/src/main/java/com/guwan/backend/constant/SecurityConstants.java @@ -13,6 +13,7 @@ public class SecurityConstants { */ public static final List<String> WHITE_LIST = List.of( "/demo/**", // 测试接口 + "/api/products", "/api/user/register", // 用户注册 "/api/user/login", // 用户登录 "/api/user/getEmailCode", // 获取邮箱验证码 diff --git a/src/main/java/com/guwan/backend/controller/DemoController.java b/src/main/java/com/guwan/backend/controller/DemoController.java index d725b0d..adbe1af 100644 --- a/src/main/java/com/guwan/backend/controller/DemoController.java +++ b/src/main/java/com/guwan/backend/controller/DemoController.java @@ -26,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; @@ -51,15 +52,15 @@ public class DemoController { (bucketName, minioUtil.uploadFile(bucketName, file)))); } - + @GetMapping("demo111") public int saveTenPerson() { try { try { GetObjectArgs args = GetObjectArgs.builder() - .bucket("images") - .object("test/webwxgetmsgimg.jpg") + .bucket("videos") + .object("James Gosling.jpg") .build(); //判断人脸照片是否合格 @@ -86,7 +87,7 @@ public class DemoController { faceRecognitionResDTO.setRect(faceInfo.getRect()); byte[] featureBytes = faceEngineService.extractFaceFeature(rgbData, faceInfo, ExtractType.REGISTER); if (featureBytes != null) { - System.out.println("featureBytes = " + featureBytes); + System.out.println("featureBytes = " + Arrays.toString(featureBytes)); }else{ log.error("图片不合格,未检测到人脸"); return 2; diff --git a/src/main/java/com/guwan/backend/controller/LiveStreamController.java b/src/main/java/com/guwan/backend/controller/LiveStreamController.java new file mode 100644 index 0000000..62b81c6 --- /dev/null +++ b/src/main/java/com/guwan/backend/controller/LiveStreamController.java @@ -0,0 +1,80 @@ +package com.guwan.backend.controller; + +import com.guwan.backend.common.Result; +import com.guwan.backend.dto.live.CreateRoomRequest; +import com.guwan.backend.dto.live.StartLiveRequest; +import com.guwan.backend.dto.live.WebRTCRequest; +import com.guwan.backend.dto.live.WebRTCResponse; +import com.guwan.backend.entity.LiveRoom; +import com.guwan.backend.service.LiveStreamService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@Tag(name = "直播管理", description = "直播相关接口") +@RestController +@RequestMapping("/api/live") +@RequiredArgsConstructor +public class LiveStreamController { + + private final LiveStreamService liveStreamService; + + @Operation(summary = "创建直播间") + @PostMapping("/room") + public Result<LiveRoom> createLiveRoom(@RequestBody CreateRoomRequest request) { + try { + LiveRoom room = liveStreamService.createLiveRoom( + request.getTitle(), + request.getDescription(), + request.getUserId() + ); + return Result.success(room); + } catch (Exception e) { + log.error("创建直播间失败", e); + return Result.error(e.getMessage()); + } + } + + @Operation(summary = "开始直播") + @PostMapping("/room/{id}/start") + public Result<Void> startLive( + @PathVariable Long id, + @RequestBody StartLiveRequest request) { + try { + liveStreamService.startLive(id, request.getSourceUrl()); + return Result.success(); + } catch (Exception e) { + log.error("开始直播失败", e); + return Result.error(e.getMessage()); + } + } + + @Operation(summary = "结束直播") + @PostMapping("/room/{id}/end") + public Result<Void> endLive(@PathVariable Long id) { + try { + liveStreamService.endLive(id); + return Result.success(); + } catch (Exception e) { + log.error("结束直播失败", e); + return Result.error(e.getMessage()); + } + } + + @Operation(summary = "获取WebRTC Offer") + @PostMapping("/room/{id}/webrtc") + public Result<WebRTCResponse> getWebRTCOffer( + @PathVariable Long id, + @RequestBody WebRTCRequest request) { + try { + String sdp = liveStreamService.getWebRTCOffer(id, request.getSdp()); + return Result.success(new WebRTCResponse(sdp)); + } catch (Exception e) { + log.error("获取WebRTC Offer失败", e); + return Result.error(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/controller/ProductController.java b/src/main/java/com/guwan/backend/controller/ProductController.java new file mode 100644 index 0000000..ed5c39e --- /dev/null +++ b/src/main/java/com/guwan/backend/controller/ProductController.java @@ -0,0 +1,63 @@ +package com.guwan.backend.controller; + +import com.guwan.backend.common.Result; +import com.guwan.backend.dto.product.ProductDTO; +import com.guwan.backend.service.ProductSearchService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +@Slf4j +@Tag(name = "商品搜索", description = "商品搜索相关接口") +@RestController +@RequestMapping("/api/products") +@RequiredArgsConstructor +public class ProductController { + + @Autowired(required = false) + private ProductSearchService productSearchService; + + @Operation(summary = "保存商品") + @PostMapping + public Result<ProductDTO> save(@RequestBody ProductDTO product) { + try { + if (productSearchService == null) { + return Result.error("搜索服务未启用"); + } + productSearchService.saveOrUpdate(product); + return Result.success(product); + } catch (Exception e) { + log.error("保存商品失败", e); + return Result.error(e.getMessage()); + } + } + + @Operation(summary = "搜索商品") + @GetMapping("/search") + public Result<List<ProductDTO>> search( + @Parameter(description = "搜索关键词") @RequestParam String keyword) { + try { + return Result.success(productSearchService.search(keyword)); + } catch (Exception e) { + log.error("搜索商品失败", e); + return Result.error(e.getMessage()); + } + } + + @Operation(summary = "按分类查询商品") + @GetMapping("/category/{category}") + public Result<List<ProductDTO>> getByCategory( + @Parameter(description = "商品分类") @PathVariable String category) { + try { + return Result.success(productSearchService.getByCategory(category)); + } catch (Exception e) { + log.error("查询商品分类失败", e); + return Result.error(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/controller/VideoController.java b/src/main/java/com/guwan/backend/controller/VideoController.java index a1c169b..7e4b136 100644 --- a/src/main/java/com/guwan/backend/controller/VideoController.java +++ b/src/main/java/com/guwan/backend/controller/VideoController.java @@ -3,6 +3,7 @@ package com.guwan.backend.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.guwan.backend.common.Result; import com.guwan.backend.dto.video.VideoDTO; +import com.guwan.backend.dto.video.VideoUploadDTO; import com.guwan.backend.service.VideoService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -26,13 +27,10 @@ public class VideoController { @Operation(summary = "上传视频", description = "上传视频文件并返回视频信息") // @SecurityRequirement(name = "bearer-jwt") @PostMapping("/upload") - public Result<VideoDTO> uploadVideo( - @Parameter(description = "视频文件") @RequestParam("fileUrl") String fileUrl, - @Parameter(description = "视频标题") @RequestParam("title") String title, - @Parameter(description = "视频描述") @RequestParam("description") String description, - @Parameter(description = "视频标签,多个用逗号分隔") @RequestParam(value = "tags", required = false) String tags) { + public Result<VideoDTO> uploadVideo(@RequestBody VideoUploadDTO videoUploadDTO) { try { - VideoDTO video = videoService.uploadVideo(fileUrl, title, description, tags); + VideoDTO video = videoService.uploadVideo(videoUploadDTO.getFileUrl(), + videoUploadDTO.getTitle(), videoUploadDTO.getDescription(), videoUploadDTO.getTags()); return Result.success(video); } catch (Exception e) { log.error("上传视频失败", e); diff --git a/src/main/java/com/guwan/backend/dto/live/CreateRoomRequest.java b/src/main/java/com/guwan/backend/dto/live/CreateRoomRequest.java new file mode 100644 index 0000000..7b75e50 --- /dev/null +++ b/src/main/java/com/guwan/backend/dto/live/CreateRoomRequest.java @@ -0,0 +1,18 @@ +package com.guwan.backend.dto.live; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "创建直播间请求") +public class CreateRoomRequest { + + @Schema(description = "直播间标题") + private String title; + + @Schema(description = "直播间描述") + private String description; + + @Schema(description = "主播用户ID") + private Long userId; +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/dto/live/StartLiveRequest.java b/src/main/java/com/guwan/backend/dto/live/StartLiveRequest.java new file mode 100644 index 0000000..4c6911c --- /dev/null +++ b/src/main/java/com/guwan/backend/dto/live/StartLiveRequest.java @@ -0,0 +1,12 @@ +package com.guwan.backend.dto.live; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "开始直播请求") +public class StartLiveRequest { + + @Schema(description = "推流地址(RTMP/RTSP)") + private String sourceUrl; +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/dto/live/WebRTCRequest.java b/src/main/java/com/guwan/backend/dto/live/WebRTCRequest.java new file mode 100644 index 0000000..a8dc97d --- /dev/null +++ b/src/main/java/com/guwan/backend/dto/live/WebRTCRequest.java @@ -0,0 +1,12 @@ +package com.guwan.backend.dto.live; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "WebRTC请求") +public class WebRTCRequest { + + @Schema(description = "SDP offer") + private String sdp; +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/dto/live/WebRTCResponse.java b/src/main/java/com/guwan/backend/dto/live/WebRTCResponse.java new file mode 100644 index 0000000..5450242 --- /dev/null +++ b/src/main/java/com/guwan/backend/dto/live/WebRTCResponse.java @@ -0,0 +1,14 @@ +package com.guwan.backend.dto.live; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@Schema(description = "WebRTC响应") +public class WebRTCResponse { + + @Schema(description = "SDP answer") + private String sdp; +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/dto/product/ProductDTO.java b/src/main/java/com/guwan/backend/dto/product/ProductDTO.java new file mode 100644 index 0000000..2d1e128 --- /dev/null +++ b/src/main/java/com/guwan/backend/dto/product/ProductDTO.java @@ -0,0 +1,30 @@ +package com.guwan.backend.dto.product; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@Schema(description = "商品信息DTO") +public class ProductDTO { + @Schema(description = "商品ID") + private String id; + + @Schema(description = "商品名称") + private String name; + + @Schema(description = "商品分类") + private String category; + + @Schema(description = "商品价格") + private Double price; + + @Schema(description = "库存数量") + private Integer stock; + + @Schema(description = "商品描述") + private String description; + + @Schema(description = "创建时间") + private LocalDateTime createdTime; +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/dto/video/VideoUploadDTO.java b/src/main/java/com/guwan/backend/dto/video/VideoUploadDTO.java new file mode 100644 index 0000000..aee773f --- /dev/null +++ b/src/main/java/com/guwan/backend/dto/video/VideoUploadDTO.java @@ -0,0 +1,19 @@ +package com.guwan.backend.dto.video; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.web.bind.annotation.RequestParam; + +@Schema(description = "视频上传DTO") +@Data +public class VideoUploadDTO { + @Schema(description = "视频文件地址") + String fileUrl; + @Schema(description = "视频标题") + String title; + @Schema(description = "视频描述") + String description; + @Schema(description = "视频标签,多个用逗号分隔", nullable = true) + String tags; +} diff --git a/src/main/java/com/guwan/backend/entity/LiveRoom.java b/src/main/java/com/guwan/backend/entity/LiveRoom.java index 8403d00..76edb52 100644 --- a/src/main/java/com/guwan/backend/entity/LiveRoom.java +++ b/src/main/java/com/guwan/backend/entity/LiveRoom.java @@ -16,10 +16,7 @@ public class LiveRoom { private String coverUrl; // 封面图 private Long userId; // 主播ID private String username; // 主播名称 - private String streamKey; // 推流密钥 - private String rtmpUrl; // RTMP推流地址 - private String hlsUrl; // HLS播放地址 - private String replayUrl; // 回放地址 + private String streamId; // Go2RTC流ID private String status; // 状态:PREPARING-准备中,LIVING-直播中,ENDED-已结束 private Integer onlineCount; // 在线人数 private Integer likeCount; // 点赞数 diff --git a/src/main/java/com/guwan/backend/es/document/ProductDocument.java b/src/main/java/com/guwan/backend/es/document/ProductDocument.java new file mode 100644 index 0000000..1e5d6b2 --- /dev/null +++ b/src/main/java/com/guwan/backend/es/document/ProductDocument.java @@ -0,0 +1,34 @@ +package com.guwan.backend.es.document; + +import cn.easyes.annotation.IndexField; +import cn.easyes.annotation.IndexId; +import cn.easyes.annotation.IndexName; +import cn.easyes.annotation.rely.FieldType; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@IndexName("products") +public class ProductDocument { + + @IndexId + private String id; + + @IndexField(fieldType = FieldType.TEXT, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") + private String name; + + @IndexField(fieldType = FieldType.KEYWORD) + private String category; + + @IndexField(fieldType = FieldType.DOUBLE) + private Double price; + + @IndexField(fieldType = FieldType.INTEGER) + private Integer stock; + + @IndexField(fieldType = FieldType.TEXT, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") + private String description; + + @IndexField(fieldType = FieldType.DATE) + private LocalDateTime createdTime; +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/es/mapper/ProductEsMapper.java b/src/main/java/com/guwan/backend/es/mapper/ProductEsMapper.java new file mode 100644 index 0000000..1706cf1 --- /dev/null +++ b/src/main/java/com/guwan/backend/es/mapper/ProductEsMapper.java @@ -0,0 +1,9 @@ +package com.guwan.backend.es.mapper; + +import cn.easyes.core.conditions.interfaces.BaseEsMapper; +import com.guwan.backend.es.document.ProductDocument; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductEsMapper extends BaseEsMapper<ProductDocument> { +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/service/LiveStreamService.java b/src/main/java/com/guwan/backend/service/LiveStreamService.java new file mode 100644 index 0000000..067d0cb --- /dev/null +++ b/src/main/java/com/guwan/backend/service/LiveStreamService.java @@ -0,0 +1,88 @@ +package com.guwan.backend.service; + +import com.guwan.backend.client.Go2RTCClient; +import com.guwan.backend.entity.LiveRoom; +import com.guwan.backend.mapper.LiveRoomMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class LiveStreamService { + + private final Go2RTCClient go2rtcClient; + private final LiveRoomMapper liveRoomMapper; + + /** + * 创建直播间 + */ + @Transactional + public LiveRoom createLiveRoom(String title, String description, Long userId) { + // 生成streamId + String streamId = UUID.randomUUID().toString(); + + // 创建直播间记录 + LiveRoom room = new LiveRoom(); + room.setTitle(title); + room.setDescription(description); + room.setUserId(userId); + room.setStreamId(streamId); + room.setStatus("PREPARING"); + + liveRoomMapper.insert(room); + return room; + } + + /** + * 开始直播 + */ + @Transactional + public void startLive(Long roomId, String sourceUrl) { + LiveRoom room = liveRoomMapper.selectById(roomId); + if (room == null) { + throw new IllegalArgumentException("直播间不存在"); + } + + // 创建Go2RTC流 + go2rtcClient.createStream(room.getStreamId(), sourceUrl); + + // 更新状态 + room.setStatus("LIVING"); + liveRoomMapper.updateById(room); + } + + /** + * 结束直播 + */ + @Transactional + public void endLive(Long roomId) { + LiveRoom room = liveRoomMapper.selectById(roomId); + if (room == null) { + throw new IllegalArgumentException("直播间不存在"); + } + + // 删除Go2RTC流 + go2rtcClient.deleteStream(room.getStreamId()); + + // 更新状态 + room.setStatus("ENDED"); + liveRoomMapper.updateById(room); + } + + /** + * 获取WebRTC Offer + */ + public String getWebRTCOffer(Long roomId, String sdp) { + LiveRoom room = liveRoomMapper.selectById(roomId); + if (room == null) { + throw new IllegalArgumentException("直播间不存在"); + } + + return go2rtcClient.getOffer(room.getStreamId(), sdp); + } +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/service/ProductSearchService.java b/src/main/java/com/guwan/backend/service/ProductSearchService.java new file mode 100644 index 0000000..ca1a9c4 --- /dev/null +++ b/src/main/java/com/guwan/backend/service/ProductSearchService.java @@ -0,0 +1,81 @@ +package com.guwan.backend.service; + +import cn.easyes.core.conditions.LambdaEsQueryWrapper; +import com.guwan.backend.dto.product.ProductDTO; +import com.guwan.backend.es.document.ProductDocument; +import com.guwan.backend.es.mapper.ProductEsMapper; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.SneakyThrows; + +@Slf4j +@Service +@ConditionalOnProperty(name = "easy-es.enable", havingValue = "true") +@RequiredArgsConstructor +public class ProductSearchService { + + private final ProductEsMapper productEsMapper; + + public void saveOrUpdate(ProductDTO productDTO) { + ProductDocument document = convertToDocument(productDTO); + productEsMapper.insert(document); + } + + public void delete(Long id) { + productEsMapper.deleteById(id); + } + + public List<ProductDTO> search(String keyword) { + LambdaEsQueryWrapper<ProductDocument> wrapper = new LambdaEsQueryWrapper<>(); + wrapper.and(w -> w + .match(ProductDocument::getName, keyword) + .or() + .match(ProductDocument::getDescription, keyword) + ); + + List<ProductDocument> documents = productEsMapper.selectList(wrapper); + return documents.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + public List<ProductDTO> getByCategory(String category) { + LambdaEsQueryWrapper<ProductDocument> wrapper = new LambdaEsQueryWrapper<>(); + wrapper.eq(ProductDocument::getCategory, category); + + List<ProductDocument> documents = productEsMapper.selectList(wrapper); + return documents.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + private ProductDocument convertToDocument(ProductDTO dto) { + ProductDocument document = new ProductDocument(); + BeanUtils.copyProperties(dto, document); + return document; + } + + private ProductDTO convertToDTO(ProductDocument document) { + ProductDTO dto = new ProductDTO(); + BeanUtils.copyProperties(document, dto); + return dto; + } + + @PostConstruct + public void init() { + log.info("ProductSearchService initialized with ES enabled"); + } + + @SneakyThrows + public void handleError(String operation, Exception e) { + log.error("ES {} operation failed", operation, e); + throw new RuntimeException("搜索服务暂时不可用"); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 11b9daf..41f3dd0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,13 +15,13 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root - password: root + password: 123456 # Redis配置 data: redis: host: localhost - port: 6379 + port: 6380 password: 123456 # 如果有密码,请设置 database: 8 timeout: 10000 @@ -117,10 +117,10 @@ springdoc: config: arcface-sdk: version: 4.1 - app-id: BYYPcPa3qQaAA88HZw7awne1BqZiePgT6axtALtK2qun - sdk-key: 3eFAwSCMQRhoUoAaZ2Li9XUVDUubAWYksQtNPWD32UrX - active-key: 82K1-11TQ-W11B-H7MU - active-file: ArcFacePro64.dat + app-id: 5nPWymNAibvWTq6XPypUWxroyzjMScZ9RwVkDjCFgK32 + sdk-key: 7dsPvanADtYAP1TiiiFjTsms2mAU85m5duVwHChhumyV + active-key: 86C1-11T1-K131-FJQU + active-file: 86C111T1K131FJQU.dat detect-pool-size: 16 compare-pool-size: 16 rec-face-thd: 0.8 @@ -132,10 +132,8 @@ srs: url: http://localhost:1985 # SRS HTTP API地址 easy-es: - enable: false # 改为false禁用Easy-Es - address: localhost:9200 # ES地址 - username: ${ES_USERNAME:} # ES用户名,可选 - password: ${ES_PASSWORD:} # ES密码,可选 + enable: true + address: localhost:9200 global-config: process-index-mode: manual print-dsl: true @@ -143,7 +141,12 @@ easy-es: response-log: true db-config: map-underscore-to-camel-case: true - index-prefix: video_ # 索引前缀 + index-prefix: product_ + async-process: true + schema-update: true + max-connect-num: 50 + connect-timeout: 5000 + socket-timeout: 60000 netty: danmaku: @@ -155,4 +158,8 @@ netty: heartbeat: interval: 30 cluster: - nodes: localhost:8088,localhost:8089 \ No newline at end of file + nodes: localhost:8088,localhost:8089 + +go2rtc: + api: + url: http://localhost:1984 # Go2RTC API地址 \ No newline at end of file diff --git a/src/main/resources/db/migration/V9__alter_live_room_add_stream_id.sql b/src/main/resources/db/migration/V9__alter_live_room_add_stream_id.sql new file mode 100644 index 0000000..65b5a64 --- /dev/null +++ b/src/main/resources/db/migration/V9__alter_live_room_add_stream_id.sql @@ -0,0 +1,6 @@ +ALTER TABLE `live_room` +ADD COLUMN `stream_id` varchar(50) NOT NULL COMMENT 'Go2RTC流ID' AFTER `username`, +DROP COLUMN `stream_key`, +DROP COLUMN `rtmp_url`, +DROP COLUMN `hls_url`, +DROP COLUMN `replay_url`; \ No newline at end of file