SpringCloud + Spring AI Alibaba 整合阿里云百炼大模型

一、前言

记录一次自己使用微服务整合阿里云的百炼大模型,需要用到Redis来记录最近五条信息,已能够保证上下文的连通性,Ai和用户之间的对话是使用的MongoDB来进行存储。然后我这篇文章是介绍了两种请求方式,一种是通过Http请求,一种是通过WebSocket+Netty的方式,如果你还没有Redis可以先去安装对应环境或者可以将Redis改为通过使用List来对最近的消息进行存储。话不多说,开始。

二、引入依赖

(1)相关Maven依赖

 <!--            alibaba-ai-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-ai</artifactId>
    <version>2023.0.1.2</version>
</dependency>

 <!--            redis-->
 <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>

 <!--            netty-->
 <dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
 </dependency>

 <!--            mongodb-->

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
  </dependency>

(2)yaml配置

spring:
  redis:
    host: 192.168.254.100
    port: 6379
    password: 123456
  cloud:
      ai:
        tongyi:
          connection:
            #这里的api-key需要你到阿里云大模型控制台获取,具体获取可以百度
            api-key: 您的api-key
  data:
    mongodb:
      host: 192.168.254.100
      port: 27017
      database: chat
      password: 'mongo' #注意这里的密码一定要加单引号包裹起来,不然会连接不上
      username: mongo
      authentication-database: admin 

 三、Http请求方式

(1)Controller层

@RestController
@RequestMapping("/ai")
@CrossOrigin
public class AiController {

	@Autowired
	private AiService aiService;

	/**
	 * 调用百炼大模型
	 * @param message 询问消息
	 * @return 结果
	 */
	@GetMapping("/tongyi")
	public R<String> completion(@RequestParam(value = "message", defaultValue = "") String message) {
		return aiService.handleAI(message,0L);
	}


	/**
	 * 获取与ai的聊天记录
	 * @param timestamp 时间戳(作为搜索游标)
	 * @param size 每页显示行数
	 * @return 结果
	 */
	@GetMapping("/getAiChatLogs")
	public TableDataInfo getAiChatLogs(@RequestParam(defaultValue = "0") Long timestamp,
	                                   @RequestParam(defaultValue = "20") int size){
		return aiService.getAiChatLogs(timestamp,size);
	}
}

(2)Service层

public interface AiService {

	/**
	 * 调用百炼大模型
	 * @param message 询问消息
	 * @return 结果
	 */
	R<String> handleAI(String message, Long userId);

	/**
	 * 获取与ai的聊天记录
	 * @param timestamp 时间戳(作为搜索游标)
	 * @param size 每页显示行数
	 * @return 结果
	 */
	TableDataInfo getAiChatLogs(Long timestamp, int size);
}

(3)ServiceImpl层

@Service
@Slf4j
public class AiServiceImpl implements AiService {

	private static final String REDIS_KEY_PREFIX = "ai:chat:history:"; // Redis键前缀
	private static final int MAX_HISTORY_ROUNDS = 5; // 保留最近5轮对话

	@Autowired
	private RedisService redisService;

	@Autowired
	private MongoTemplate mongoTemplate;

	/**
	 * 调用百炼大模型
	 * @param message 询问消息
	 * @return 结果
	 */
	@Override
	public R<String> handleAI(String message, Long userId) {
		Generation gen = new Generation();

		// 从 Redis 中获取历史消息
		List<Message> messages = getHistoryFromRedis(userId);
		// 添加用户消息
		Message userMsg = Message.builder()
				.role(Role.USER.getValue())
				.content(message)
				.build();
		messages.add(userMsg);

		// 构建请求参数
		GenerationParam param = GenerationParam.builder()
				.model("qwen-turbo")  //指定使用的 AI 模型名称
				.messages(messages) //设置对话上下文 (说明:接收一个 List<Message> 对象,包含用户与 AI 的历史对话记录。模型会根据完整的消息列表理解上下文关系,生成连贯的回复)
				.resultFormat(GenerationParam.ResultFormat.MESSAGE) //指定响应格式(MESSAGE 表示返回结构化消息对象(包含角色、内容等元数据))
				.topK(50) //控制候选词范围(每个生成步骤仅考虑概率最高的前 50 个候选词。增大该值会提高多样性但可能降低准确性,减小则使输出更集中)
				.temperature(0.8f) //调节输出随机性(0.8 属于中等偏高随机性,适合需要创造性但保持一定连贯性的场景)
				.topP(0.8) //动态候选词选择(核采样)
				.seed(1234) //设置随机种子(固定种子(如 1234)可使生成结果可重复)
				.build();

		try {
			// 调用API并获取回复
			GenerationResult result = gen.call(param);
			Message aiMessage = result.getOutput().getChoices().get(0).getMessage();
			String content = aiMessage.getContent();
			// 将AI回复加入对话历史
			messages.add(aiMessage);
			// 保存对话历史到 Redis
			saveHistoryToRedis(userId, messages);
			return R.ok(content);
		} catch (NoApiKeyException | InputRequiredException e) {
			log.error("调用模型出错---->{}",e.getMessage());
			throw new RuntimeException(e);
		}
	}

