当前位置: 首页 > news >正文

数字果园管理系统的设计与实现(Tensorflow的害虫识别结合高德API的害虫定位与Websocket的在线聊天室)

文章目录

    • 技术栈
    • 主要功能
    • 害虫识别与定位
      • 害虫识别的实现
        • 训练与测试评估代码
        • 模型转化为TFLite
        • 预测脚本
        • PredictController预测控制器
        • 害虫识别过程展示
      • 害虫定位实现
        • 害虫定位代码
        • 害虫定位过程展示
    • 专家咨询功能
      • 在线咨询聊天室
        • 主要前端代码如下
        • 主要后端代码如下

技术栈

  • Spring Boot
  • Vue3
  • MyBatis
  • ECharts
  • Tensorflow
  • ElementUI

主要功能

  1. 害虫识别与定位功能:利用Tensorflow深度学习框架,在预训练模型MobileNetV2的基础上进行训练优化,对训练集9840张,测试集2513张害虫图片进行训练,每种害虫大体上按照4:1的比例划分训练集和测试集,并生成可直接部署的模型文件并编写预测脚本。利用高德API对每种害虫出现地点进行标记定位并提供相关定位功能(步行、驾车、害虫定位、当前定位等)。
  2. 专家咨询功能:利用Websocket提供在线咨询,通过聊天室进行在线聊天并对每一次咨询进行系统自动记录。同时提供预约咨询功能(主要预约线下),提供咨询反馈,咨询者可以对每次咨询记录进行评星、反馈等。评星将直接影响到专家个人的评星。

害虫识别与定位

害虫识别的实现

因为害虫图片数据集有限,且在计算资源、硬件等方面受限,在训练上可能有失准确率,最终选择的版本(本系统使用的害虫识别模型)训练准确率:96.15%;验证准确率:90.55%;测试Top-1:78.90%;测试Top-3:93.87%;测试Top-5:96.78%。以下是多版本训练模型的表格:

模型增加的处理步骤训练准确率验证准确率测试Top-1测试Top-3测试Top-5
BaseMobileNetV2基础模型94.74%86.38%76.47%93.19%96.10%
MobileNetV2_V1-添加Dropout -规范验证集划分83.88%84.45%74.96%92.56%95.86%
MobileNetV2_V2-增强数据增强 -添加L2正则化 -分层Dropout -两阶段微调95.34%88.11%79.10%92.68%96.14%
MobileNetV2_V3-学习率调度可视化 -延长微调周期至70轮95.40%88.36%77.67%93.99%96.82%
本模型-AdamW优化器​ -余弦退火学习率 -梯度裁剪 -适度数据增强96.15%90.55%78.90%93.87%96.78%
训练与测试评估代码
import tensorflow as tf
from tensorflow.keras import layers, models, optimizers, regularizers
from tensorflow.keras.models import Sequential
from tensorflow.keras.callbacks import EarlyStopping, TensorBoard, LearningRateScheduler, ModelCheckpoint
import os
import json
import numpy as np
from sklearn.utils.class_weight import compute_class_weight# ======================
# 配置参数
# ======================
IMG_SIZE = (224, 224)
BATCH_SIZE = 64
EPOCHS = 100
NUM_CLASSES = len(os.listdir('train'))
LEARNING_RATE = 1e-4
FINE_TUNE_LR = 1e-6
SAVE_PATH = 'pestModel_MobileNetV2_V4'
REGULARIZATION = 1e-3
DROPOUT_RATE = 0.5# ======================
# 数据准备(新增部分)
# ======================
def prepare_datasets():# 加载原始数据集以获取class_namesraw_train = tf.keras.utils.image_dataset_from_directory('train',image_size=IMG_SIZE,batch_size=BATCH_SIZE,validation_split=0.2,subset='training',seed=123)class_names = raw_train.class_names  # 先获取类别名称# 训练集和验证集train_dataset = tf.keras.utils.image_dataset_from_directory('train',image_size=IMG_SIZE,batch_size=BATCH_SIZE,label_mode='categorical',validation_split=0.2,subset='training',seed=123).map(lambda x, y: (x/255.0, y))  # 归一化validation_dataset = tf.keras.utils.image_dataset_from_directory('train',image_size=IMG_SIZE,batch_size=BATCH_SIZE,label_mode='categorical',validation_split=0.2,subset='validation',seed=123).map(lambda x, y: (x/255.0, y))# 测试集test_dataset = tf.keras.utils.image_dataset_from_directory('test',image_size=IMG_SIZE,batch_size=BATCH_SIZE,label_mode='categorical').map(lambda x, y: (x/255.0, y))return train_dataset, validation_dataset, test_dataset, class_names# ======================
# 数据增强(调整参数以减少过度扰动)
# ======================
data_augmentation = Sequential([layers.RandomFlip("horizontal", seed=42),  # 仅水平翻转,保留语义信息layers.RandomRotation(factor=0.1, fill_mode='reflect'),  # 旋转角度±10%layers.RandomZoom(height_factor=(-0.05, 0.05)),  # 缩小缩放幅度layers.RandomContrast(factor=0.05),  # 降低对比度扰动强度
])# ======================
# 学习率调度(余弦退火)
# ======================
def lr_scheduler(epoch):warmup_epochs = 5total_epochs = 50  # 总训练周期if epoch < warmup_epochs:return LEARNING_RATE * (epoch + 1) / warmup_epochsprogress = (epoch - warmup_epochs) / (total_epochs - warmup_epochs)return LEARNING_RATE * 0.5 * (1 + tf.math.cos(np.pi * progress))# ======================
# 模型构建(优化分类头结构)
# ======================
def build_enhanced_model():base_model = tf.keras.applications.MobileNetV2(input_shape=(*IMG_SIZE, 3),include_top=False,weights='imagenet')base_model.trainable = Falseinputs = tf.keras.Input(shape=(*IMG_SIZE, 3))x = data_augmentation(inputs)x = base_model(x)x = layers.GlobalAveragePooling2D()(x)# 优化分类头(增加层间批标准化)x = layers.Dense(1024, kernel_regularizer=regularizers.l2(1e-4))(x)  # 降低L2系数x = layers.BatchNormalization()(x)x = layers.Activation('relu')(x)x = layers.Dropout(0.4)(x)  # 降低Dropout率x = layers.Dense(512, kernel_regularizer=regularizers.l2(1e-4))(x)x = layers.BatchNormalization()(x)x = layers.Activation('relu')(x)x = layers.Dropout(0.3)(x)  # 分层设置Dropoutoutputs = layers.Dense(NUM_CLASSES, activation='softmax',kernel_regularizer=regularizers.l2(1e-4))(x)model = tf.keras.Model(inputs, outputs)# 使用AdamW优化器(提升泛化能力)model.compile(optimizer=optimizers.AdamW(learning_rate=LEARNING_RATE, weight_decay=1e-5),loss=tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.05),  # 调整标签平滑metrics=['accuracy',tf.keras.metrics.TopKCategoricalAccuracy(k=3, name='top3_acc'),tf.keras.metrics.TopKCategoricalAccuracy(k=5, name='top5_acc')])return model# ======================
# 主程序流程
# ======================
if __name__ == "__main__":# 准备数据train_dataset, validation_dataset, test_dataset, class_names = prepare_datasets()  # 添加class_names接收# 计算类别权重y_train = np.concatenate([y for x, y in train_dataset], axis=0)class_weights = compute_class_weight('balanced',classes=np.arange(NUM_CLASSES),y=np.argmax(y_train, axis=1))class_weights = dict(enumerate(class_weights))# ======================# 回调配置(增加梯度裁剪与早停监控Top-3)# ======================callbacks = [EarlyStopping(monitor='val_top3_acc', patience=12, mode='max', restore_best_weights=True),ModelCheckpoint('best_model.h5', monitor='val_top3_acc', mode='max', save_best_only=True),LearningRateScheduler(lr_scheduler),TensorBoard(log_dir='./logs'),tf.keras.callbacks.TerminateOnNaN(),  # 防止数值不稳定]# 初始训练model = build_enhanced_model()history = model.fit(train_dataset,epochs=EPOCHS,validation_data=validation_dataset,class_weight=class_weights,callbacks=callbacks,verbose=2)# 微调策略(分阶段解冻更多层)# ======================# 初始训练后执行def unfreeze_layers(model, unfreeze_ratio=0.5):base_model = model.layers[2]num_layers = len(base_model.layers)unfreeze_from = int(num_layers * (1 - unfreeze_ratio))for layer in base_model.layers[:unfreeze_from]:layer.trainable = Falsefor layer in base_model.layers[unfreeze_from:]:layer.trainable = True# 微调阶段model.layers[2].trainable = Trueunfreeze_layers(model, unfreeze_ratio=0.5)  # 解冻后50%的层model.compile(optimizer=optimizers.AdamW(learning_rate=FINE_TUNE_LR, weight_decay=1e-5),loss=tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.02),  # 减少微调阶段的标签平滑metrics= ['accuracy',tf.keras.metrics.TopKCategoricalAccuracy(k=3, name='top3_acc'),tf.keras.metrics.TopKCategoricalAccuracy(k=5, name='top5_acc')])history_fine = model.fit(train_dataset,epochs=EPOCHS + 20,initial_epoch=history.epoch[-1],validation_data=validation_dataset,class_weight=class_weights,callbacks=callbacks,verbose=2)# 梯度裁剪(提升稳定性)tf.keras.backend.set_epsilon(1e-4)  # 防止梯度爆炸optimizer = model.optimizeroptimizer.clipnorm = 1.0  # 设置梯度裁剪# ======================# 扩展评估(新增测试集Top-K评估)# ======================print("扩展评估测试集...")test_loss, test_acc, test_top3, test_top5 = model.evaluate(test_dataset)print(f"测试结果: Acc={test_acc:.2%}, Top-3={test_top3:.2%}, Top-5={test_top5:.2%}")# 模型评估与保存# ======================print("评估测试集...")test_loss, test_acc, test_top3, test_top5 = model.evaluate(test_dataset)print(f"测试准确率: {test_acc:.2%}")print("保存模型...")# 保存为 SavedModel 格式tf.saved_model.save(model, SAVE_PATH)print(f"Model saved to {SAVE_PATH}")
模型转化为TFLite
import tensorflow as tf
from tensorflow.python.lib.io.file_io import create_dir_v2# 设置输入输出路径(根据实际路径调整)
saved_model_dir = r"C:\Users\lenove\Desktop\doms\src\main\resources\models\pestModel_MobileNetV2_V4"
output_tflite = "optimized_model.tflite"# 创建TFLite转换器
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)# 添加优化配置(关键步骤!)
converter.optimizations = [tf.lite.Optimize.DEFAULT]  # 启用默认优化(动态范围量化)
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]  # 确保TFLite兼容性# 执行转换
tflite_model = converter.convert()# 保存优化后的模型
with open(output_tflite, "wb") as f:f.write(tflite_model)print(f"转换成功!输出文件: {output_tflite}")
预测脚本
import warnings
warnings.filterwarnings('ignore')
import sys
import json
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'  # 禁用TensorFlow日志
os.environ['OMP_NUM_THREADS'] = '1'       # 优化OpenMP配置
import cv2
import numpy as np
import tensorflow as tf
from time import time# ---------------------- 全局配置 ----------------------
# 设置TensorFlow线程数 (根据CPU核心数调整)
tf.config.threading.set_intra_op_parallelism_threads(4)  # 单个操作内部并行线程
tf.config.threading.set_inter_op_parallelism_threads(2)   # 操作间并行线程# ---------------------- 常驻内存组件 ----------------------
# TFLite模型加载 (只需加载一次)
INTERPRETER = tf.lite.Interpreter(model_path=r'C:\Users\lenove\Desktop\doms\src\main\resources\models\optimized_model.tflite')
INTERPRETER.allocate_tensors()
INPUT_DETAILS = INTERPRETER.get_input_details()[0]
OUTPUT_DETAILS = INTERPRETER.get_output_details()[0]# 类别标签加载 (只需加载一次)
with open(r'C:\Users\lenove\Desktop\doms\src\main\resources\scripts\class_labels.json', 'r') as f:CLASS_LABELS = json.load(f)# ---------------------- 预处理优化 ----------------------
def preprocess_image_opencv(img_path, target_size=(224, 224)):"""OpenCV预处理提速约3倍"""img = cv2.imread(img_path)if img is None:raise ValueError(f"无法读取图片: {img_path}")# 单次转换替代PIL的多步操作img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)img = cv2.resize(img, target_size)# 归一化并直接生成批处理维度return np.expand_dims(img.astype(np.float32) / 255.0, axis=0)# ---------------------- 推理核心 ----------------------
def batch_predict(image_batch):"""批量推理优化"""# 设置输入张量INTERPRETER.resize_tensor_input(INPUT_DETAILS['index'], image_batch.shape)INTERPRETER.allocate_tensors()INTERPRETER.set_tensor(INPUT_DETAILS['index'], image_batch)INTERPRETER.invoke()return INTERPRETER.get_tensor(OUTPUT_DETAILS['index'])# ---------------------- 主逻辑 ----------------------
def main(img_paths):try:# 批量预处理batch_images = np.vstack([preprocess_image_opencv(p) for p in img_paths])# 执行推理start_time = time()predictions = batch_predict(batch_images)infer_time = time() - start_time# 解析结果results = []for i, probs in enumerate(predictions):top_3_indices = np.argsort(probs)[-3:][::-1]results.append({"image": img_paths[i],"predictions": [{"class": CLASS_LABELS[str(idx)],"confidence": float(probs[idx])} for idx in top_3_indices],"infer_time": f"{infer_time/len(img_paths):.3f}s per image"})print(json.dumps({"status": "success", "results": results}))except Exception as e:print(json.dumps({"status": "error", "message": str(e)}))if __name__ == "__main__":if len(sys.argv) < 2:print(json.dumps({"status": "error","message": "请传入图片路径,支持多图批量处理"}))sys.exit(1)# 支持多图批量推理main(sys.argv[1:])
PredictController预测控制器
package com.example.doms.controller;import com.example.doms.po.Pest;
import com.example.doms.resultDTO.PredictionResultDTO;
import com.example.doms.service.impl.PestServiceImpl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;@RestController
@RequestMapping("/predict")
public class PredictController {@Autowiredprivate PestServiceImpl pestService;@Async@PostMapping("/")public ResponseEntity<?> predict(@RequestParam("file") MultipartFile file) {System.out.println("Received file: " + file.getOriginalFilename());System.out.println("File size: " + file.getSize());if (file == null || file.isEmpty()) {return ResponseEntity.badRequest().body(Map.of("error", "文件为空或未正确上传"));}try {// 1. 保存临时文件Path tempDir = Files.createTempDirectory("pest-");File tempFile = new File(tempDir.toFile(), file.getOriginalFilename());file.transferTo(tempFile);// 2. 调用Python脚本ProcessBuilder pb = new ProcessBuilder("python","C:\\Users\\lenove\\Desktop\\doms\\src\\main\\resources\\scripts\\predict.py",tempFile.getAbsolutePath());Process process = pb.start();// 3. 读取Python输出String result = new String(process.getInputStream().readAllBytes());String error = new String(process.getErrorStream().readAllBytes());//            if (!error.isEmpty()) {//                System.err.println("Python错误输出: " + error);//            }System.out.println("Python原始输出: " + result); // 添加在解析前//解析后的predictions列表List<PredictionResultDTO> predictions = parseResult(result); // 解析JSON// 补充害虫信息predictions = predictions.stream().map(p -> {Pest pest = pestService.getPestByPestName(p.getClassName());if (pest != null) {p.setPestId(pest.getPestId());p.setDescription(pest.getDescription());p.setControlMeasures(pest.getControlMeasures());}return p;}).collect(Collectors.toList());if (predictions == null || predictions.isEmpty()) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", "预测结果解析失败"));}return ResponseEntity.ok().body(Map.of("predictions", predictions));} catch (IOException e) {return ResponseEntity.internalServerError().body(Map.of("error", "文件处理失败:" + e.getMessage()));}}private List<PredictionResultDTO> parseResult(String jsonResult) {ObjectMapper objectMapper = new ObjectMapper();try {Map<String, Object> resultMap = objectMapper.readValue(jsonResult, new TypeReference<Map<String, Object>>() {});// 检查状态是否为错误if ("error".equals(resultMap.get("status"))) {System.err.println("Python错误: " + resultMap.get("message"));return Collections.emptyList();}// 获取顶层results列表List<Map<String, Object>> results = (List<Map<String, Object>>) resultMap.get("results");if (results == null) {System.err.println("JSON结构错误: 缺少results字段");return Collections.emptyList();}List<PredictionResultDTO> allPredictions = new ArrayList<>();for (Map<String, Object> resultItem : results) {// 提取每个result项中的predictions列表List<Map<String, Object>> predictions = (List<Map<String, Object>>) resultItem.get("predictions");if (predictions == null || predictions.isEmpty()) {System.err.println("警告: 某条结果缺少predictions字段");continue;}// 转换每个预测项for (Map<String, Object> predMap : predictions) {try {String className = predMap.containsKey("class") ?(String) predMap.get("class") :"未知类别";Double confidence = predMap.containsKey("confidence") ?((Number) predMap.get("confidence")).doubleValue() :0.0;allPredictions.add(new PredictionResultDTO(className, confidence));} catch (ClassCastException e) {System.err.println("类型转换异常: " + predMap);}}}return allPredictions;} catch (IOException e) {System.err.println("JSON解析失败: " + e.getMessage());return Collections.emptyList();} catch (Exception e) {System.err.println("未知解析错误: " + e.getMessage());return Collections.emptyList();}}
}
害虫识别过程展示
  1. 害虫识别初始显示:通过点击左侧蓝色按钮“点击上传害虫图片”进行上传图片进行上传,右侧将展示识别三个识别结果(由模型识别准确率前三决定)

在这里插入图片描述