	/**
	 * 获取与ai的聊天记录
	 * @param timestamp 时间戳(作为搜索游标)
	 * @param size 每页显示行数
	 * @return 结果
	 */
	@Override
	public TableDataInfo getAiChatLogs(Long timestamp, int size) {
		// 创建分页请求,按create_time降序
		Query query = new Query()
				.with(Sort.by(Sort.Direction.DESC, "timestamp"))
				.limit(size);
		//添加用户作为条件
		Long userId = SecurityUtils.getUserId();
		query.addCriteria(Criteria.where("userId").is(userId));
		if (timestamp != null && timestamp>0) {
			// 添加条件:timestamp < 上一页最后一条记录的 timestamp
			query.addCriteria(Criteria.where("timestamp").lt(timestamp));
		}
		List<AiChat> aiChats = mongoTemplate.find(query, AiChat.class);
		Collections.reverse(aiChats);
		TableDataInfo tableDataInfo = new TableDataInfo();
		tableDataInfo.setCode(200);
		tableDataInfo.setMsg("成功");
		tableDataInfo.setRows(aiChats);
		return tableDataInfo;
	}

	/**
	 * 从 Redis 获取历史对话记录
	 */
	private List<Message> getHistoryFromRedis(Long userId) {
		String redisKey = REDIS_KEY_PREFIX + userId;
		Object obj = redisService.get(redisKey);
		if (obj instanceof String) {
			return JSON.parseArray((String) obj, Message.class);
		}
		List<Message> objects = new ArrayList<>();
		// 添加系统消息(只在会话首次建立时添加)
		Message systemMsg = Message.builder()
				.role(Role.SYSTEM.getValue())
				.content("你的身份是一名AI教练,你只回答关于健身方面的问题,其他问题可以委婉表明自己只能回答健身有关的问题!")
				.build();
		objects.add(systemMsg);
		return objects;
	}

	/**
	 * 保存对话历史到 Redis
	 */
	private void saveHistoryToRedis(Long userId, List<Message> messages) {
		truncateHistory(messages);
		String redisKey = REDIS_KEY_PREFIX + userId;
		// 转换为JSON字符串存储
		String jsonString = JSON.toJSONString(messages);
		redisService.set(redisKey, jsonString, 30 * 60);
	}

	/**
	 * 截断历史记录,保留最近的对话轮次
	 */
	private void truncateHistory(List<Message> messages) {
		int maxSize = 1 + MAX_HISTORY_ROUNDS * 2;
		if (messages.size() > maxSize) {
			List<Message> truncated = new ArrayList<>();
			// 添加类型校验
			if (messages.get(0) != null) {
				truncated.add(messages.get(0));
			}
			int start = Math.max(1, messages.size() - MAX_HISTORY_ROUNDS * 2);
			truncated.addAll(messages.subList(start, messages.size()));
			messages.clear();
			messages.addAll(truncated);
		}
	}
}

四、WebSocket+Netty方式

(1)创建Session层用于保存连接与用户的关联

  •         创建AiSession
/**
 * 存储ai业务中用户与netty之间的关联关系
 */
public interface AiSession {

	void save(Long userId, Channel channel);

	Channel getChannelByUserId(Long userId);

	Long getUserIdByChannel(Channel channel);

	void removeSessionByUserId(Long userId);

	void removeSessionByChannel(Channel channel);

	void clearAllSession();

}
  •         AiSession对应的实现类
@Service
public class AiSessionImpl implements AiSession {

	//用户id与Channel连接(key:用户id,value:channel)
	private final Map<Long, Channel> userIdLinkChannel = new HashMap<>();

	//Channel与用户id连接(key:channel,value:用户id)
	private final Map<Channel, Long> channelLinkUserId = new HashMap<>();


	/**
	 * 保存userId和Channel关系
	 * @param userId 用户id
	 * @param channel channel
	 */
	@Override
	public void save(Long userId, Channel channel) {
		userIdLinkChannel.put(userId,channel);
		channelLinkUserId.put(channel,userId);
	}

	/**
	 * 根据用户id获取Channel
	 * @param userId 用户id
	 * @return 结果
	 */
	@Override
	public Channel getChannelByUserId(Long userId) {
		return userIdLinkChannel.get(userId);
	}

	/**
	 * 根据Channel获取用户id
	 * @param channel Channel
	 * @return 结果
	 */
	@Override
	public Long getUserIdByChannel(Channel channel) {
		return channelLinkUserId.get(channel);
	}

	/**
	 * 根据用户id删除userId和Channel相互关联
	 * @param userId 用户id
	 */
	@Override
	public void removeSessionByUserId(Long userId) {
		Channel channelByUserId = getChannelByUserId(userId);
		channelLinkUserId.remove(channelByUserId);
		userIdLinkChannel.remove(userId);
	}

	/**
	 * 根据用户Channel删除userId和Channel相互关联
	 * @param channel channel
	 */
	@Override
	public void removeSessionByChannel(Channel channel) {
		Long userIdByChannel = getUserIdByChannel(channel);
		userIdLinkChannel.remove(userIdByChannel);
		channelLinkUserId.remove(channel);
	}

	/**
	 * 清空所有关联关系
	 */
	@Override
	public void clearAllSession() {
		userIdLinkChannel.clear();
		channelLinkUserId.clear();
	}


}

(2)Netty配置

  • 创建WebSocketNettyServer
@Slf4j
@Component
public class WebSocketNettyServer {

	@Autowired
	private AiInitializer aiInitializer;

	private final ServerBootstrap aiServerBootstrap = new ServerBootstrap();

	private final EventLoopGroup bossGroup = new NioEventLoopGroup(1);

	private final EventLoopGroup workerGroup = new NioEventLoopGroup();

	@PostConstruct
	public void WebSocketNettyServerInit() {
/*		// 初始化服务器启动对象
		// 主线程池
		NioEventLoopGroup mainGrp = new NioEventLoopGroup();
		// 从线程池
		NioEventLoopGroup subGrp = new NioEventLoopGroup();*/

		aiServerBootstrap
				// 指定使用上面创建的两个线程池
				.group(bossGroup, workerGroup)
				// 指定Netty通道类型
				.channel(NioServerSocketChannel.class)
				// 指定通道初始化器用来加载当Channel收到事件消息后
				.childHandler(aiInitializer);


	}

	public void start() throws InterruptedException {
		// 绑定服务器端口,以异步的方式启动服务器
		ChannelFuture futureRelays = aiServerBootstrap.bind("0.0.0.0",6000).sync();
		if (futureRelays.isSuccess()){
			log.info("ai-netty初始化完成,端口6000)");
		}
	}
}
  • 创建AiInitializer
@Component
public class AiInitializer extends ChannelInitializer<SocketChannel> {

	@Autowired
	private AiHandler aiHandler;
	@Override
	protected void initChannel(SocketChannel socketChannel) throws Exception {
		//获取对应的管道
		ChannelPipeline pipeline = socketChannel.pipeline();
		pipeline
		        .addLast(new HttpServerCodec())
				//添加对大数据流的支持
				.addLast(new ChunkedWriteHandler())
				//添加聚合器
				.addLast(new HttpObjectAggregator(1024 * 64*64))
				//设置websocket连接前缀前缀
				//心跳检查(30秒)
				.addLast(new IdleStateHandler(30, 0, 0))
				//添加自定义处理器
				.addLast(new WebSocketServerProtocolHandler("/ws",null,true))
				.addLast(aiHandler);
	}
}
  • 创建AiHandler