  1. 上传图片
    在这里插入图片描述
  2. 展示害虫识别结果。从左到右依次为模型认为准确率前三的害虫类别。
    在这里插入图片描述

害虫定位实现

害虫定位代码

基于高德API及相关害虫定位经纬度进行实现。相关高德API使用可在如下链接查看:高德API,代码如下:

<template><div style="display: flex;background-color: #b0ffca;box-shadow: 10px 10px 10px #6dffff, -10px -10px 10px #f9f9c6 !important;border-radius: 20px;overflow: hidden;"><div id="container"></div><div style="padding: 20px;"><div style="display: flex;flex-direction: column;"><span style="font-size: x-large;font-weight: 600;color: #08e908;">害虫分布</span><div style="margin-top: 10px;"><el-button type="success" @click="currentGeolocation" size="small"style="margin-left: 10px;">当前定位</el-button><el-button type="primary" @click="clearRoute" size="small"style="margin-left: 10px;">清除路线</el-button><el-button type="info" @click="showFilteredPestStorages" size="small" style="margin-left: 10px;">{{ user.role === '工作人员' ? '所属果园害虫分布' : '管理果园害虫分布' }}</el-button><el-button type="warning" @click="showAllPestStorages" size="small" style="margin-left: 10px;">所有果园害虫分布</el-button></div></div><el-table :data="paginatedPestStorages"style="width: 100%; border-radius: 20px; font-weight: 600;margin-top: 10px;"><el-table-column prop="chineseName" label="害虫名称" width="150px" /><el-table-column prop="latitude" label="纬度"><template #default="scope">{{ scope.row.latitude || '无记录' }}</template></el-table-column><el-table-column prop="longitude" label="经度"><template #default="scope">{{ scope.row.longitude || '无记录' }}</template></el-table-column><el-table-column label="操作" width="200px"><template #default="scope"><!-- 当坐标存在时显示操作按钮 --><div v-if="hasValidLocation(scope.row)"><el-button type="warning" size="small" @click="handleWalk(scope.row)">步行</el-button><el-button type="primary" size="small" @click="handleDrive(scope.row)">驾车</el-button><el-button type="success" size="small" @click="handleLocate(scope.row)">定位</el-button></div><!-- 坐标缺失时显示提示 --><span v-else style="color:#999">不可操作</span></template></el-table-column></el-table><!-- 分页控件 --><el-pagination v-if="totalPestStorages > 0" v-model:current-page="currentPestStoragesPage":page-size="pestStoragesPageSize" :total="totalPestStorages" layout="prev, pager, next"@current-change="handlePestStoragesPageChange" class="mt-4" backgroundstyle="display:flex; justify-content: center;" /></div></div>
</template><script>
import AMapLoader from '@amap/amap-jsapi-loader';
import { ElMessage } from 'element-plus';
import { onMounted, ref, getCurrentInstance, computed } from 'vue';export default {name: 'MapOrchard',setup() {const instance = getCurrentInstance();let map = ref(null);let AMapInstance = ref(null);let markers = ref([]);let drivingInstance = ref(null);let walkingInstance = ref(null);let geolocation = ref(null);const pestStorages = ref([]);// 从 sessionStorage 读取用户信息const user = JSON.parse(sessionStorage.getItem('user') || { userName: '', role: '' })// 害虫寄存分页状态管理const currentPestStoragesPage = ref(1);const pestStoragesPageSize = ref(2); // 每页显示数量const totalPestStorages = computed(() => pestStorages.value.length);// 计算当前害虫寄存页的数据const paginatedPestStorages = computed(() => {const start = (currentPestStoragesPage.value - 1) * pestStoragesPageSize.value;const end = start + pestStoragesPageSize.value;return pestStorages.value.slice(start, end);});const handlePestStoragesPageChange = (page) => {currentPestStoragesPage.value = page;}// 初始化地图const initMap = () => {window._AMapSecurityConfig = {securityJsCode: "",};AMapLoader.load({key: "",version: "2.0",plugins: ["AMap.Scale", "AMap.Geolocation", "AMap.ControlBar", "AMap.Driving", "AMap.Walking"],}).then((AMap) => {AMapInstance.value = AMap;map.value = new AMap.Map("container", {resizeEnable: true,viewMode: "2D",zoom: 15,zoomToAccuracy: true,center: [113.380696, 23.202551],});// 添加地图控件map.value.addControl(new AMap.ControlBar());map.value.addControl(new AMap.Scale());geolocation.value = new AMap.Geolocation({enableHighAccuracy: true,timeout: 10000,buttonOffset: new AMap.Pixel(10, 20),zoomToAccuracy: true});map.value.addControl(geolocation.value);// 开始定位geolocation.value.getCurrentPosition((status, result) => {if (status === 'complete') {ElMessage.success('当前定位成功');map.value.setCenter([result.position.lng, result.position.lat]);} else {ElMessage.error('当前定位失败');}});// 初始化驾车实例drivingInstance.value = new AMap.Driving({map: map.value,});//初始化步行规划walkingInstance.value = new AMap.Walking({map: map.value});// 加载害虫位置loadPestLocations();}).catch(console.error);};// 加载害虫位置const loadPestLocations = async () => {try {const response = await instance.proxy.$request.get('/pestStorage/getAll');const rawResponse = response._rawResponse;if (rawResponse.status === 200) {pestStorages.value = response.pestStorages.map(item => ({...item,latitude: item.latitude || null,longitude: item.longitude || null}));response.pestStorages.forEach(pestStorage => {if (pestStorage.latitude && pestStorage.longitude) {addMarker([pestStorage.longitude, pestStorage.latitude], pestStorage);}});}} catch (error) {ElMessage.error('加载害虫位置失败:' + (error.message || '未知错误'));}};// 更新害虫位置并动态更新标记const updatePestLocations = async (pestStoragesData) => {try {clearMarkers(); // 清除现有标记pestStorages.value = pestStoragesData; // 更新害虫数据pestStoragesData.forEach(pestStorage => {if (pestStorage.latitude && pestStorage.longitude) {addMarker([pestStorage.longitude, pestStorage.latitude], pestStorage);}});} catch (error) {ElMessage.error('更新害虫位置失败:' + (error.message || '未知错误'));}};const showFilteredPestStorages = async () => {try {let response;if (user.role === '工作人员') {// 工作人员:获取所属果园的害虫分布response = await instance.proxy.$request.get(`/pestStorage/getByStaff/${user.userId}`);} else {// 果园管理者:获取所有管理果园的害虫分布response = await instance.proxy.$request.get(`/pestStorage/getByManager/${user.userId}`);}const rawResponse = response._rawResponse;if (rawResponse.status === 200) {await updatePestLocations(response.pestStorages); // 动态更新数据ElMessage.success('加载成功');}} catch (error) {ElMessage.error('加载失败:' + (error.message || '未知错误'));}};const showAllPestStorages = async () => {try {// 加载所有果园的害虫分布const response = await instance.proxy.$request.get('/pestStorage/getAll');const rawResponse = response._rawResponse;if (rawResponse.status === 200) {await updatePestLocations(response.pestStorages); // 动态更新数据ElMessage.success('加载成功');}} catch (error) {ElMessage.error('加载失败:' + (error.message || '未知错误'));}};// 添加标记const addMarker = (lngLat, pestStorage) => {if (!map.value) return;const marker = new AMapInstance.value.Marker({position: lngLat,icon: new AMapInstance.value.Icon({size: new AMapInstance.value.Size(19, 31),imageSize: new AMapInstance.value.Size(19, 31),image: "https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png",imageOffset: new AMapInstance.value.Pixel(0, 0)}),offset: new AMapInstance.value.Pixel(-9, -31)});marker.on('click', () => {ElMessage.info(`害虫类型:${pestStorage.chineseName},发现时间:${formatDate(pestStorage.discoveryTime)}`);});map.value.add(marker);markers.value.push(marker);};// 清除地图上的所有标记const clearMarkers = () => {if (map.value && markers.value.length > 0) {markers.value.forEach(marker => {map.value.remove(marker);});markers.value = []; // 清空标记数组}};// 定位到标记const handleLocate = (pestStorage) => {if (!map.value) {console.error("地图实例未初始化");return;}const lngLat = [pestStorage.longitude, pestStorage.latitude];map.value.setCenter(lngLat);};// 驾车路线规划const drivingRoute = (start, end) => {if (!drivingInstance.value) return;drivingInstance.value.clear();drivingInstance.value.search(start, end, (status, result) => {if (status === 'complete') {ElMessage.success('驾车路线规划成功');console.log('驾车路线规划成功', result);} else {ElMessage.error('驾车路线规划失败');console.error('驾车路线规划失败', result);}});};// 步行路线规划const walkingRoute = (start, end) => {if (!walkingInstance.value) return;walkingInstance.value.clear();walkingInstance.value.search(start, end, (status, result) => {if (status === 'complete') {ElMessage.success('步行路线规划成功');console.log('步行路线规划成功', result);} else {ElMessage.error('步行路线规划失败');console.error('步行路线规划失败', result);}});};// 处理驾车路线规划const handleDrive = async (pestStorage) => {try {const startLngLat = await currentGeolocation();const endLngLat = [pestStorage.longitude, pestStorage.latitude];drivingRoute(startLngLat, endLngLat);} catch (error) {ElMessage.error('获取当前位置失败,使用默认起点');const startLngLat = [113.380696, 23.202551]; // 默认起点const endLngLat = [pestStorage.longitude, pestStorage.latitude];drivingRoute(startLngLat, endLngLat);}};// 处理步行路线规划const handleWalk = async (pestStorage) => {try {const startLngLat = await currentGeolocation();const endLngLat = [pestStorage.longitude, pestStorage.latitude];walkingRoute(startLngLat, endLngLat);} catch (error) {ElMessage.error('获取当前位置失败,使用默认起点');const startLngLat = [113.380696, 23.202551]; // 默认起点const endLngLat = [pestStorage.longitude, pestStorage.latitude];walkingRoute(startLngLat, endLngLat);}};// 清除步行或者驾车路线const clearRoute = async () => {if (drivingInstance.value) {await drivingInstance.value.clear();currentGeolocation();}if (walkingInstance.value) {await walkingInstance.value.clear();currentGeolocation();}};//currentGeolocation 当前定位// 获取当前定位const currentGeolocation = () => {return new Promise((resolve, reject) => {if (!geolocation.value) {reject(new Error('定位功能未初始化'));return;}geolocation.value.getCurrentPosition((status, result) => {if (status === 'complete') {resolve([result.position.lng, result.position.lat]);} else {reject(new Error('获取定位失败'));}});}).catch(() => {// 定位失败时使用默认值return [113.380696, 23.202551]; // 默认经纬度});};// 检查坐标有效性const hasValidLocation = (row) => {return row.latitude && row.longitude &&!isNaN(row.latitude) &&!isNaN(row.longitude)}// 格式化时间的方法const formatDate = (time) => {if (!time) return '';const date = new Date(time);const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, '0');const day = String(date.getDate()).padStart(2, '0');const hours = String(date.getHours()).padStart(2, '0');const minutes = String(date.getMinutes()).padStart(2, '0');const seconds = String(date.getSeconds()).padStart(2, '0');return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;};onMounted(async () => {await initMap();await loadPestLocations();});return {user,currentPestStoragesPage,pestStoragesPageSize,totalPestStorages,paginatedPestStorages,handlePestStoragesPageChange,showFilteredPestStorages,showAllPestStorages,pestStorages,handleLocate,handleDrive,handleWalk,clearRoute,currentGeolocation,hasValidLocation,loadPestLocations,};}
};
</script><style scoped>
#container {width: 100%;height: 100%;border-radius: 20px;
}
</style>
害虫定位过程展示
  1. 一开始地图将展示当前定位位置,右侧害虫分布将展示系统内数据库所存在的所有害虫上报的位置信息(包括有经纬度和无经纬度信息的)
    在这里插入图片描述
  2. 点击对应害虫的步行/驾车按钮效果如下图所示:
    在这里插入图片描述
    在这里插入图片描述
  3. 点击对应害虫的“定位”按钮,将会把地图中心移至害虫标记所在位置,如图所示:
    在这里插入图片描述
  4. 点击“清除路线”按钮,将清除地图上所有的路线规划,并且地图中心将回到当前定位位置。
  5. “所属果园害虫分布”按钮是为系统角色“工作人员”专设,将为“工作人员”展示所属果园的害虫分布,方便“工作人员”进行定位处理,点击“所有果园害虫分布”按钮将展示系统内所保存的所有果园的害虫分布信息。

专家咨询功能

在线咨询聊天室

用户可以对所有在线的专家发起咨询请求,并进行聊天室的咨询对话。如图所示:

  1. 选择对应专家进行“立即咨询”操作

在这里插入图片描述

  1. 专家接受咨询

在这里插入图片描述

  1. 用户与专家聊天室进行咨询

在这里插入图片描述

主要前端代码如下
  • 工作人员端
<template><el-row :gutter="20" style="margin-top: 20px;"><!-- 左侧在线专家列表 --><el-col :span="12"><el-card class="list-card"><template #header><span style="font-size:large;font-weight: 600;color:#08e908;">专家列表</span></template><el-table :data="paginatedExperts" style="border-radius: 20px;"><el-table-column prop="expertId" label="专家ID" width="80" /><el-table-column prop="userName" label="用户名" width="80" /><el-table-column prop="expertName" label="专家姓名" /><el-table-column prop="expertise" label="专家详情"><template #default="{ row }"><el-button @click="showModal(row)" type="primary" size="small">查看</el-button></template></el-table-column><el-table-column label="状态" width="100"><template #default="{ row }"><el-tag v-if="users.some(user => user.userName === row.userName)" type="success">在线</el-tag><el-tag v-else type="info">离线</el-tag></template></el-table-column><el-table-column label="操作" width="120"><template #default="{ row }"><!-- 继续咨询按钮 --><el-button v-if="activeConsultations[row.userName]" @click="handleContinueConsult(row)"type="warning" size="small" style="margin-left: 5px">继续咨询<el-badge v-if="unreadCount[row.userName]" :value="unreadCount[row.userName]":offset="[21, 0]" :max="10"></el-badge></el-button><el-button v-else-if="users.some(user => user.userName === row.userName)"@click="handleConsult(row)" type="primary" size="small">立即咨询</el-button><el-button v-else type="info" size="small" @click="handleAppointment(row)">预约咨询</el-button></template></el-table-column></el-table><el-pagination v-if="totalExperts > 0" v-model:current-page="currentExpertPage":page-size="expertPageSize" :total="totalExperts" layout="prev, pager, next"@current-change="handleExpertPageChange" class="mt-4" backgroundstyle="display:flex; justify-content: center; margin-top: 18px;" /></el-card><!-- 新增:预约咨询模态框 --><el-dialog title="新建预约咨询" v-model="appointmentModalVisible" width="600px" :append-to-body="true"><el-form :model="appointmentForm" :rules="rules" ref="formRef" label-width="100px"><el-form-item label="用户名" prop="userName"><el-input v-model="appointmentForm.userName" disabled /></el-form-item><!-- 果园信息 --><el-form-item label="所属果园" prop="orchardName"><el-input v-model="appointmentForm.orchardName" disabled /></el-form-item><!-- 专家信息 --><el-form-item label="咨询专家" prop="expertId"><el-select v-model="appointmentForm.expertId" placeholder="选择专家" @change="handleExpertChange"><el-option v-for="expert in experts" :key="expert.expertId" :label="expert.expertName":value="expert.expertId" /></el-select></el-form-item><!-- 预约时间 --><el-form-item label="预约时间" prop="appointmentTime"><el-date-picker v-model="appointmentForm.appointmentTime" type="datetime":disabled-date="disabledPastDates" /></el-form-item><!-- 联系方式 --><el-form-item label="手机号码" prop="contactMethod"><el-input v-model="appointmentForm.contactMethod" placeholder="请输入手机号" maxlength="11" /></el-form-item><!-- 咨询内容 --><el-form-item label="咨询内容" prop="appointmentContent"><el-input v-model="appointmentForm.appointmentContent" type="textarea" :rows="4"placeholder="请详细描述问题(如病虫害症状、果园面积等)" /></el-form-item></el-form><template #footer><el-button @click="appointmentModalVisible = false">取消</el-button><el-button type="primary" @click="submitAppointment">提交预约</el-button></template></el-dialog><!-- 新增:预约记录和反馈表格(放在专家列表下方) --><el-card class="list-card" style="margin-top: 20px"><template #header><span style="font-size:large;font-weight: 600;color:#08e908;">预约咨询</span></template><el-table :data="paginatedAppointments" style="width: 100%;border-radius: 20px;" stripe border><el-table-column prop="expertName" label="专家姓名" width="120" /><el-table-column prop="appointmentTime" label="预约时间" width="180"><template #default="{ row }">{{ formatDate(row.appointmentTime) }}</template></el-table-column><el-table-column prop="appointmentContent" label="咨询内容"><template #default="{ row }"><el-button @click="showAppointmentContent(row)" type="primary" size="small">查看</el-button></template></el-table-column><el-table-column prop="status" label="状态" width="100"><template #default="{ row }"><el-tag :type="statusTagType(row.status)">{{ row.status }}</el-tag></template></el-table-column></el-table><el-pagination v-if="totalAppointments > 0" v-model:current-page="currentAppointmentPage":page-size="appointmentPageSize" :total="totalAppointments" layout="prev, pager, next"@current-change="handleAppointmentPageChange" class="mt-4" backgroundstyle="display:flex; justify-content: center; margin-top: 18px;" /><el-dialog v-model="appointmentContentDialog" title="咨询内容" width="50%"><el-descriptions :column="1" border><el-descriptions-item label="果园名称">{{ currentAppointment.orchardName }}</el-descriptions-item><el-descriptions-item label="联系方式">{{ currentAppointment.contactMethod }}</el-descriptions-item><el-descriptions-item label="咨询问题">{{currentAppointment.appointmentContent }}</el-descriptions-item></el-descriptions></el-dialog></el-card></el-col><!-- 右侧聊天框 --><el-col :span="12"><el-card style="height: inherit;" class="chat-card"><div v-if="chatUser !== ''"><div class="chat-container"><div style="text-align: center; line-height: 50px;">聊天室({{ chatUser }}</div><div style="height: 350px; overflow:auto; border-top: 1px solid #ccc" v-html="content"></div><div style="height: 250px"><textarea v-model="text" style="height: 160px; width: 100%; padding: 20px; border: none; border-top: 1px solid #ccc;border-bottom: 1px solid #ccc; outline: none" @keydown.enter="handleEnter"></textarea><div style="text-align: right; padding-right: 10px"><el-button @click="handleEndConsult" type="danger" size="small">结束咨询</el-button><el-button type="primary" size="small" @click="send">发送</el-button></div></div></div></div><div v-else style="background-color: #b0ffca;"><el-card class="feedback-card"><template #header><span style="font-size:large;font-weight: 600;color:#08e908;">咨询反馈</span></template><!-- 未反馈记录 --><div class="feedback-section" v-if="pendingFeedbacks.length > 0"style="display: flex;flex-direction: column;align-items: center;"><divstyle="background-color: #ccf6f6;width:100%;border-radius: 20px;padding: 0px 10px;color: #65f578;margin-bottom: 10px;height: 60px;"><h4>待反馈记录</h4></div><el-table :data="paginatedPending" style="width: 100%;border-radius: 20px;"><el-table-column prop="consultationType" label="咨询类型" width="80" /><el-table-column prop="consultationTime" label="咨询时间" width="180"><template #default="{ row }">{{ formatDate(row.consultationTime) }}</template></el-table-column><el-table-column prop="expertName" label="专家" width="100" /><el-table-column label="操作"><template #default="{ row }"><el-button type="success" size="default"@click="openFeedbackDialog(row)">填写反馈</el-button></template></el-table-column></el-table><el-pagination v-if="totalPending > 0" v-model:current-page="currentPendingPage":page-size="pendingPageSize" :total="totalPending" layout="prev, pager, next"@current-change="handlePendingPageChange" backgroundstyle="margin-top: 15px; justify-content: center" /></div><!-- 已反馈记录 --><div class="feedback-section" v-if="completedFeedbacks.length > 0"style="display: flex;flex-direction: column;align-items: center;"><divstyle="margin-top: 10px;background-color: #ccf6f6;width:100%;border-radius: 20px;padding: 0px 10px;color: #65f578;margin-bottom: 10px;height: 60px;"><h4>历史反馈</h4></div><el-table :data="paginatedCompleted" style="width: 100%;border-radius: 20px;"><el-table-column prop="consultationType" label="咨询类型" width="80" /><el-table-column prop="consultationTime" label="咨询时间" width="180"><template #default="{ row }">{{ formatDate(row.consultationTime) }}</template></el-table-column><el-table-column prop="expertName" label="专家" width="80" /><el-table-column prop="rating" label="评分"><template #default="{ row }"><el-rate v-model="row.rating" disabled:colors="['#99A9BF', '#F7BA2A', '#FF9900']" /></template></el-table-column><el-table-column label="反馈内容"><template #default="{ row }"><el-button type="primary" size="default"@click="viewFeedbackDetail(row)">查看详情</el-button></template></el-table-column></el-table><el-pagination v-if="totalCompleted > 0" v-model:current-page="currentCompletedPage":page-size="completedPageSize" :total="totalCompleted" layout="prev, pager, next"@current-change="handleCompletedPageChange" backgroundstyle="margin-top: 15px; justify-content: center" /></div><!-- 无记录提示 --><el-empty v-if="!pendingFeedbacks.length && !completedFeedbacks.length" description="暂无反馈记录" /></el-card><!-- 反馈模态框 --><el-dialog title="填写反馈" v-model="feedbackDialogVisible" width="600px"><el-form :model="feedbackForm" :rules="feedbackRules" ref="feedbackFormRef"><el-form-item label="咨询专家" prop="expertName"><el-input v-model="feedbackForm.expertName" disabled /></el-form-item><el-form-item label="咨询时间" prop="consultationTime"><el-input :model-value="formatDate(feedbackForm.consultationTime)" disabled /></el-form-item><el-form-item label="服务评分" prop="rating"><el-rate v-model="feedbackForm.rating" :colors="['#99A9BF', '#F7BA2A', '#FF9900']":texts="['非常差', '差劲', '一般', '良好', '优秀']" show-text /></el-form-item><el-form-item label="反馈内容" prop="feedbackText"><el-input v-model="feedbackForm.feedbackText" type="textarea" :rows="4"placeholder="请输入您的反馈意见" maxlength="500" show-word-limit /></el-form-item></el-form><template #footer><el-button @click="feedbackDialogVisible = false">取消</el-button><el-button type="primary" @click="submitFeedback" :loading="submitting">提交反馈</el-button></template></el-dialog><!-- 反馈详情模态框 --><el-dialog title="反馈详情" v-model="detailDialogVisible" width="500px"><el-descriptions :column="1" border><el-descriptions-item label="专家姓名">{{ currentFeedback.expertName }}</el-descriptions-item><el-descriptions-item label="咨询时间">{{ formatDate(currentFeedback.consultationTime) }}</el-descriptions-item><el-descriptions-item label="服务评分"><el-rate v-model="currentFeedback.rating" disabled:colors="['#99A9BF', '#F7BA2A', '#FF9900']" /></el-descriptions-item><el-descriptions-item label="反馈内容">{{ currentFeedback.feedbackText }}</el-descriptions-item></el-descriptions></el-dialog></div></el-card></el-col><el-dialog title="专家详情" v-model="dialogVisible" width="50%" @close="dialogVisible = false" top="2%":append-to-body="true"><el-descriptions :column="1" border v-if="selectedExpert"><el-descriptions-item label="姓名">{{ selectedExpert.expertName }}</el-descriptions-item><el-descriptions-item label="性别">{{ selectedExpert.gender }}</el-descriptions-item><el-descriptions-item label="专业领域">{{ selectedExpert.expertise }}</el-descriptions-item><el-descriptions-item label="资质证书"><el-image style="width: 200px; height: 150px" :src="selectedExpert.certificate":preview-src-list="[selectedExpert.certificate]" fit="cover" /></el-descriptions-item><el-descriptions-item label="个人简介">{{ selectedExpert.bio }}</el-descriptions-item><el-descriptions-item label="综合评分"><el-rate v-model="selectedExpert.rating" disabled :colors="['#99A9BF', '#F7BA2A', '#FF9900']":max="5" /></el-descriptions-item></el-descriptions></el-dialog></el-row>
</template><script>
import { ref, reactive, onMounted, onUnmounted, getCurrentInstance, nextTick, computed } from 'vue';
import { ElMessage } from 'element-plus';
export default {name: 'StaffConsultationMainTop',
}
</script>
<script setup>
let socket = ref(null)
const instance = getCurrentInstance();
const user = reactive(JSON.parse(sessionStorage.getItem("user")) || {})
const userId = computed(() => user.userId);
const userName = computed(() => user.userName);
const users = ref([])
const experts = ref([]);const avatarNowUserUrl = new URL('@/assets/avatar/avatar_1.png', import.meta.url).href;
const avatarRemoteUserUrl = new URL('@/assets/avatar/avatar_2.png', import.meta.url).href;const chatUser = ref('')// 跟踪当前聊天专家
const text = ref('')
const content = ref('')// 新增响应式变量
const dialogVisible = ref(false);
const selectedExpert = ref(null);// 新增状态
const chatHistory = reactive({});
//添加未读计数
const unreadCount = reactive({});const activeConsultations = reactive({});const consultationIds = reactive({}); // 存储专家与咨询ID的映射 {userName: consultationId}// 专家分页状态管理
const currentExpertPage = ref(1);
const expertPageSize = ref(4); // 每页显示数量
const totalExperts = computed(() => experts.value.length);// 计算当前专家页的数据
const paginatedExperts = computed(() => {const start = (currentExpertPage.value - 1) * expertPageSize.value;const end = start + expertPageSize.value;return experts.value.slice(start, end);
});const handleExpertPageChange = (page) => {currentExpertPage.value = page;
}
// 新增显示模态框方法
const showModal = (expert) => {selectedExpert.value = expert;dialogVisible.value = true;
};onMounted(() => {const savedIds = sessionStorage.getItem('consultationIds');const savedConsultations = sessionStorage.getItem('activeConsultations');const savedHistory = sessionStorage.getItem('chatHistory');if (savedIds) {Object.assign(consultationIds, JSON.parse(savedIds));}if (savedConsultations) {Object.assign(activeConsultations, JSON.parse(savedConsultations));}if (savedHistory) {Object.assign(chatHistory, JSON.parse(savedHistory));}loadExperts();init();loadAppointments();loadFeedbackRecords(); // 需要添加await确保加载完成});const loadExperts = async () => {try {const res = await instance.proxy.$request.get('/expert/');experts.value = res;} catch (err) {ElMessage.error('加载专家列表失败!');}
};const handleConsult = async (row) => {// 设置咨询状态activeConsultations[row.userName] = true;// 切换专家时重置未读unreadCount[row.userName] = 0;chatUser.value = row.userName;// 初始化聊天记录if (!chatHistory[row.userName]) {chatHistory[row.userName] = [];}content.value = chatHistory[row.userName].join('');//创建新的线上咨询记录保存至数据库try {const consultationTime = new Date().toISOString()const res = await instance.proxy.$request.post('/consultation/create', {userId: userId.value,expertId: row.expertId,expertName: row.expertName,status: '未处理',consultationTime, // 使用 ISO 8601 格式的时间});await instance.proxy.$request.post('/feedback/create', {consultationType: '在线',consultationId: res.consultationId,consultationTime,userId: userId.value,userName: userName.value,expertId: row.expertId,expertName: row.expertName,feedbackStatus: '未反馈',})loadFeedbackRecords();// 存储consultationId(关键修改)consultationIds[row.userName] = res.consultationId;sessionStorage.setItem('consultationIds', JSON.stringify(consultationIds));} catch (error) {ElMessage.error('保存线上咨询记录失败!')}// 发送咨询请求const message = {type: 'consult_request',from: user.userName,to: row.userName,text: `${user.userName}发起了咨询请求`,consultationId: consultationIds[row.userName]};const html = createContent('system', null, null, message.text); // 传递消息类型chatHistory[row.userName].push(html);content.value = chatHistory[row.userName].join('');if (socket.value) {socket.value.send(JSON.stringify(message));}// 滚动到底部nextTick(() => {const container = document.querySelector('.chat-container div[style*="overflow"]');if (container) container.scrollTop = container.scrollHeight;});text.value = ''
};// 继续咨询处理
const handleContinueConsult = (row) => {//新增咨询ID验证if (!consultationIds[row.userName]) {ElMessage.warning('未找到有效的咨询记录');}// 设置咨询状态activeConsultations[row.userName] = true;unreadCount[row.userName] = 0;chatUser.value = row.userName;content.value = chatHistory[row.userName]?.join('') || '';// 滚动到底部nextTick(() => {const container = document.querySelector('.chat-container div[style*="overflow"]');if (container) container.scrollTop = container.scrollHeight;});text.value = ''
};// 结束咨询处理
const handleEndConsult = async () => {if (!chatUser.value)console.log("当前聊天专家不存在chatUser.value", chatUser);else {try {// 获取当前咨询ID(关键新增)const consultationId = consultationIds[chatUser.value];if (!consultationId) {ElMessage.error('未找到咨询记录ID');}// 发送结束请求到后端(新增接口调用)await instance.proxy.$request.put(`/consultation/end/${consultationId}`, {status: '已完成',endTime: new Date().toISOString()});// 发送结束通知const message = {type: 'consult_end',from: user.userName,to: chatUser.value,text: `${user.userName}结束了本次咨询`,consultationId: consultationId // 新增携带咨询ID(后续在后台显示)};socket.value.send(JSON.stringify(message));// 清除本地状态(新增清理consultationIds)delete consultationIds[chatUser.value];sessionStorage.setItem('consultationIds', JSON.stringify(consultationIds));// 清除状态delete activeConsultations[chatUser.value];chatUser.value = '';content.value = '';ElMessage.success('咨询已结束');} catch (err) {console.log(err);ElMessage.error('结束咨询失败');}}
};const send = () => {if (!chatUser.value || !text.value.trim()) {ElMessage.warning("请选择专家并输入内容");return;}const message = {from: user.userName,to: chatUser.value,text: text.value};// 发送消息socket.value.send(JSON.stringify(message));// 保存到本地记录const html = createContent(null, null, user.userName, text.value);chatHistory[chatUser.value].push(html);content.value = chatHistory[chatUser.value].join('');text.value = '';// 滚动到底部nextTick(() => {const container = document.querySelector('.chat-container div[style*="overflow"]');if (container) container.scrollTop = container.scrollHeight;});
};const createContent = (messageType, remoteUser, nowUser, text) => {let html = '';if (messageType === 'system') { // 新增系统消息类型html = `<div class="el-row" style="padding: 5px 0; margin: 2px; background-color: #f0f4ff; border: 4px solid #409eff; border-radius: 10px;"><div class="el-col el-col-22" style="text-align: center; font-weight: 700;"><div class="tip system">系统提示:${text}</div></div></div>`;}else if (nowUser) {html = `<div class="el-row" style="padding: 5px 0"><div class="el-col el-col-22" style="text-align: right; padding-right: 10px"><div class="tip left">${text}</div></div><div class="el-col el-col-2"><span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;"><img src="${avatarNowUserUrl}" style="object-fit: cover;"></span></div></div>`} else if (remoteUser) {html = `<div class="el-row" style="padding: 5px 0"><div class="el-col el-col-2" style="text-align: right"><span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;"><img src="${avatarRemoteUserUrl}" style="object-fit: cover;"></span></div><div class="el-col el-col-22" style="text-align: left; padding-left: 10px"><div class="tip right">${text}</div></div></div>`}return html;
}const init = () => {const userName = user.userNameif (typeof (WebSocket) == "undefined") {console.log("您的浏览器不支持WebSocket")return}const socketUrl = `ws://localhost:8080/server/${userName}`if (socket.value) {socket.value.close()socket.value = null}socket.value = new WebSocket(socketUrl)socket.value.onopen = () => {console.log("websocket已打开")}socket.value.onmessage = (msg) => {const data = JSON.parse(msg.data)if (data.users) {users.value = data.users.filter(u => u.userName !== userName)}//处理对方发起咨询请求else if (data.type === 'consult_request') {const fromUser = data.from;// 初始化记录if (!chatHistory[fromUser]) chatHistory[fromUser] = [];// 生成消息内容const html = createContent('system', null, null, data.text);chatHistory[fromUser].push(html);content.value = chatHistory[fromUser].join('');ElMessage.warning(`${data.text}`);}// 处理结束咨询消息else if (data.type === 'consult_end') {const fromUser = data.from;if (chatUser.value === fromUser) {chatUser.value = '';content.value = '';}activeConsultations[fromUser] = false;ElMessage.warning(`${fromUser}结束了咨询`);}else {const fromUser = data.from;// 初始化记录if (!chatHistory[fromUser]) chatHistory[fromUser] = [];// 生成消息内容const html = createContent(null, fromUser, null, data.text);chatHistory[fromUser].push(html);// 更新显示if (chatUser.value === fromUser) {content.value = chatHistory[fromUser].join('');} else {// 更新未读计数unreadCount[fromUser] = (unreadCount[fromUser] || 0) + 1;}}}socket.value.onclose = () => {console.log("websocket已关闭")}socket.value.onerror = () => {console.log("websocket发生了错误")}
}// 统一的关闭WebSocket方法
const closeSocket = () => {if (socket.value) {// 获取所有活跃咨询的专家用户名列表const activeExperts = Object.keys(activeConsultations).filter(userName => activeConsultations[userName]);// 发送最后一条关闭通知const message = {type: 'force_close',from: user.userName,to: activeExperts,text: `用户${user.userName}已离开页面`};socket.value.send(JSON.stringify(message));socket.value.close()  // 主动调用WebSocket的close方法socket.value = nullconsole.log('WebSocket连接已主动关闭')}
}// 组件卸载时
onUnmounted(async () => {sessionStorage.setItem('activeConsultations', JSON.stringify(activeConsultations));sessionStorage.setItem('chatHistory', JSON.stringify(chatHistory));// await handleEndConsult();closeSocket();
})const handleEnter = (event) => {event.preventDefault(); // 阻止默认换行行为send();
};// 格式化时间的方法
const formatDate = (time) => {if (!time) return '';const date = new Date(time);const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, '0');const day = String(date.getDate()).padStart(2, '0');const hours = String(date.getHours()).padStart(2, '0');const minutes = String(date.getMinutes()).padStart(2, '0');const seconds = String(date.getSeconds()).padStart(2, '0');return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};// 预约模态框状态
const appointmentModalVisible = ref(false);
const selectedExpertForAppointment = ref(null);// 禁止选择过去的时间
const disabledPastDates = (date) => {return date < Date.now() - 86400000; // 86400000ms = 1天
};// 预约表单数据
const appointmentForm = reactive({expertId: '',expertName: '',orchardId: '',orchardName: '',appointmentTime: '',contactMethod: '',appointmentContent: '',
});// 表单验证规则
const appointmentRules = {orchardId: [{ required: true, message: '请选择果园', trigger: 'change' }],expertId: [{ required: true, message: '请选择专家', trigger: 'change' }],appointmentTime: [{ required: true, message: '请选择预约时间', trigger: 'change' }],contactMethod: [{ required: true, message: '请输入手机号', trigger: 'blur' },{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式错误', trigger: 'blur' }],appointmentContent: [{ required: true, message: '请输入咨询内容', trigger: 'blur' },{ min: 10, message: '至少输入10个字符', trigger: 'blur' }]
};// 预约记录数据
const appointments = ref([]);
const currentAppointmentPage = ref(1);
const appointmentPageSize = ref(3);const totalAppointments = computed(() => appointments.value.length);// 计算属性 ------------------------------------------------------------
const paginatedAppointments = computed(() => {const start = (currentAppointmentPage.value - 1) * appointmentPageSize.value;const end = start + appointmentPageSize.value;return appointments.value.slice(start, end);
});const handleAppointmentPageChange = (page) => {currentAppointmentPage.value = page;
}const fetchOrchard = async () => {try {const response = await instance.proxy.$request.get(`/orchard/getOrchardByStaff/${userId.value}`)appointmentForm.orchardId = response.orchard.orchardId;appointmentForm.orchardName = response.orchard.orchardName;} catch (error) {ElMessage.error('获取果园数据失败')}
}// 方法 ---------------------------------------------------------------
// 打开预约模态框(修改原有预约按钮)
const handleAppointment = async (row) => {await fetchOrchard();selectedExpertForAppointment.value = row;appointmentForm.expertId = row.expertId;appointmentForm.expertName = row.expertName;appointmentForm.userName = userName.value;appointmentModalVisible.value = true;
};// 提交预约
const submitAppointment = async () => {try {const createTime = new Date().toISOString()// 调用创建预约接口const res = await instance.proxy.$request.post('/appointment/create', {...appointmentForm,userId: userId.value,createTime,status: '待确认' // 初始状态});await instance.proxy.$request.post('/feedback/create', {consultationType: '预约',consultationId: res.appointmentId,consultationTime: createTime,userId: userId.value,userName: userName.value,expertId: appointmentForm.expertId,expertName: appointmentForm.expertName,feedbackStatus: '未反馈',})// 刷新预约列表loadAppointments();loadFeedbackRecords();ElMessage.success('预约提交成功');appointmentModalVisible.value = false;} catch (err) {ElMessage.error('预约提交失败: ' + err.message);}
};// 专家选择事件
const handleExpertChange = (id) => {const expert = appointmentForm.experts.find(e => e.expertId === id);appointmentForm.expertName = expert?.expertName || '';
};// 加载预约记录
const loadAppointments = async () => {try {const res = await instance.proxy.$request.get(`/appointment/${userId.value}`)appointments.value = res.appointments;} catch (err) {ElMessage.error('加载预约记录失败');}
};// 状态标签样式
const statusTagType = (status) => {const map = {'待确认': 'warning','已确认': 'success','已取消': 'danger','已完成': ''};return map[status] || '';
};const appointmentContentDialog = ref(false)
const currentAppointment = ref(null)const showAppointmentContent = (appointment) => {appointmentContentDialog.value = truecurrentAppointment.value = appointment;
}const feedbackList = ref([])
const feedbackDialogVisible = ref(false)
const detailDialogVisible = ref(false)
const submitting = ref(false)
const currentFeedback = reactive({expertName: '',consultationTime: '',rating: 0,feedbackText: ''
})const feedbackForm = reactive({consultationId: '',expertName: '',consultationTime: '',rating: 0,feedbackText: ''
})// 待反馈分页
const currentPendingPage = ref(1)
const pendingPageSize = ref(2)
const totalPending = computed(() => pendingFeedbacks.value.length)// 已反馈分页
const currentCompletedPage = ref(1)
const completedPageSize = ref(2)
const totalCompleted = computed(() => completedFeedbacks.value.length)// 分页数据计算
const paginatedPending = computed(() => {const start = (currentPendingPage.value - 1) * pendingPageSize.valueconst end = start + pendingPageSize.valuereturn pendingFeedbacks.value.slice(start, end)
})const paginatedCompleted = computed(() => {const start = (currentCompletedPage.value - 1) * completedPageSize.valueconst end = start + completedPageSize.valuereturn completedFeedbacks.value.slice(start, end)
})// 分页变更处理
const handlePendingPageChange = (page) => {currentPendingPage.value = page
}const handleCompletedPageChange = (page) => {currentCompletedPage.value = page
}const feedbackRules = {rating: [{ required: true, message: '请选择评分', trigger: 'change' }],feedbackText: [{ required: true, message: '请输入反馈内容', trigger: 'blur' },{ min: 10, message: '至少输入10个字符', trigger: 'blur' }]
}// 计算属性
const pendingFeedbacks = computed(() => {return feedbackList.value.filter(f => f.feedbackStatus === '未反馈')
})const completedFeedbacks = computed(() => {return feedbackList.value.filter(f => f.feedbackStatus === '已反馈')
})// 方法
const loadFeedbackRecords = async () => {try {const res = await instance.proxy.$request.get(`/feedback/${userId.value}`)feedbackList.value = res.feedbacks} catch (error) {ElMessage.error('加载反馈记录失败')}
}const openFeedbackDialog = (record) => {Object.assign(feedbackForm, {feedbackId: record.feedbackId,consultationId: record.consultationId,expertName: record.expertName,consultationTime: record.consultationTime,rating: 0,feedbackText: ''})feedbackDialogVisible.value = true
}const submitFeedback = async () => {submitting.value = truetry {await instance.proxy.$request.post(`/feedback/updateFeedback/${feedbackForm.feedbackId}`, {...feedbackForm,feedbackStatus: '已反馈',feedbackTime: new Date().toISOString(),})ElMessage.success('反馈提交成功')feedbackDialogVisible.value = falseawait loadFeedbackRecords()} catch (error) {ElMessage.error('反馈提交失败')} finally {submitting.value = false}
}const viewFeedbackDetail = (record) => {Object.assign(currentFeedback, record)detailDialogVisible.value = true
}</script><style scoped>
.el-card {--el-card-padding: 0;
}.chat-container {height: 500px;display: flex;flex-direction: column;
}.list-card {padding: 20px;background-color: #b0ffca;box-shadow: 10px 10px 10px #6dffff, -10px -10px 10px #f9f9c6 !important;border-radius: 20px;overflow: hidden;
}.chat-card {box-shadow: 10px 10px 10px #6dffff, -10px -10px 10px #f9f9c6 !important;border-radius: 20px;overflow: hidden;
}:deep(.el-card__header) {border-bottom: 0 !important;padding-bottom: 10px;
}.feedback-card {padding: 20px;background-color: #b0ffca;box-shadow: 10px 10px 10px #6dffff, -10px -10px 10px #f9f9c6 !important;border-radius: 20px;overflow: hidden;
}
</style>
  • 专家端
<template><el-row :gutter="20" style="margin-top: 20px;"><!-- 左侧在线咨询列表 --><el-col :span="12"><el-card class="list-card"><el-table :data="filteredUsers"><el-table-column prop="userId" label="用户ID" width="80" /><el-table-column prop="userName" label="用户名" width="80" /><el-table-column prop="role" label="用户角色" /><el-table-column label="状态" width="100"><template #default="{ row }"><el-tag v-if="users.some(user => user.userName === row.userName)" type="success">在线</el-tag><el-tag v-else type="info">离线</el-tag></template></el-table-column><el-table-column label="操作" width="120"><template #default="{ row }"><el-buttonv-if="activeConsultations[row.userName] && consultationIds && consultationIds.hasOwnProperty(row.userName)"@click="handleContinueConsult(row)" type="warning" size="small"style="margin-left: 5px">继续咨询<el-badge v-if="unreadCount[row.userName]" :value="unreadCount[row.userName]":offset="[21, 0]" :max="10"></el-badge></el-button><el-button v-else-if="existConsultationIds &&existConsultationIds.hasOwnProperty(row.userName)&& users.some(user => user.userName === row.userName)" @click="handleConsult(row)"type="primary" size="small">接受咨询</el-button><el-button v-else type="info" size="small" disabled>未发起咨询</el-button></template></el-table-column></el-table></el-card></el-col><!-- 右侧聊天框 --><el-col :span="12"><el-card style="height: inherit;" class="chat-card"><div v-if="chatUser !== ''"><div class="chat-container"><div style="text-align: center; line-height: 50px;">聊天室({{ chatUser }}</div><div style="height: 350px; overflow:auto; border-top: 1px solid #ccc" v-html="content"></div><div style="height: 250px"><textarea v-model="text" style="height: 160px; width: 100%; padding: 20px; border: none; border-top: 1px solid #ccc;border-bottom: 1px solid #ccc; outline: none" @keydown.enter="handleEnter"></textarea><div style="text-align: right; padding-right: 10px"><el-button type="primary" size="small" @click="send">发送</el-button></div></div></div></div><div v-else style="background-color: #b0ffca;"><el-empty description="请从左侧选择一个在线聊天咨询进行服务" /></div></el-card></el-col></el-row>
</template><script>
import { ref, reactive, onMounted, getCurrentInstance, onUnmounted, nextTick, computed } from 'vue';
import { ElMessage } from 'element-plus';export default {name: 'ExpertConsultationMainTop',setup() {let socket = ref(null)const instance = getCurrentInstance();const user = reactive(JSON.parse(sessionStorage.getItem("user")) || {})const users = ref([])const notExpertUsers = ref([]);const avatarNowUserUrl = new URL('@/assets/avatar/avatar_1.png', import.meta.url).href;const avatarRemoteUserUrl = new URL('@/assets/avatar/avatar_2.png', import.meta.url).href;const chatUser = ref('')const text = ref('')const content = ref('')const chatHistory = reactive({}); // 新增:存储各用户的聊天记录// 添加未读计数const unreadCount = reactive({});const activeConsultations = reactive({});const currentChatUser = ref(''); // 跟踪当前聊天用户const consultationIds = reactive(sessionStorage.getItem('consultationIds')? JSON.parse(sessionStorage.getItem('consultationIds')): {}); // 存储用户与咨询ID的映射 {userName: consultationId}const existConsultationIds = reactive({}) //临时存储existConsultationIds,直到专家点击接受咨询后再赋值给consultationIdsonMounted(() => {const savedIds = sessionStorage.getItem('consultationIds');const savedConsultations = sessionStorage.getItem('activeConsultations');const savedHistory = sessionStorage.getItem('chatHistory');if (savedIds) {if (savedIds) {try {const parsed = JSON.parse(savedIds);if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {Object.keys(parsed).forEach(key => {consultationIds[key] = parsed[key];});} else {console.warn('Invalid consultationIds format:', parsed);}} catch (e) {console.error('Failed to parse consultationIds:', e);}}}if (savedConsultations) {Object.assign(activeConsultations, JSON.parse(savedConsultations));}if (savedHistory) {Object.assign(chatHistory, JSON.parse(savedHistory));}init();loadNotExpertUsers();});const loadNotExpertUsers = async () => {try {const res = await instance.proxy.$request.get('/user/notExpertUsers');notExpertUsers.value = res.notExpertUsers;} catch (err) {ElMessage.error('加载在线用户列表失败!');}};// 计算属性过滤数据源const filteredUsers = computed(() => {return notExpertUsers.value.filter(user =>users.value.some(u => u.userName === user.userName));});const handleConsult = async (row) => {// 设置接受咨询后的咨询状态activeConsultations[row.userName] = true;currentChatUser.value = row.userName;// 切换用户时重置未读unreadCount[row.userName] = 0;chatUser.value = row.userName;// 存储consultationId(关键修改)  该值应该在专家点击接收后才能赋值consultationIds[row.userName] = existConsultationIds[row.userName]sessionStorage.setItem('consultationIds', JSON.stringify(consultationIds));// 初始化聊天记录if (!chatHistory[row.userName]) {chatHistory[row.userName] = [];}content.value = chatHistory[row.userName].join('');// 发送接受咨询const message = {type: 'consult_request',from: user.userName,to: row.userName,text: `${user.userName}接受了咨询请求`,consultationId: consultationIds[row.userName]};const html = createContent('system', null, null, message.text); // 传递消息类型chatHistory[row.userName].push(html);content.value = chatHistory[row.userName].join('');if (socket.value) {socket.value.send(JSON.stringify(message));}// 滚动到底部nextTick(() => {const container = document.querySelector('.chat-container div[style*="overflow"]');if (container) container.scrollTop = container.scrollHeight;});text.value = ''//修改会话状态为 进行中 // *************await instance.proxy.$request.put(`/consultation/consulting/${consultationIds[row.userName]}`, {status: '进行中'});};// 继续咨询处理const handleContinueConsult = (row) => {//新增咨询ID验证if (!consultationIds[row.userName]) {ElMessage.warning('未找到有效的咨询记录');}// 设置咨询状态activeConsultations[row.userName] = true;currentChatUser.value = row.userName;unreadCount[row.userName] = 0;chatUser.value = row.userName;content.value = chatHistory[row.userName]?.join('') || '';// 滚动到底部nextTick(() => {const container = document.querySelector('.chat-container div[style*="overflow"]');if (container) container.scrollTop = container.scrollHeight;});text.value = ''};//接收到结束咨询的处理const handleEndConsult = async (from) => {if (!currentChatUser.value)console.log("当前聊天用户不存在currentChatUser.value", currentChatUser);try {// 获取当前咨询ID(关键新增)const consultationId = consultationIds[from];if (!consultationId) {ElMessage.error('未找到咨询记录ID');}// 发送结束通知(修改为只在后台发送通知,而不发给currentChatUser.value)// const message = {//     type: 'consult_end',//     from: user.userName,//     to: currentChatUser.value,//     text: `${user.userName}结束了本次咨询`,//     consultationId: consultationId // 新增携带咨询ID// };// socket.value.send(JSON.stringify(message));// 清除本地状态(新增清理consultationIds,existConsultationIds)delete consultationIds[from];delete existConsultationIds[from]sessionStorage.setItem('consultationIds', JSON.stringify(consultationIds));// 清除状态activeConsultations[from] = false;delete activeConsultations[from];currentChatUser.value = '';chatUser.value = '';content.value = '';} catch (err) {ElMessage.error('结束咨询失败');}};// 修改后的send方法const send = () => {if (!chatUser.value || !text.value.trim()) {ElMessage.warning("请选择专家并输入内容");return;}const message = {from: user.userName,to: chatUser.value,text: text.value};// 发送WebSocket消息socket.value.send(JSON.stringify(message));// 保存到本地记录const html = createContent(null, null, user.userName, text.value);chatHistory[chatUser.value].push(html);content.value = chatHistory[chatUser.value].join('');text.value = '';nextTick(() => {const container = document.querySelector('.chat-container div[style*="overflow"]');if (container) {container.scrollTop = container.scrollHeight;}});};const createContent = (messageType, remoteUser, nowUser, text) => {let html = '';if (messageType === 'system') { // 新增系统消息类型html = `<div class="el-row" style="padding: 5px 0; margin: 2px; background-color: #f0f4ff; border: 4px solid #409eff; border-radius: 10px;"><div class="el-col el-col-22" style="text-align: center; font-weight: 700;"><div class="tip system">系统提示:${text}</div></div></div>`;}else if (nowUser) {html = `<div class="el-row" style="padding: 5px 0"><div class="el-col el-col-22" style="text-align: right; padding-right: 10px"><div class="tip left">${text}</div></div><div class="el-col el-col-2"><span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;"><img src="${avatarRemoteUserUrl}" style="object-fit: cover;"></span></div></div>`} else if (remoteUser) {html = `<div class="el-row" style="padding: 5px 0"><div class="el-col el-col-2" style="text-align: right"><span class="el-avatar el-avatar--circle" style="height: 40px; width: 40px; line-height: 40px;"><img src="${avatarNowUserUrl}" style="object-fit: cover;"></span></div><div class="el-col el-col-22" style="text-align: left; padding-left: 10px"><div class="tip right">${text}</div></div></div>`}return html;}const init = () => {const userName = user.userNameif (typeof (WebSocket) == "undefined") {console.log("您的浏览器不支持WebSocket")return}const socketUrl = `ws://localhost:8080/server/${userName}`if (socket.value) {socket.value.close()socket.value = null}socket.value = new WebSocket(socketUrl)socket.value.onopen = () => {console.log("websocket已打开")}socket.value.onmessage = (msg) => {const data = JSON.parse(msg.data)if (data.users) {users.value = data.users.filter(u => u.userName !== userName)}//处理对方发起咨询请求else if (data.type === 'consult_request') {const fromUser = data.from;// 初始化记录if (!chatHistory[fromUser]) chatHistory[fromUser] = [];// 生成消息内容const html = createContent('system', null, null, data.text);chatHistory[fromUser].push(html);// 存储consultationId(关键修改)  该值应该在专家点击接收后才能赋值existConsultationIds[data.from] = data.consultationIdElMessage.warning(`${data.text}`);}// 处理结束咨询消息else if (data.type === 'consult_end') {const fromUser = data.from;if (chatUser.value === fromUser) {chatUser.value = '';content.value = '';}activeConsultations[fromUser] = false;handleEndConsult(data.from);ElMessage.warning(`${fromUser}结束了咨询`);}// 处理用户离开界面else if (data.type === 'force_close') {const fromUser = data.from;if (chatUser.value === fromUser) {chatUser.value = '';content.value = '';}activeConsultations[fromUser] = false;sessionStorage.setItem('activeConsultations', JSON.stringify(activeConsultations));ElMessage.warning(`${data.text}`);}//正常信息对话else {const fromUser = data.from;// 初始化记录if (!chatHistory[fromUser]) chatHistory[fromUser] = [];// 生成消息内容const html = createContent(null, fromUser, null, data.text);chatHistory[fromUser].push(html);// 更新显示if (chatUser.value === fromUser) {content.value = chatHistory[fromUser].join('');} else {// 更新未读计数unreadCount[fromUser] = (unreadCount[fromUser] || 0) + 1;}}}socket.value.onclose = () => {console.log("websocket已关闭")}socket.value.onerror = () => {console.log("websocket发生了错误")}}// 统一的关闭WebSocket方法const closeSocket = () => {if (socket.value) {socket.value.close()  // 主动调用WebSocket的close方法socket.value = nullconsole.log('WebSocket连接已主动关闭')}}// 组件卸载时onUnmounted(() => {sessionStorage.setItem('activeConsultations', JSON.stringify(activeConsultations));sessionStorage.setItem('chatHistory', JSON.stringify(chatHistory));closeSocket()})const handleEnter = (event) => {event.preventDefault(); // 阻止默认换行行为send();};return {user,users,filteredUsers,chatUser,text,content,send,avatarNowUserUrl,avatarRemoteUserUrl,handleConsult,handleContinueConsult,handleEnter,unreadCount,activeConsultations,existConsultationIds,consultationIds};},
};
</script><style scoped>
.el-card {--el-card-padding: 0;
}.chat-container {height: 500px;display: flex;flex-direction: column;
}.list-card {padding: 20px;background-color: #b0ffca;box-shadow: 10px 10px 10px #6dffff, -10px -10px 10px #f9f9c6 !important;border-radius: 20px;overflow: hidden;
}.chat-card {box-shadow: 10px 10px 10px #6dffff, -10px -10px 10px #f9f9c6 !important;border-radius: 20px;overflow: hidden;
}.time {font-size: 12px;color: #999;margin-top: 4px;
}/* 添加未读标记样式 */
.unread-badge {background: #f56c6c;color: white;border-radius: 11px;min-width: 10px;height: 18px;line-height: 18px;text-align: center;font-size: 10px;padding: 0 4px;margin: 0px 0px 0px 3px;
}/* 确保禁用按钮样式明显 */
.el-button.is-disabled {opacity: 0.6;cursor: not-allowed;
}
</style>
主要后端代码如下
package com.example.doms.component;import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** @author websocket服务*/
@ServerEndpoint(value = "/server/{userName}")
@Component
public class WebSocketServer {private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);/*** 记录当前在线连接数*/public static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("userName") String userName) {sessionMap.put(userName, session);log.info("有新用户加入userName={},当前在线人数为:{}", userName, sessionMap.size());JSONObject result = new JSONObject();JSONArray array = new JSONArray();result.set("users", array);for (Object key : sessionMap.keySet()) {JSONObject jsonObject = new JSONObject();jsonObject.set("userName", key);// {"userName", "zhang", "userName": "admin"}array.add(jsonObject);}
//        {"users": [{"userName": "zhang"},{ "userName": "admin"}]}sendAllMessage(JSONUtil.toJsonStr(result));  // 后台发送消息给所有的客户端}/*** 连接关闭调用的方法*/@OnClosepublic void onClose(Session session, @PathParam("userName") String userName) {sessionMap.remove(userName);log.info("有一连接关闭,移除userName={}的用户session, 当前在线人数为:{}", userName, sessionMap.size());// 新增:通知所有客户端用户列表更新(可用在用户退出的时候,实时更新是否在线)JSONObject result = new JSONObject();JSONArray array = new JSONArray();result.set("users", array);for (Object key : sessionMap.keySet()) {JSONObject jsonObject = new JSONObject();jsonObject.set("userName", key);array.add(jsonObject);}sendAllMessage(JSONUtil.toJsonStr(result)); // 广播更新后的用户列表}/*** 收到客户端消息后调用的方法* 后台收到客户端发送过来的消息* onMessage 是一个消息的中转站* 接受 浏览器端 socket.send 发送过来的 json数据* @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, Session session, @PathParam("userName") String userName) {log.info("服务端收到用户userName={}的消息:{}", userName, message);JSONObject obj = JSONUtil.parseObj(message);// 处理咨询请求(新增)if ("consult_request".equals(obj.getStr("type"))) {String touserName = obj.getStr("to");String text = obj.getStr("text");int consultationId = obj.getInt("consultationId");Session toSession = sessionMap.get(touserName);if (toSession != null) {JSONObject jsonObject = new JSONObject();jsonObject.set("type", "consult_request");jsonObject.set("from", userName);jsonObject.set("text", text);jsonObject.set("consultationId", consultationId);sendMessage(jsonObject.toString(), toSession);log.info("已向专家{}发送咨询请求通知(咨询对话ID:{})", touserName,consultationId);}return; // 结束处理}// 结束对话处理:if ("consult_end".equals(obj.getStr("type"))) {String touserName = obj.getStr("to");String text = obj.getStr("text");Session toSession = sessionMap.get(touserName);if (toSession != null) {JSONObject endMsg = new JSONObject();endMsg.set("type", "consult_end");endMsg.set("from", userName);endMsg.set("text", text);sendMessage(endMsg.toString(), toSession);log.info("用户userName={}已结束对专家userName={}咨询", userName,touserName);}return;}//用户离开页面if ("force_close".equals(obj.getStr("type"))) {String fromUserName = obj.getStr("from");JSONArray toUserNames = obj.getJSONArray("to"); // 接收专家列表String text = obj.getStr("text");// 1. 防御性检查:确保 to 字段是有效数组if (toUserNames == null || toUserNames.isEmpty()) {log.info("用户 {} 离开页面", fromUserName);return;}// 遍历所有关联专家for (int i = 0; i < toUserNames.size(); i++) {String expertUserName = toUserNames.getStr(i);Session expertSession = sessionMap.get(expertUserName);if (expertSession != null) {JSONObject endMsg = new JSONObject();endMsg.set("type", "force_close");endMsg.set("from", fromUserName);endMsg.set("text", text);sendMessage(endMsg.toString(), expertSession);log.info("用户 {} 离开页面,已通知专家 {}", fromUserName, expertUserName);}}return;}String touserName = obj.getStr("to"); // to表示发送给哪个用户,比如 adminString text = obj.getStr("text"); // 发送的消息文本  hello// {"to": "admin", "text": "聊天文本"}Session toSession = sessionMap.get(touserName); // 根据 to用户名来获取 session,再通过session发送消息文本if (toSession != null) {// 服务器端 再把消息组装一下,组装后的消息包含发送人和发送的文本内容// {"from": "zhang", "text": "hello"}JSONObject jsonObject = new JSONObject();jsonObject.set("from", userName);  // from 是 zhangjsonObject.set("text", text);  // text 同上面的textthis.sendMessage(jsonObject.toString(), toSession);log.info("发送给用户userName={},消息:{}", touserName, jsonObject.toString());} else {log.info("发送失败,未找到用户userName={}的session", touserName);}}@OnErrorpublic void onError(Session session, Throwable error) {log.error("发生错误");error.printStackTrace();}/*** 服务端发送消息给客户端*/private void sendMessage(String message, Session toSession) {try {log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);toSession.getBasicRemote().sendText(message);} catch (Exception e) {log.error("服务端发送消息给客户端失败", e);}}/*** 服务端发送消息给所有客户端*/private void sendAllMessage(String message) {try {for (Session session : sessionMap.values()) {log.info("服务端给客户端[{}]发送消息{}", session.getId(), message);session.getBasicRemote().sendText(message);}} catch (Exception e) {log.error("服务端发送消息给客户端失败", e);}}
}
  • 本文仅作个人学习笔记使用,无商业用途。
  • 如若转载,请先声明。

相关文章:

数字果园管理系统的设计与实现(Tensorflow的害虫识别结合高德API的害虫定位与Websocket的在线聊天室)

文章目录 技术栈主要功能害虫识别与定位害虫识别的实现训练与测试评估代码模型转化为TFLite预测脚本PredictController预测控制器害虫识别过程展示 害虫定位实现害虫定位代码害虫定位过程展示 专家咨询功能在线咨询聊天室主要前端代码如下主要后端代码如下 技术栈 Spring Boot…...

信息检索(包含源码)

实验目的 掌握逻辑回归模型在二分类问题中的应用方法熟悉机器学习模型评估指标PR曲线&#xff08;精确率-召回率曲线&#xff09;和ROC曲线&#xff08;受试者工作特征曲线&#xff09;的绘制与分析学习使用Python的scikit-learn库进行数据预处理、模型训练与评估理解特征选择…...

【金仓数据库征文】金仓数据库KingbaseES: 技术优势与实践指南(包含安装)

目录 前言 引言 一 : 关于KingbaseES,他有那些优势呢? 核心特性 典型应用场景 政务信息化 金融核心系统&#xff1a; 能源通信行业&#xff1a; 企业级信息系统&#xff1a; 二: 下载安装KingbaseES 三:目录一览表: 四:常用SQL语句 创建表&#xff1a; 修改表结构…...

Java数据结构——二叉树

二叉树 树的概念二叉树满二叉树和完全二叉树二叉树的性质二叉树的遍历 题目练习前序遍历中序遍历后序遍历 前言 已经知道了数据结构中的线性结构&#xff0c;那有没有非线性结构呢&#xff1f; 当然有就像我们文件夹&#xff0c;一个文件夹中有有另一个文件夹&#xff0c;这就是…...

用go从零构建写一个RPC(仿gRPC,tRPC)--- 版本2

在版本1中&#xff0c;虽然系统能够满足基本需求&#xff0c;但随着连接数的增加和处理请求的复杂度上升&#xff0c;性能瓶颈逐渐显现。为了进一步提升系统的稳定性、并发处理能力以及资源的高效利用&#xff0c;版本2引入了三个重要功能&#xff1a;客户端连接池、服务器长连…...

drf 使用jwt

安装jwt pip install pyJwt 添加登录url path("jwt/login",views.JwtLoginView.as_view(),namejwt-login),path("jwt/order",views.JwtOrderView.as_view(),namejwt-order), 创建视图 from django.contrib.auth import authenticateimport jwt from jw…...

202536 | KafKa生产者分区写入策略+消费者分区分配策略

KafKa生产者分区写入策略 1. 轮询分区策略&#xff08;Round-Robin Partitioning&#xff09; 轮询分区策略 是 Kafka 默认的分配策略&#xff0c;当消息没有指定 key 时&#xff0c;Kafka 会采用轮询的方式将消息均匀地分配到各个分区。 工作原理&#xff1a; 每次生产者发…...

《自动驾驶封闭测试场地建设技术要求》 GB/T 43119-2023——解读

目录 一、标准框架与核心内容 二、重点技术要求 三、实施要点与建议 四、实施时间与参考依据 原文链接&#xff1a;国家标准|GB/T 43119-2023 &#xff08;发布&#xff1a;2023-09-07&#xff1b;实施&#xff1a;2024-01-01&#xff09; 一、标准框架与核心内容 适用范围…...

【C++ Qt】容器类(GroupBox、TabWidget)内附思维导图 通俗易懂

每日激励&#xff1a;“不设限和自我肯定的心态&#xff1a;I can do all things。 — Stephen Curry” ✍️绪论​&#xff1a; 本章主要介绍了 Qt 中 QGroupBox 与 QTabWidget 控件。QGroupBox 是带标题的分组框&#xff0c;能容纳其他控件&#xff0c;有标题、对齐方式、是否…...

【SpringBoot】从环境准备到创建SpringBoot项目的全面解析.

本篇博客给大家带来的是SpringBoot的知识点, 包括Idea的干净卸载… &#x1f40e;文章专栏: JavaEE初阶 &#x1f680;若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子,公主请阅&#x1f680; 要…...

基于ESP32控制的机器人摄像头车

DIY Wi-Fi 控制的机器人摄像头车&#xff1a;从零开始的智能探索之旅 在当今科技飞速发展的时代&#xff0c;机器人技术已经逐渐走进了我们的生活。今天&#xff0c;我将带你一起探索如何制作一个 Wi-Fi 控制的机器人摄像头车&#xff0c;它不仅可以远程操控&#xff0c;还能通…...

Excel图表 vs 专业可视化工具:差距有多大?内容摘要

你是不是还在用 Excel 做图表&#xff0c;觉得它已经够用了&#xff1f;但你知道吗&#xff0c;Excel 和专业的可视化工具之间其实有着巨大的差距&#xff01;Excel 是办公必备&#xff0c;但它的图表功能真的能满足复杂的数据展示需求吗&#xff1f;而那些听起来高大上的专业可…...

Nacos源码—7.Nacos升级gRPC分析三

大纲 5.服务变动时如何通知订阅的客户端 6.微服务实例信息如何同步集群节点 5.服务变动时如何通知订阅的客户端 (1)服务注册和服务订阅时发布的客户端注册和订阅事件的处理 (2)延迟任务的执行引擎源码 (3)处理客户端注册和订阅事件时发布的服务变动和服务订阅事件的处理 (…...

量化学习DAY2-开始批量提交alpha!

量化学习第二天笔记 一、World Quant平台的Alpha概念 在World Quant平台中&#xff0c;alpha本质上是一个数学公式&#xff0c;它是**operator&#xff08;操作&#xff09;与Data&#xff08;数据&#xff09;**的组合。 &#xff08;一&#xff09;Data相关 Data&#xf…...

【Qwen3_ 4b lora xinli 】 task完成实践记录

task 我需要 基于llamafactory框架选取基本上相同的数据集用lora微调Qwen3_ 4b两次并保存lora参数然后分别合并这两个lora参数到基座模型。再换个数据集上接着进行微调。并且保存新的lora参数&#xff0c;然后我们匹配这里面的特征值和特征向量&#xff0c;如果这两个新的lora…...

文旅田园康养小镇规划设计方案PPT(85页)

1. 项目背景与定位 背景&#xff1a;位于长三角经济圈&#xff0c;依托安吉丰富的自然与文化资源&#xff0c;旨在打造集康养、度假、文化体验于一体的综合小镇。 定位&#xff1a;成为浙北地区知名的康养旅游目的地&#xff0c;融合“一溪两岸”规划理念&#xff0c;实现全面…...

[Windows] 能同时打开多个图片的图像游览器JWSEE v2.0

[Windows] 能同时打开多个图片的图像游览器JWSEE 链接&#xff1a;https://pan.xunlei.com/s/VOPpO86Hu3dalYLaZ1ivcTGIA1?pwdhckf# 十多年前收藏的能同时打开多个图片的图像游览器JWSEE v2.0&#xff0c;官网已没有下载资源。 JWSEE v2.0是乌鲁木齐金维图文信息科技有限公司…...

低成本自动化改造技术锚点深度解析

执行摘要 本文旨在深入剖析四项关键的低成本自动化技术&#xff0c;这些技术为工业转型提供了显著的运营和经济效益。文章将提供实用且深入的指导&#xff0c;涵盖老旧设备联网、AGV车队优化、空压机系统智能能耗管控以及此类项目投资回报率&#xff08;ROI&#xff09;的严谨…...

23盘古石决赛

一&#xff0c;流量分析 1. 计算流量包文件的SHA256值是&#xff1f;[答案&#xff1a;字母小写][★☆☆☆☆] 答案&#xff1a;2d689add281b477c82b18af8ab857ef5be6badf253db1c1923528dd73b3d61a9 解压出来流量包计算 2. 流量包长度在“640 - 1279”之间的的数据包总共有多少…...

C语言—指针3

1. 数组名的理解 观察以下代码 可以观察到pa指向的地址与数组首元素地址相同&#xff0c;那么可以说明数组就是首元素地址吗&#xff1f; 这种说法是不严谨的&#xff0c;观察以下代码&#xff1a; 程序输出的结果为16&#xff0c;此时的arr表示的是整个数组的大小。 观察以…...

操作系统 第2章节 进程,线程和作业

一:多道程序设计 1-多道程设计的目的 for:提高吞吐量(作业道数/处理时间),我们可以从提高资源的利用率出发 2-单道程序设计缺点: 设备的利用率低,内存的利用率低,处理机的利用率低 比如CPU去访问内存,CPU空转.内存等待CPU访问也是没有任何操作的.要是有多个东西要去访问不冲…...

数字化转型-4A架构之数据架构

系列文章 数字化转型-4A架构&#xff08;业务架构、应用架构、数据架构、技术架构&#xff09; 数字化转型-4A架构之业务架构 数字化转型-4A架构之应用架构 数据架构 Data Architecture&#xff08;DA&#xff09; 1. 定义 数据架构&#xff0c;是组织管理数据资产的科学之…...

Java中的反射

目录 什么是反射 反射的核心作用 反射的核心类 反射的基本使用 获取Class对象 创建对象 操作字段&#xff08;Field&#xff09; 调用方法&#xff08;Method&#xff09; 反射的应用场景 反射的优缺点 优点 缺点 示例&#xff1a;完整反射操作 总结 什么是反射 …...

LINUX CFS算法解析

文章目录 1. Linux调度器的发展历程2. CFS设计思想3. CFS核心数据结构3.1 调度实体(sched_entity)3.2 CFS运行队列(cfs_rq)3.3 任务结构体中的调度相关字段 4. 优先级与权重4.1 优先级范围4.2 权重映射表 (prio_to_weight[])优先级计算4.3.1. static_prio (静态优先级)4.3.2. n…...

内网渗透——红日靶场三

目录 一、前期准备 二、外网探测 1.使用nmap进行扫描 2.网站信息收集 3.漏洞复现(CVE-2021-23132) 4.disable_function绕过 5.反弹shell&#xff08;也&#xff0c;并不是&#xff09; 6.SSH登录 7.权限提升&#xff08;脏牛漏洞&#xff09; 8.信息收集 9.上线msf 三…...

The 2024 ICPC Kunming Invitational Contest G. Be Positive

https://codeforces.com/gym/105386/problem/G 题目&#xff1a; 结论&#xff1a; 从0开始每四个相邻数的异或值为0 代码&#xff1a; #include<bits/stdc.h> using namespace std; #define int long long void solve() {int n;cin >> n;if(n1||n%40){cout &…...

CommunityToolkit.Mvvm详解

属性可视化 给一个属性添加ObservableProperty就可以可视化了 [ObservableProperty] private string currentNameInfo;[ObservableProperty] private string currentClassInfo;[ObservableProperty] private string currentPhoneInfo;xaml中只需要绑定大写的属性就可以了 &l…...

密码学--AES

一、实验目的 1、完成AES算法中1轮加密和解密操作 2、掌握AES的4个基本处理步骤 3、理解对称加密算法的“对称”思想 二、实验内容 1、题目内容描述 &#xff08;1&#xff09;利用C语言实现字节代换和逆向字节代换&#xff0c;字节查S盒代换 &#xff08;2&#xff09;利…...

操作系统的初步了解

目录 引言&#xff1a;什么是操作系统&#xff1f; 一、设计操作系统的目的 二、操作系统是做什么的&#xff1a; 操作系统主要有四大核心任务&#xff1a; 1. 管理硬件 2. 运行软件 3. 存储数据 4. 提供用户界面 如何理解操作系统的管理呢&#xff1f; 1. 什么是操作…...

边缘计算:技术概念与应用详解

引言 随着物联网&#xff08;IoT&#xff09;、5G 和人工智能&#xff08;AI&#xff09;的快速发展&#xff0c;传统的云计算架构在处理海量数据和实时计算需求时逐渐显现出瓶颈。边缘计算&#xff08;Edge Computing&#xff09;作为一种新兴的计算范式&#xff0c;通过将计…...

C++进阶--红黑树的实现

文章目录 红黑树的实现红黑树的概念红黑树的规则红黑树的效率 红黑树的实现红黑树的结构红黑树的插入变色单旋&#xff08;变色&#xff09;双旋&#xff08;变色&#xff09; 红黑树的查找红黑树的验证 总结&#xff1a;结语 很高兴和大家见面&#xff0c;给生活加点impetus&a…...

[C++类和对象]类和对象的引入

面向过程和面向对象 C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用来逐步解决问题 C是基于面向对象的,关注的是对象,将一件事情分成不同的对象,靠对象之间完成交互 类的引入 C语言结构体中只能定义变量,在C中,结构体不仅仅可以定义变量,而且可以定义函…...

YOLOv12云端GPU谷歌免费版训练模型

1.效果 2.打开 https://colab.research.google.com/?utm_sourcescs-index 3.上传代码 4.解压 !unzip /content/yolov12-main.zip -d /content/yolov12-main 5.进入yolov12-main目录 %cd /content/yolov12-main/yolov12-main 6.安装依赖库 !pip install -r requirements.…...

课程审核流程揭秘:确保内容合规与用户体验

业务流程 为什么课程审核通过才可以发布呢&#xff1f; 这样做为了防止课程信息有违规情况&#xff0c;课程信息不完善对网站用户体验也不好&#xff0c;课程审核不仅起到监督作用&#xff0c;也是 帮助教学机构规范使用平台的手段。 如果流程复杂用工作流 说明如下&#xff…...

【LangChain高级系列】LangGraph第一课

前言 我们今天直接通过一个langgraph的基础案例&#xff0c;来深入探索langgraph的核心概念和工作原理。 基本认识 LangGraph是一个用于构建具有LLMs的有状态、多角色应用程序的库&#xff0c;用于创建代理和多代理工作流。与其他LLM框架相比&#xff0c;它提供了以下核心优…...

ATH12K 驱动框架

ATH12K 驱动框架 ath12k驱动框架及模块交互逻辑详解1. 总体架构2. 关键数据结构2.1 核心数据结构2.2 虚拟接口数据结构3. 硬件抽象层(HAL)4. 无线管理接口(WMI)5. 主机目标通信(HTC)6. 数据路径(DP)6.1 发送路径(TX)6.2 接收路径(RX)7. 多链路操作(MLO)8. 初始化和工作流程8.1 …...

CMA认证对象?CMA评审依据,CMA认证好处

CMA认证对象 CMA&#xff08;中国计量认证&#xff0c;China Metrology Accreditation&#xff09;的认证对象主要是第三方检测机构和实验室&#xff0c;包括&#xff1a; 独立检测机构&#xff1a;如环境监测站、产品质量检验所、食品药品检测机构等。 企业内部实验室&#…...

依赖关系-根据依赖关系求候选码

关系模式R&#xff08;U, F&#xff09;, U{}&#xff0c;F是R的函数依赖集&#xff0c;可以将属性分为4类&#xff1a; L: 仅出现在依赖集F左侧的属性 R: 仅出现在依赖集F右侧的属性 LR: 在依赖集F左右侧都出现的属性 NLR: 在依赖集F左右侧都未出现的属性 结论1: 若X是L类…...

解决应用程序在JAR包中运行时无法读取类路径下文件的问题

问题情景 java应用程序在IDE运行正常&#xff0c;打成jar包后执行却发生异常&#xff1a; java.io.FileNotFoundException: class path resource [cert/sync_signer_pri_test.key] cannot be resolved to absolute file path because it does not reside in the file system:…...

第十六届蓝桥杯B组第二题

当时在考场的时候这一道题目 无论我是使用JAVA的大数&#xff08;BIGTHGER&#xff09;还是赛后 使用PY 都是没有运行出来 今天也是突发奇想在B站上面搜一搜 看了才知道这也是需要一定的数学思维 通过转换 设X来把运算式精简化 避免运行超时 下面则是代码 public class lanba…...

龙虎榜——20250509

上证指数今天缩量&#xff0c;整体跌多涨少&#xff0c;走势处于日线短期的高位~ 深证指数今天缩量小级别震荡&#xff0c;大盘股表现更好~ 2025年5月9日龙虎榜行业方向分析 一、核心行业方向 军工航天 • 代表个股&#xff1a;航天南湖、天箭科技、襄阳轴承。 • 驱动逻辑…...

node提示node:events:495 throw er解决方法

前言 之前开发的时候喜欢使用高版本&#xff0c;追求新的东西&#xff0c;然后回头运行一下之前的项目提示如下 项目技术栈&#xff1a;node egg 报错 node:events:495 throw er; // Unhandled error event ^ Error: ENOENT: no such file or directory, scandir F:\my\gi…...

OrangePi Zero 3学习笔记(Android篇)4 - eudev编译(获取libudev.so)

目录 1. Ubuntu中编译 2. NDK环境配置 3. 编译 4. 安装 这部分主要是为了得到libudev&#xff08;因为原来的libudev已经不更新了&#xff09;&#xff0c;eudev的下载地址如下&#xff1a; https://github.com/gentoo/eudev 相应的代码最好是在Ubuntu中先编译通过&#…...

[AI ][Dify] Dify Tool 插件调试流程详解

在使用 Dify 进行插件开发时,调试是必不可少的环节。Dify 提供了远程服务调试的能力,让开发者可以快速验证插件功能和交互逻辑。本文将详细介绍如何配置环境变量进行插件调试,并成功在插件市场中加载调试状态的插件。 一、调试环境配置 在 Dify 的插件调试过程中,我们需要…...

learning ray之ray强化学习/超参调优和数据处理

之前我们掌握了Ray Core的基本编程&#xff0c;我们已经学会了如何使用Ray API。现在&#xff0c;让我们将这些知识应用到一个更实际的场景中——构建一个强化学习项目&#xff0c;并且利用Ray来加速它。 我们的目标是&#xff0c;通过Ray的任务和Actor&#xff0c;将一个简单…...

gpu硬件,gpu驱动,cuda,CUDA Toolkit,cudatoolkit,cudnn,nvcc概念解析

组件角色依赖关系GPU硬件无CUDA编程模型/平台需NVIDIA GPU和驱动CUDA Toolkit开发工具包&#xff08;含NVCC、库等&#xff09;需匹配GPU驱动和CUDA版本cuDNN深度学习加速库需CUDA ToolkitNVCCCUDA代码编译器包含在CUDA Toolkit中 GPU硬件&#xff1a; 硬件层面的图形处理器&…...

【C/C++】范围for循环

&#x1f4d8; C 范围 for 循环详解&#xff08;Range-based for loop&#xff09; 一、什么是范围 for 循环&#xff1f; 范围 for 循环&#xff08;Range-based for loop&#xff09; 是 C11 引入的一种简化容器/数组遍历的方式。它通过自动调用容器的 begin() 和 end() 方法…...

嵌入式开发学习(第二阶段 C语言基础)

C语言&#xff1a;第4天笔记 内容提要 流程控制 C语句数据的输入与输出 流程控制 C语句 定义 C程序是以函数为基础单位的。一个函数的执行部分是由若干条语句构成的。C语言都是用来完成一定操作的任务。C语句必须依赖于函数存在。 C程序结构 C语句分类 1.控制语句 作…...

大物重修之浅显知识点

第一章 质点运动学 例1 知识点公式如下&#xff1a; 例2 例3 例4 例5 例6 第四章 刚体的转动 例1 例2 例3 例4 例5 例6 第五章 简谐振动 例1 例2 例3 第六章 机械波 第八章 热力学基础 第九章 静电场 第十一章 恒定磁场…...

随笔-近况

好久没写了&#xff0c;手都生了。 我写的东西可以分为两类&#xff1a;技术和随笔。当然技术没有我自己创新的&#xff0c;都是些在解决问题过程中查询了很多资料&#xff0c;经过验证后&#xff0c;可以在项目上使用的。但是自从 deepseek 出现后&#xff0c;问题一下子简单…...