@Component
@Slf4j
public class AiHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

	@Autowired
	private AiSession aiSession;

	@Autowired
	private AiService tongYiService;

	@Autowired
	private MongoTemplate mongoTemplate;

	@Override
	public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
		JSONObject jsonObject = JSON.parseObject(msg.text());
		//获取消息类型
		Object method = jsonObject.get("method");
		// 处理消息
		//ping
		if ("ping".equals(method)){
			LoginUser loginUser = AuthUtil.getLoginUser(jsonObject.get("Authorization").toString());
			if (Objects.isNull(loginUser)){
				//表明重新登陆
				AiResponse responseData = new AiResponse();
				responseData.setCode(10002);
				responseData.setValue("relogin");
				responseData.setMethod("error");
				ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(responseData)));
				return;
			}
			//返回ack,表示心跳正常
			aiSession.save(loginUser.getUserid(),ctx.channel());
			AiResponse responseData = new AiResponse();
			responseData.setValue(String.valueOf(System.currentTimeMillis()));
			responseData.setMethod("ack");
			ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(responseData)));
		}else if ("send".equals(method)){ //消息发送
			Long userId = aiSession.getUserIdByChannel(ctx.channel());
			if (Objects.nonNull(userId)){
				Object value = jsonObject.get("value");
				log.info("发送的内容------->{}",value);
				//请求大模型api
				R<String> result = tongYiService.handleAI(value.toString().trim(),userId);
				//封装回复消息
				String aiReponseText = result.getData();
				log.info("Ai回复的内容-------->{}",aiReponseText);
				AiResponse responseData = new AiResponse();
				responseData.setCode(200);
				responseData.setValue(aiReponseText);
				responseData.setMethod("response");
				//返回消息
				ChannelFuture channelFuture = ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(responseData)));
				channelFuture.addListener(listener->{
					if (listener.isSuccess()){
						//封装用户发送的消息
						AiChat userChat = new AiChat();
						userChat.setId(IdUtils.randomUUID());
						userChat.setShowText(value.toString());
						userChat.setIsUser(true);
						userChat.setText(value.toString());
						userChat.setTimestamp((Long) jsonObject.get("timestamp"));
						userChat.setUserId(userId);

						//封装ai回复消息
						AiChat aiChat = new AiChat();
						aiChat.setId(IdUtils.randomUUID());
						aiChat.setShowText(aiReponseText);
						aiChat.setText(aiReponseText);
						aiChat.setIsUser(false);
						aiChat.setTimestamp(System.currentTimeMillis());
						aiChat.setUserId(userId);
						//保存回复的消息
						mongoTemplate.insertAll(Arrays.asList(userChat,aiChat));
					}
				});
			}else{
				//重新登陆
				AiResponse responseData = new AiResponse();
				responseData.setCode(10002);
				responseData.setValue("relogin");
				responseData.setMethod("error");
				ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(responseData)));
			}
		}
	}

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		// 添加连接
		System.out.println("新连接: " + ctx.channel().id().asShortText());
	}

	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		// 断开连接
		System.out.println("断开连接: " + ctx.channel().id().asShortText());
		aiSession.removeSessionByChannel(ctx.channel());
	}

	@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
		if (evt instanceof IdleStateEvent) {
			IdleState state = ((IdleStateEvent) evt).state();
			if (state == IdleState.READER_IDLE) {
				log.info("{}---心跳超时--->{}", ctx.channel().id().asShortText(), LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
				ctx.channel().close();
			}
		} else {
			super.userEventTriggered(ctx, evt);
		}
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		// 异常处理
		cause.printStackTrace();
//		ctx.close();
	}
}
  • 创建StartListener
@Component
public class StartListener implements ApplicationListener<ContextRefreshedEvent> {


	@Autowired
	private WebSocketNettyServer webSocketNettyServer;

	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		try {
			//启动netty服务
			webSocketNettyServer.start();
		} catch (Exception ignored) {

		}
	}
}

然后netty相关配置就搭建完成了,前端通过websocket请求路径ws://主机:6000/ws就可以连接到netty上来了,然后就可以通过websocket进行消息的发送和对回复的消息进推送了。

我使用的是uniapp搭建的小程序和app端,实测是可以的,PC的也肯定是可以

五、效果展示

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/980553.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

(贪心 合并区间)leetcode 56

思路来源&#xff1a;代码随想录--代码随想录_合并区间题解 首先用lambda 按照左界值升序排序 建立答案的二维数组&#xff0c;将第一个行区间放入&#xff0c;判断从第二行开始 第i行的左区间一定大于第i-1行的左区间&#xff08;排序过了&#xff09;&#xff0c;所以只判断…

CAN总线通信协议学习4——数据链路层之仲裁规则

CAN总线只有一对差分信号线&#xff0c;同一时间只能有一个设备操作总线发送数据若多个设备同时有发送需求&#xff0c;该如何分配总线资源? 解决问题的思路&#xff1a;制定资源分配规则&#xff0c;依次满足多个设备的发送需求&#xff0c;确保同一时间只有一个设备操作总线…

Kubespray部署企业级高可用K8S指南

目录 前言1 K8S集群节点准备1.1 主机列表1.2 kubespray节点python3及pip3准备1.2.1. 更新系统1.2.2. 安装依赖1.2.3. 下载Python 3.12源码1.2.4. 解压源码包1.2.5. 编译和安装Python1.2.6. 验证安装1.2.7. 设置Python 3.12为默认版本&#xff08;可选&#xff09;1.2.8. 安装pi…

互推机制在开源AI智能名片2+1链动模式S2B2C商城小程序源码推广中的应用探索

摘要&#xff1a; 在数字化营销时代&#xff0c;开源AI智能名片21链动模式S2B2C商城小程序源码作为一种创新的技术解决方案&#xff0c;正逐步成为企业数字化转型的重要工具。然而&#xff0c;面对激烈的市场竞争&#xff0c;如何高效推广这一前沿技术产品&#xff0c;成为开发…

波导阵列天线 学习笔记11双极化全金属垂直公共馈电平板波导槽阵列天线

摘要&#xff1a; 本communicaition提出了一种双极化全金属垂直公共馈电平板波导槽阵列天线。最初提出了一种公共馈电的单层槽平板波导来实现双极化阵列。此设计消除了传统背腔公共馈电的复杂腔体边缘的必要性&#xff0c;提供了一种更简单的天线结构。在2x2子阵列种发展了宽十…

腾讯游戏完成架构调整 IEG新设五大产品事业部

易采游戏网2月28日独家消息&#xff1a;继1月份腾讯天美工作室群完成内部组织架构调整后&#xff0c;腾讯旗下互动娱乐事业群&#xff08;IEG&#xff09;再次宣布对组织架构进行优化调整。此次调整的核心在于新设立了五大产品事业部&#xff0c;包括体育产品部、音舞产品部、V…

Vue3国际化开发实战:i18n-Ally + vue-i18n@next高效配置教程,项目中文显示

本文详解 Vue3 国际化开发全流程&#xff1a;从安装 vue-i18nnext 到配置多语言文件&#xff08;JSON/YAML&#xff09;&#xff0c;结合 i18n-Ally 插件实现高效翻译管理。重点涵盖&#xff1a; 工程配置&#xff1a;创建 i18n 实例、模块化语言文件结构&#xff08;支持命名…

【愚公系列】《Python网络爬虫从入门到精通》036-DataFrame日期数据处理

标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主,腾讯云内容共创官,掘金优秀博主,亚马逊技领云博主,51CTO博客专家等。近期荣誉2022年度…

计算机网络---TCP三握四挥

文章目录 TCPTCP 的核心特点TCP 与 UDP 特性对比TCP 标志位 TCP 的三次握手&#xff08;建立连接&#xff09;TCP 三次握手概述图解 TCP 三次握手为什么需要三次握手&#xff0c;而不是两次为什么要三次握手&#xff0c;而不是四次三次握手连接阶段&#xff0c;最后一次 ACK 包…

ubuntu服务器安装VASP.6.4.3

ubuntu服务器安装VASP.6.4.3 1 安装Intel OneAPI Base Toolkit和Intel OneAPI HPC Toolkit1.1 更新并安装环境变量1.2 下载Intel OneAPI Base Toolkit和Intel OneAPI HPC Toolkit安装包1.3 安装 Intel OneAPI Base Toolkit1.4 安装 Intel OneAPI HPC Toolkit1.5 添加并激活环境…

【大模型系列篇】DeepSeek开源周,解锁AI黑科技

&#x1f525; Day1&#xff1a;FlashMLA —— GPU推理加速器 专为处理长短不一的AI推理请求而生&#xff0c;就像给Hopper GPU装上了智能导航&#xff0c;让数据在芯片上跑出3000GB/s的"磁悬浮"速度。✅ 已支持BF16格式&#xff5c;580万亿次浮点运算/秒FlashMLA G…

【JSON2WEB】15 银河麒麟操作系统下部署JSON2WEB

【JSON2WEB】系列目录 【JSON2WEB】01 WEB管理信息系统架构设计 【JSON2WEB】02 JSON2WEB初步UI设计 【JSON2WEB】03 go的模板包html/template的使用 【JSON2WEB】04 amis低代码前端框架介绍 【JSON2WEB】05 前端开发三件套 HTML CSS JavaScript 速成 【JSON2WEB】06 JSO…

leetcode第40题组合总和Ⅱ

原题出于leetcode第40题https://leetcode.cn/problems/combination-sum-ii/题目如下&#xff1a; 给定一个候选人编号的集合 candidates &#xff08;candidate中有重复的元素&#xff09;和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合…

大语言模型学习--LangChain

LangChain基本概念 ReAct学习资料 https://zhuanlan.zhihu.com/p/660951271 LangChain官网地址 Introduction | &#x1f99c;️&#x1f517; LangChain LangChain是一个基于语言模型开发应用程序的框架。它可以实现以下应用程序&#xff1a; 数据感知&#xff1a;将语言模型…

【Android】类加载器热修复-随记(二)

1. 背景 在【Android】类加载器&热修复-随记一文中了解了类加载,要完成完整的热修复过程,我们需要构建出差量jar包。而这构建差量包分为两个步骤: 原包,注解解析和插桩;变更后,差量包构建;在这两步过程中会涉及到较多的字节码操作,这里我们需要了解下。我们都听过…

SpringBoot高校运动会管理系统 附带详细运行指导视频

文章目录 一、项目演示二、项目介绍三、运行截图四、主要代码1.报名赛事代码2.用户登录代码3.保存成绩代码 一、项目演示 项目演示地址&#xff1a; 视频地址 二、项目介绍 项目描述&#xff1a;这是一个基于SpringBoot框架开发的高校运动会管理系统项目。首先&#xff0c;这…

Readability.js 与 Newspaper提取网页内容和元数据

在当今信息爆炸的时代&#xff0c;网页内容的提取和处理变得尤为重要。无论是从新闻网站、博客还是教程网站中提取内容&#xff0c;都需要一个高效、准确的工具来帮助我们去除无关信息&#xff0c;提取出有价值的正文内容。这不仅能够提高我们的工作效率&#xff0c;还能让我们…

Linux中的UDP编程接口基本使用

UDP编程接口基本使用 本篇介绍 在前面网络基础部分已经介绍了网络的基本工作模式&#xff0c;有了这些理论基础之后&#xff0c;下面先从UDP编程开始从操作部分深入网络 在本篇中&#xff0c;主要考虑下面的内容&#xff1a; 创建并封装服务端&#xff1a;了解创建服务端的…

Yocto + 树莓派摄像头驱动完整指南

—— 从驱动配置、Yocto 构建&#xff0c;到 OpenCV 实战 在树莓派上运行摄像头&#xff0c;在官方的 Raspberry Pi OS 可能很简单&#xff0c;但在 Yocto 项目中&#xff0c;需要手动配置驱动、设备树、软件依赖 才能确保摄像头正常工作。本篇文章从 BSP 驱动配置、Yocto 关键…

【全栈开发】从0开始搭建一个图书管理系统【一】框架搭建

【全栈开发】从0开始搭建一个图书管理系统【一】框架搭建 前言 现在流行降本增笑&#xff0c;也就是不但每个人都要有事干不能闲着&#xff0c;更重要的是每个人都要通过报功的方式做到平日的各项工作异常饱和&#xff0c;实现1.5人的支出干2人的活计。单纯的数据库开发【肤浅…