面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 01:21

OpenCV.js 在实际项目中有哪些应用场景?

OpenCV.js 在实际开发中有很多应用场景,以下是几个典型的实战案例:1. 网页端图像编辑器功能实现class ImageEditor { constructor(canvasId) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.originalImage = null; this.currentImage = null; } loadImage(file) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { this.canvas.width = img.width; this.canvas.height = img.height; this.ctx.drawImage(img, 0, 0); this.originalImage = cv.imread(this.canvas); this.currentImage = this.originalImage.clone(); resolve(); }; img.onerror = reject; img.src = URL.createObjectURL(file); }); } applyFilter(filterType) { let temp = new cv.Mat(); try { switch(filterType) { case 'grayscale': cv.cvtColor(this.currentImage, temp, cv.COLOR_RGBA2GRAY); cv.cvtColor(temp, this.currentImage, cv.COLOR_GRAY2RGBA); break; case 'blur': cv.GaussianBlur(this.currentImage, temp, new cv.Size(15, 15), 0); temp.copyTo(this.currentImage); break; case 'sharpen': let kernel = cv.matFromArray(3, 3, cv.CV_32FC1, [ 0, -1, 0, -1, 5, -1, 0, -1, 0 ]); cv.filter2D(this.currentImage, temp, -1, kernel); temp.copyTo(this.currentImage); kernel.delete(); break; case 'edge': cv.cvtColor(this.currentImage, temp, cv.COLOR_RGBA2GRAY); cv.Canny(temp, temp, 50, 100); cv.cvtColor(temp, this.currentImage, cv.COLOR_GRAY2RGBA); break; } cv.imshow(this.canvas.id, this.currentImage); } finally { temp.delete(); } } adjustBrightness(value) { let temp = new cv.Mat(); try { this.currentImage.convertTo(temp, -1, 1, value); temp.copyTo(this.currentImage); cv.imshow(this.canvas.id, this.currentImage); } finally { temp.delete(); } } reset() { this.currentImage = this.originalImage.clone(); cv.imshow(this.canvas.id, this.currentImage); } download() { const link = document.createElement('a'); link.download = 'edited-image.png'; link.href = this.canvas.toDataURL(); link.click(); }}2. 实时人脸检测和识别class FaceDetector { constructor(videoId, canvasId) { this.video = document.getElementById(videoId); this.canvas = document.getElementById(canvasId); this.faceCascade = new cv.CascadeClassifier(); this.isRunning = false; } async init() { // 加载人脸检测模型 await this.loadModel('haarcascade_frontalface_default.xml'); // 启动摄像头 const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }); this.video.srcObject = stream; await this.video.play(); this.canvas.width = this.video.videoWidth; this.canvas.height = this.video.videoHeight; } async loadModel(url) { return new Promise((resolve, reject) => { this.faceCascade.load(url); resolve(); }); } start() { this.isRunning = true; this.detect(); } stop() { this.isRunning = false; } detect() { if (!this.isRunning) return; let src = new cv.Mat(); let gray = new cv.Mat(); let faces = new cv.RectVector(); try { // 读取视频帧 src = cv.imread(this.video); // 转灰度 cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY); // 检测人脸 this.faceCascade.detectMultiScale(gray, faces, 1.1, 3, 0); // 绘制人脸框 for (let i = 0; i < faces.size(); ++i) { let face = faces.get(i); let point1 = new cv.Point(face.x, face.y); let point2 = new cv.Point(face.x + face.width, face.y + face.height); cv.rectangle(src, point1, point2, [255, 0, 0, 255], 2); // 添加标签 cv.putText(src, `Face ${i + 1}`, new cv.Point(face.x, face.y - 10), cv.FONT_HERSHEY_SIMPLEX, 0.5, [0, 255, 0, 255], 1); } cv.imshow(this.canvas.id, src); requestAnimationFrame(() => this.detect()); } finally { src.delete(); gray.delete(); faces.delete(); } }}3. OCR 文字识别class OCRProcessor { constructor() { this.tesseract = null; } async init() { // 初始化 Tesseract.js this.tesseract = Tesseract.createWorker({ logger: m => console.log(m) }); await this.tesseract.loadLanguage('eng'); await this.tesseract.initialize('eng'); } async preprocessImage(imageElement) { let src = cv.imread(imageElement); let gray = new cv.Mat(); let binary = new cv.Mat(); let denoised = new cv.Mat(); try { // 转灰度 cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY); // 降噪 cv.medianBlur(gray, denoised, 3); // 二值化 cv.threshold(denoised, binary, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU); // 显示预处理结果 const canvas = document.getElementById('preprocessedCanvas'); cv.imshow(canvas.id, binary); return binary; } finally { src.delete(); gray.delete(); denoised.delete(); } } async recognizeText(imageElement) { // 预处理图像 const processed = await this.preprocessImage(imageElement); // 转换为 ImageData const canvas = document.getElementById('preprocessedCanvas'); const imageData = canvas.toDataURL('image/png'); // OCR 识别 const { data: { text } } = await this.tesseract.recognize(imageData); processed.delete(); return text; } async cleanup() { await this.tesseract.terminate(); }}4. 实时二维码扫描class QRScanner { constructor(videoId, canvasId) { this.video = document.getElementById(videoId); this.canvas = document.getElementById(canvasId); this.isScanning = false; } async start() { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); this.video.srcObject = stream; await this.video.play(); this.canvas.width = this.video.videoWidth; this.canvas.height = this.video.videoHeight; this.isScanning = true; this.scan(); } scan() { if (!this.isScanning) return; let src = new cv.Mat(); let gray = new cv.Mat(); let edges = new cv.Mat(); try { src = cv.imread(this.video); cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY); cv.Canny(gray, edges, 50, 150); // 查找轮廓 let contours = new cv.MatVector(); let hierarchy = new cv.Mat(); cv.findContours(edges, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE); // 检测二维码 for (let i = 0; i < contours.size(); i++) { let contour = contours.get(i); let area = cv.contourArea(contour); if (area > 1000) { // 绘制轮廓 cv.drawContours(src, contours, i, [0, 255, 0, 255], 2); // 提取二维码区域 let rect = cv.boundingRect(contour); let qrCode = src.roi(rect); // 使用 jsQR 库解码 const imageData = new ImageData( new Uint8ClampedArray(qrCode.data), qrCode.cols, qrCode.rows ); const code = jsQR(imageData.data, imageData.width, imageData.height); if (code) { console.log('QR Code:', code.data); // 触发回调 this.onQRCodeDetected(code.data); } qrCode.delete(); } } cv.imshow(this.canvas.id, src); requestAnimationFrame(() => this.scan()); } finally { src.delete(); gray.delete(); edges.delete(); } } stop() { this.isScanning = false; } onQRCodeDetected(data) { // 重写此方法处理二维码数据 console.log('QR Code detected:', data); }}5. 实时视频滤镜class VideoFilter { constructor(videoId, canvasId) { this.video = document.getElementById(videoId); this.canvas = document.getElementById(canvasId); this.currentFilter = 'none'; } async start() { const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }); this.video.srcObject = stream; await this.video.play(); this.canvas.width = this.video.videoWidth; this.canvas.height = this.video.videoHeight; this.process(); } setFilter(filterName) { this.currentFilter = filterName; } process() { let src = new cv.Mat(); let dst = new cv.Mat(); try { src = cv.imread(this.video); switch(this.currentFilter) { case 'grayscale': cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); cv.cvtColor(dst, dst, cv.COLOR_GRAY2RGBA); break; case 'sepia': this.applySepia(src, dst); break; case 'cartoon': this.applyCartoon(src, dst); break; case 'emboss': this.applyEmboss(src, dst); break; default: src.copyTo(dst); } cv.imshow(this.canvas.id, dst); requestAnimationFrame(() => this.process()); } finally { src.delete(); dst.delete(); } } applySepia(src, dst) { let kernel = cv.matFromArray(3, 3, cv.CV_32FC1, [ 0.272, 0.534, 0.131, 0.349, 0.686, 0.168, 0.393, 0.769, 0.189 ]); cv.transform(src, dst, kernel); kernel.delete(); } applyCartoon(src, dst) { let gray = new cv.Mat(); let edges = new cv.Mat(); let color = new cv.Mat(); cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY); cv.medianBlur(gray, gray, 7); cv.Canny(gray, edges, 50, 150); cv.cvtColor(edges, edges, cv.COLOR_GRAY2RGBA); cv.bilateralFilter(src, color, 9, 250, 250); cv.bitwise_and(color, edges, dst); gray.delete(); edges.delete(); color.delete(); } applyEmboss(src, dst) { let kernel = cv.matFromArray(3, 3, cv.CV_32FC1, [ -2, -1, 0, -1, 1, 1, 0, 1, 2 ]); cv.filter2D(src, dst, -1, kernel); kernel.delete(); }}这些实战案例展示了 OpenCV.js 在不同场景下的应用,开发者可以根据具体需求选择合适的实现方案。
服务端阅读 05月29日 01:21

GORM 的 AutoMigrate 功能如何使用?

AutoMigrate 根据 Go 结构体的 tag 自动创建表、添加缺失的列和索引,是纯增量操作——不会删除列、不会修改列类型、不会重命名列。这意味着一旦某列被创建,即使结构体中删除了该字段,数据库中仍会保留。对于列类型变更(如 string 改为 text),AutoMigrate 静默跳过。生产环境不应依赖 AutoMigrate,应使用 golang-migrate 等版本化迁移工具,AutoMigrate 仅适合开发和快速原型阶段。追问AutoMigrate 检测到列类型不匹配时会怎样?会报错吗?如何手动删除一列?db.Migrator().DropColumn() 在生产环境有什么风险?AutoMigrate 对已存在的表修改索引的行为是什么?多个服务同时启动时 AutoMigrate 会冲突吗?如何保证迁移安全?db.Migrator().HasTable() 和 db.Migrator().HasColumn() 在实际迁移逻辑中怎么用?写段代码type User struct { gorm.Model Name string `gorm:"size:100;not null"` Email string `gorm:"uniqueIndex"`}// 只做增量,不会删除旧列db.AutoMigrate(&User{})// 手动操作需用 Migratordb.Migrator().DropColumn(&User{}, "OldField")
服务端阅读 05月29日 01:21

OpenCV.js 开发中常见问题及解决方案有哪些?

OpenCV.js 开发中最常见的问题有三个:一是 WASM 加载失败,cv 对象 undefined,原因是 opencv.js 文件约 8MB 加载慢或 CDN 不稳定,解决方案是配置多个 CDN 备用并监听 cv.onRuntimeInitialized 回调确认就绪;二是内存泄漏,浏览器长时间运行变卡,根因是 cv.Mat 通过 WASM 堆分配内存不受 JS GC 管理,必须在 try-finally 中调用 mat.delete(),视频循环中更要复用 Mat 对象而非每帧新建;三是跨域图像无法处理,canvas 被 tainted 后 cv.imread() 报错,需在 img 标签设置 crossOrigin='Anonymous' 或通过服务端代理。此外,模型文件(如 Haar Cascade XML)在浏览器端无法直接读本地路径,需 fetch 下载为 ArrayBuffer 再加载。追问cv.onRuntimeInitialized 和 Module.onRuntimeInitialized 有什么区别?前者是 OpenCV.js 的回调,后者是 Emscripten 底层回调。推荐用 cv.onRuntimeInitialized,它在 OpenCV API 完全可用时触发,而 Module 版本可能在 WASM 编译完成但 JS 绑定未就绪时就触发。delete 一个已 delete 的 Mat 会怎样?会抛出异常。安全做法是 delete 后将变量设为 null,或封装一个 safeDelete(mat) 函数先判断再调用。视频循环中更好的做法是复用 Mat 而非反复创建删除。如何排查 OpenCV.js 的内存泄漏?用 Chrome DevTools 的 Memory 面板做堆快照对比,关注 WASM 堆增长。也可在代码中用 cv.getBuildInformation() 确认版本,用 performance.memory(Chrome)监控 JS 堆外内存变化。opencv.js 文件太大怎么优化加载?可自行编译精简版,通过 opencvcontrib 的 buildjs.py 脚本用 -DBUILD_LIST 指定只编译需要的模块(如 core,imgproc),体积可从 8MB 降到 2-3MB。也可开启 WASM 流式编译(Streaming instantiation)加速。canvas tainted 的具体报错是什么?怎么彻底避免?报错为 'The canvas has been tainted by cross-origin data'。根本方案:所有外部图片设 crossOrigin 属性、服务端返回正确 CORS 头、避免在 canvas 中绘制未授权跨域资源。一旦 tainted 无法逆转,只能重建 canvas。写段代码function onOpenCvReady() { let src = cv.imread('canvasInput'); let dst = new cv.Mat(); try { cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); cv.imshow('canvasOutput', dst); } finally { src.delete(); dst.delete(); }}cv.onRuntimeInitialized = onOpenCvReady;
服务端阅读 05月29日 01:20

GORM 中的软删除(Soft Delete)是如何工作的?

GORM 软删除通过 gorm.DeletedAt 字段实现:模型包含该字段后,db.Delete() 不会执行 DELETE,而是 UPDATE SET deleted_at=NOW();查询时 GORM 自动追加 WHERE deleted_at IS NULL 过滤已删除记录。要用 Unscoped() 查询包含已删除的记录,用 Unscoped().Delete() 执行真正的硬删除。软删除的最大坑是唯一约束——已软删除的记录仍占据唯一索引位置,导致无法插入同值新记录,需用复合唯一索引 (email, deleted_at) 解决。追问db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users) 和 db.Unscoped().Find(&users) 结果有何区别?软删除记录如何恢复?恢复时唯一约束冲突怎么处理?关联查询(Preload)中软删除的记录会被过滤吗?如何加载已删除的关联?为什么不推荐在生产环境依赖软删除做数据审计?应该用什么替代方案?自定义软删除字段(如 is_deleted bool)时 GORM 还会自动过滤吗?写段代码type User struct { gorm.Model Email string `gorm:"uniqueIndex:idx_email_deleted"`}// 复合唯一索引解决软删除冲突// db.Unscoped().Delete(&user) // 硬删除// db.Unscoped().Where("id = ?", id).Update("deleted_at", nil) // 恢复
服务端阅读 05月29日 01:20

OpenCV.js 如何进行机器学习任务?

OpenCV.js 的机器学习能力有限,主要提供传统算法(KNN、SVM、决策树、RTrees、Boost、MLP),不支持训练深度学习模型。实际开发中更常用的方式是通过 DNN 模块加载预训练模型做推理,支持 Caffe、TensorFlow、ONNX 等格式的模型。训练流程通过 cv.ml.KNearest / cv.ml.SVM.create() 等创建模型,调用 train() 方法用 Mat 格式的特征和标签训练,再用 predict() 推理。但 OpenCV.js 不适合做复杂 ML 任务,浏览器端做 ML 推理更推荐 TensorFlow.js 或 ONNX Runtime Web,OpenCV.js 的 ML 模块更适合小规模分类等轻量场景。追问OpenCV.js 能训练深度学习模型吗?不能。OpenCV.js 的 DNN 模块只支持前向推理,不包含反向传播。要在浏览器训练深度学习模型需用 TensorFlow.js 等框架。OpenCV.js 的 MLP 也只是传统浅层网络。DNN 模块支持哪些模型格式?支持 Caffe(.caffemodel + .prototxt)、TensorFlow(.pb)、ONNX(.onnx)、Darknet(.weights + .cfg)。加载用 cv.readNetFromONNX() 等方法,模型文件需通过 fetch 下载到浏览器。KNN 和 SVM 在 OpenCV.js 中哪个更实用?小数据集简单分类用 KNN 更方便(无需调参),SVM 在特征空间复杂时效果更好但需调核函数和超参。两者都不适合大规模数据,浏览器内存有限。如何在浏览器端提取图像特征用于 ML?常用方法:cv.calcHist() 计算直方图特征、HOG 描述子(cv.HOGDescriptor)、或用 ORB 提取局部特征再聚合。更高级的特征提取建议用 DNN 模块加载预训练 CNN 做特征提取。OpenCV.js ML 模块最大的局限是什么?三点:一是不支持 GPU 加速,纯 CPU/WASM 运行速度慢;二是训练数据必须全部加载到内存,浏览器内存限制制约了数据规模;三是模型无法持久化保存,每次刷新页面需重新训练。写段代码let svm = cv.ml.SVM.create();svm.setType(cv.ml.SVM_C_SVC);svm.setKernel(cv.ml.SVM_LINEAR);svm.train(trainData, cv.ml.ROW_SAMPLE, labels);let result = svm.predict(testSample);console.log('class:', result);svm.clear();
服务端阅读 05月29日 01:20

React Query 的缓存机制是如何工作的,如何配置和管理缓存?

React Query 缓存的核心是两个时间参数:staleTime 决定数据何时被标记为"过期"(默认 0,即立即过期),gcTime(原 cacheTime)决定数据何时被垃圾回收(默认 5 分钟)。数据在 staleTime 内不会重新请求,但窗口聚焦时会触发 refetch;超过 gcTime 且无观察者的查询会被从缓存清除。管理缓存的三个关键 API:queryClient.invalidateQueries() 标记失效并触发重新获取、queryClient.setQueryData() 直接更新缓存数据、queryClient.removeQueries() 彻底移除缓存条目。对于需要持久化的场景,可借助 persistQueryClient 将缓存写入 localStorage。追问staleTime 设为 Infinity 意味着什么?什么场景下合理?invalidateQueries 和 resetQueries 的区别是什么?如何利用 queryKey 层级结构实现局部失效(如只刷新某个用户下的 todo 列表)?窗口聚焦 refetch 在移动端或 Electron 中表现如何?如何关闭?持久化缓存到 localStorage 时如何处理敏感数据和版本不一致问题?写段代码const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60_000, gcTime: 30 * 60_000 } }})// mutation 成功后局部失效const mu = useMutation(updateTodo, { onSuccess: () => queryClient.invalidateQueries(['todos'])})
服务端阅读 05月29日 01:20

OpenCV.js 如何实现实时视频处理?

OpenCV.js 实现实时视频处理的核心流程是:通过 navigator.mediaDevices.getUserMedia() 获取摄像头流绑定到 video 元素,再用 cv.VideoCapture(video) 逐帧读取到 cv.Mat,在 requestAnimationFrame 循环中完成处理和 cv.imshow() 输出。性能瓶颈主要在 WASM 单线程执行和帧处理耗时,常用优化手段包括降低处理分辨率(先 resize 到小尺寸处理再放大显示)、用 Web Worker 将计算移到后台线程、以及控制帧率跳帧处理。内存管理上,视频循环中必须及时 delete 每帧的 Mat 对象,否则几秒内就会内存溢出。追问cv.VideoCapture 的参数可以传 canvas 吗?可以。VideoCapture 构造函数接受 video 或 canvas 元素,传 canvas 时从 canvas 读取当前帧。这对于处理已有图像序列或截图场景很有用。requestAnimationFrame 和 setTimeout 控制帧循环哪个好?requestAnimationFrame 与浏览器渲染同步,在不活动标签页自动暂停,更省资源。setTimeout 可精确控制帧率但不会自动暂停。实时视频场景推荐 requestAnimationFrame。Web Worker 中能直接使用 cv 对象吗?不能直接共享。Worker 需要独立加载 opencv.js 脚本,主线程通过 postMessage 传递 ImageData 的 ArrayBuffer(使用 Transferable 零拷贝),Worker 处理后回传结果。如何检测和处理帧率下降?用 performance.now() 记录每帧处理耗时,当单帧耗时超过 1000/targetFPS 时跳过处理帧只显示原始画面,或降低处理分辨率动态适配。CascadeClassifier 加载 XML 模型文件在浏览器端怎么做?浏览器无法直接读本地文件,需通过 fetch 下载 XML 文件为 ArrayBuffer,再用 cv.CascadeClassifier 的 load 方法传入 Uint8Array。也可将 XML 编码为 Base64 内嵌在代码中。写段代码let cap = new cv.VideoCapture(video);let src = new cv.Mat(), dst = new cv.Mat();function loop() { cap.read(src); cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); cv.Canny(dst, dst, 50, 100); cv.imshow('output', dst); requestAnimationFrame(loop);}loop();
服务端阅读 05月29日 01:20

Shell 脚本中单引号和双引号有什么区别?

单引号是强引用,内部所有字符按字面量处理,变量 $var、命令替换 $(cmd)、转义符 \n 全部原样输出。双引号是弱引用,允许变量展开、命令替换和少数转义(\$ \" \ \n),其余字符原样保留。实际开发中 90% 的场景用双引号——既能展开变量,又能防止空格拆分和 glob 展开。只在需要原样输出 $ 符号或特殊字符时才用单引号。另外 $'…' 语法支持 \n \t 等转义序列,是 echo -e 的替代方案。追问1. 不加引号、双引号、单引号对变量赋值有什么不同?不加引号时变量值会被 word splitting 和 glob 展开:var=hello world 会报错,var=*.txt 会展开为匹配的文件列表。双引号阻止 word splitting 和 glob 但保留变量展开。单引号阻止一切展开。2. 单引号里怎么输出单引号本身?单引号内无法转义单引号。解决办法是用拼接:'it'\''s' ——结束单引号、转义单引号、重新开始单引号。或改用双引号 "it's"。3. $'…' 和 "…" 都支持转义,有什么区别?$'…' 只做转义展开,不做变量和命令替换,是真正的转义字符串。"…" 既做转义又做变量/命令替换。需要转义但不需变量展开时用 $'…' 更精确。4. 双引号能防止哪些问题?举个实际例子。防止空格拆分:file="my doc.txt"; rm "$file" 正确删除,不加引号会删 my 和 doc.txt 两个文件。防止 glob 展开:echo "*.txt" 输出字面量,不加引号会列出所有 .txt 文件。5. 单引号和双引号有性能差异吗?理论上单引号更快(跳过展开解析),但差异极小可忽略。选择依据应是语义正确性而非性能。写段代码name="world"echo 'Hello $name' # Hello $nameecho "Hello $name" # Hello worldecho $'line1\nline2' # 换行输出echo 'it'\''s me' # it's me
服务端阅读 05月29日 01:20

OpenCV.js 中如何进行特征检测和匹配?

OpenCV.js 中特征检测首选 ORB,因为它是免费的且速度快,通过 new cv.ORB() 创建检测器,调用 detectAndCompute() 同时提取关键点和描述子。特征匹配使用 cv.BFMatcher 配合 cv.NORM_HAMMING 距离(ORB 描述子是二进制的)。注意 SIFT 在 OpenCV.js 中支持有限,需确认编译时是否启用。匹配完成后用 cv.drawMatches() 可视化结果。对于形状检测,cv.findContours() 做轮廓提取,cv.HoughLinesP() 和 cv.HoughCircles() 分别做直线和圆检测。追问为什么 OpenCV.js 推荐 ORB 而不是 SIFT?ORB 不受专利限制,计算速度比 SIFT 快一个数量级,且描述子是二进制的,匹配用汉明距离更快。SIFT 在 JS 端性能开销大,且部分 OpenCV.js 构建不包含 SIFT 模块。BFMatcher 的 crossCheck 参数有什么作用?crossCheck 为 true 时只保留双向匹配一致的对(A匹配B且B也匹配A),能有效过滤误匹配,代价是匹配点数量减少。FLANN 匹配器在 OpenCV.js 中可用吗?cv.FlannBasedMatcher 在部分版本可用,但因其依赖 FLANN 库的完整构建,部分精简版 OpenCV.js 不包含。实际项目中 BFMatcher 更稳妥。如何筛选优质匹配点?对 knnMatch 返回的 k=2 邻近结果计算距离比值(Lowe's ratio test),若最近邻距离 / 次近邻距离 < 0.7 则保留,这是滤除误匹配的标准做法。轮廓检测前为什么要先做二值化?cv.findContours() 要求输入为 8 位单通道二值图像,非零像素被视为前景。不二值化则轮廓提取结果不可预测,通常先灰度化再 threshold 或 Canny。写段代码let orb = new cv.ORB(), kp = new cv.KeyPointVector();let desc = new cv.Mat();orb.detectAndCompute(gray, new cv.Mat(), kp, desc);let matcher = new cv.BFMatcher(cv.NORM_HAMMING, true);let matches = new cv.DMatchVector();matcher.match(desc1, desc2, matches);kp.delete(); desc.delete(); matches.delete();
服务端阅读 05月29日 01:20

Shell 脚本中 for、while、until 循环怎么用?

Shell 有三种循环:for 遍历列表或范围,while 条件为真时重复,until 条件为真时停止。for 有两种写法——for item in list 遍历集合,for ((i=0;i<N;i++)) 是 C 风格计数循环。while 最经典的用法是逐行读文件:while IFS= read -r line。until 和 while 逻辑相反,常用于等待服务就绪。break N 可跳出 N 层循环,continue 跳过当前迭代。无限循环用 while true 或 for ((;;))。追问1. for item in $(ls) 有什么问题?应该怎么遍历文件?$(ls) 会按空格拆分,含空格的文件名会被切成多段。正确做法是 for item in * 或 for item in dir/*,由 Shell 的路径展开直接生成文件列表,不会拆分含空格的文件名。2. while read 读文件为什么要加 IFS= 和 -r?不加 IFS=,前导尾随空白会被截掉;不加 -r,反斜杠会被当作转义符吞掉。正确写法 while IFS= read -r line 是防御性编程的标准范式。3. while 和 until 分别适合什么场景?while 用于"满足条件就继续"(如逐行处理),until 用于"不满足就等"(如轮询服务状态)。until curl -s localhost:8080 >/dev/null; do sleep 1; done 比 while ! curl … 更直观。4. 怎么控制循环的并发数?用计数器+wait 控制后台任务数:每启动 N 个后台任务执行一次 wait,等上一批完成再继续。或用 GNU parallel / xargs -P 等工具。5. for 循环中 {1..$n} 为什么不生效?{} 范围展开在变量替换之前执行,所以 {1..$n} 不会展开。应改用 C 风格 for ((i=1;i<=n;i++)) 或 seq 1 $n。写段代码# 安全遍历文件 + while 逐行处理for f in /var/log/*.log; do [ -f "$f" ] || continue while IFS= read -r line; do echo "${f##*/}: $line" done < "$f"done
服务端阅读 05月29日 01:19

OpenCV.js 中常用的图像处理操作有哪些?

OpenCV.js 常用的图像处理操作涵盖颜色转换、滤波、边缘检测、几何变换和阈值处理五大类。核心 API 包括:cv.cvtColor() 做颜色空间转换(如 RGBA2GRAY),cv.GaussianBlur() 和 cv.medianBlur() 做图像平滑,cv.Canny() 做边缘检测,cv.resize() 和 cv.warpAffine() 做几何变换,cv.threshold() 和 cv.adaptiveThreshold() 做二值化。所有操作前需通过 cv.imread() 从 canvas 读取图像,处理后用 cv.imshow() 输出,且每个 cv.Mat 对象必须手动调用 .delete() 释放内存。追问cv.cvtColor() 中 RGBA2GRAY 和 RGB2GRAY 有什么区别?浏览器 canvas 默认输出 RGBA 四通道,所以用 cv.imread() 读到的图像必须用 COLORRGBA2GRAY 而非 COLORRGB2GRAY,否则通道数不匹配会报错。高斯模糊的核大小为什么必须是奇数?奇数核保证高斯函数有明确的中心点,OpenCV 强制要求 ksize 的宽高都为正奇数,否则抛异常。核越大模糊越强,但计算量也越大。cv.threshold() 和 cv.adaptiveThreshold() 分别适合什么场景?全局阈值适合光照均匀的图像,自适应阈值适合光照不均的场景(如阴影下的文档),后者对每个像素根据邻域计算局部阈值。形态学操作中开运算和闭运算的区别是什么?开运算(先腐蚀后膨胀)去除小噪点,闭运算(先膨胀后腐蚀)填充小孔洞,选择取决于目标是去噪还是补洞。为什么 OpenCV.js 中 Mat 对象必须手动 delete?OpenCV.js 通过 WebAssembly 在堆上分配内存,JavaScript 的 GC 无法回收 WASM 堆内存,不 delete 就会造成内存泄漏,视频处理循环中尤其致命。写段代码let src = cv.imread('canvasInput');let gray = new cv.Mat(), dst = new cv.Mat();cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);cv.GaussianBlur(gray, gray, new cv.Size(5, 5), 0);cv.Canny(gray, dst, 50, 100);cv.imshow('canvasOutput', dst);src.delete(); gray.delete(); dst.delete();
服务端阅读 05月29日 01:19

Shell 脚本中常用的字符串操作有哪些?

Shell 字符串操作全部通过 ${} 参数展开完成,无需外部命令。拼接直接并排书写即可:"$a$b" 或 "${a}_${b}"。长度用 ${#var}。截取用 ${var:offset:length},支持负偏移从末尾取。最常用的模式删除:${var#pattern} 删最短前缀、${var##pattern} 删最长前缀、${var%pattern} 删最短后缀、${var%%pattern} 删最长后缀——这是提取文件名、路径、扩展名的标准做法。替换用 ${var/pattern/replacement} 替首次、${var//pattern/replacement} 替全部。追问1. # 和 ## 删除前缀有什么区别?% 和 %% 呢?从左删最短匹配,## 删最长匹配。以 path=/a/b/c.txt 为例,${path#/} 得 a/b/c.txt(删到第一个 /),${path##/} 得 c.txt(删到最后一个 /)。% 和 %% 从右删,逻辑对称。记忆:# 在键盘左边删左边,% 在右边删右边。2. ${var:offset:length} 的 offset 是从 0 还是 1 开始?负数偏移要注意什么?offset 从 0 开始。负偏移表示从末尾计数,但冒号后必须加空格:${var: -3} 取末尾3个字符,不加空格会被误认为默认值语法 ${var:-3}。3. 怎么提取文件路径中的目录、文件名和扩展名?dir=${path%/}、name=${path##/}、ext=${path##.}、base=${name%.}。这比 dirname/basename 命令更快,因为不需要 fork 子进程。4. 字符串替换能用正则吗?${var/pattern/replacement} 的 pattern 是 glob 通配符(* ? [abc]),不是正则。需要正则匹配要用 [[ $var =~ regex ]] 配合 BASH_REMATCH 数组,或调用 sed/awk。写段代码path="/data/logs/app.access.log"filename=${path##*/} # app.access.logdir=${path%/*} # /data/logsbase=${filename%.*} # app.accessext=${filename##*.} # logecho "$dir / $base / $ext"
服务端阅读 05月29日 01:19

Shell 脚本中如何定义函数?参数传递和返回值怎么处理?

Shell 函数用 name() { } 或 function name { } 定义,调用时直接写函数名。参数通过 $1、$2、$@ 等位置变量访问,不写在括号里。返回值有两个机制:return 只能返回 0-255 的退出码,用于表示成功或失败;要返回字符串或计算结果,需用 echo 输出后由调用方通过 $(func) 命令替换捕获。函数内变量默认是全局的,必须用 local 声明才能限定作用域,这是 Shell 和大多数语言的重要区别。追问1. return 和 echo 返回值有什么本质区别?return 设置的是函数的退出状态码(0-255),只能用于条件判断(if func; then)。echo 输出到 stdout,由 $(func) 捕获后可当字符串使用。两者常配合:echo 返回结果,return 返回状态。2. $@ 和 $* 有什么区别?什么时候加引号?不加引号时两者相同,展开为独立单词。加引号后 "$@" 保留每个参数的边界("$1" "$2" …),"$*" 把所有参数合并为一个字符串("$1 $2 …")。循环遍历参数时始终用 "$@"。3. 函数中不用 local 会怎样?变量变成全局的,会污染外部作用域甚至覆盖同名变量。这在调试时极难排查,是 Shell 脚本 bug 的常见来源。建议所有函数内变量都加 local。4. 如何在另一个脚本中复用函数?将函数写在独立文件中,用 source 或 . 命令导入。也可以用 export -f func_name 将函数导出给子进程,但只对 bash 子进程有效。5. 函数能递归调用吗?有什么限制?可以递归,但 Shell 没有尾递归优化,深度递归会很快耗尽栈空间。实际中递归深度超过几百层就会报错,复杂数据结构建议用其他语言。写段代码# return 退出码 + echo 返回值grep_key() { local file=$1 key=$2 grep -q "$key" "$file" || return 1 grep "$key" "$file" | head -1}result=$(grep_key /etc/hosts localhost)if [ $? -eq 0 ]; then echo "Found: $result"fi
服务端阅读 05月29日 01:18

Shell 重定向和管道的工作原理是什么?

Shell 通过文件描述符(FD)管理数据流:stdin(0) 读入、stdout(1) 正常输出、stderr(2) 错误输出。重定向改变数据流向,> 覆盖写、>> 追加写,2> 重定向错误,2>&1 将 stderr 合并到 stdout,&> 是 Bash 4+ 的简写。管道 | 将左侧 stdout 传给右侧 stdin,但 stderr 不经过管道——需先用 2>&1 转换。/dev/null 是黑洞设备,丢弃输出用 > /dev/null 2>&1。Here Document(<<) 和 Here String(<<<) 用于内联输入,进程替换 <() 让两个命令的输出直接比较而无需临时文件。追问管道为什么只传 stdout?如何让 stderr 也通过管道?管道连接的是 FD1→FD0,stderr 走 FD2 所以被丢弃。用 cmd 2>&1 | grep err 先将 FD2 合并到 FD1 即可。set -o pipefail 有什么用?默认管道返回最后一个命令的退出码,中间命令失败会被忽略。pipefail 使管道任一命令失败就返回非零,适合脚本严格错误检查。进程替换 管道是进程间 stdin/stdout 直连,只能单流向;<() 将命令输出映射为临时文件路径,支持多个输入源同时使用,如 diff <(cmd1) <(cmd2)。exec 3> file.txt 是什么用法?exec 打开自定义 FD,后续通过 >&3 写入、&- 关闭。适合脚本中多次读写同一文件避免反复打开。写段代码# 分别记录输出和错误,同时用管道过滤错误./deploy.sh 1>deploy.log 2>&1 | grep -i "error"# 进程替换对比两个目录if ! diff <(ls dir1) <(ls dir2); then echo "目录内容有差异"fi
服务端阅读 05月29日 01:18

什么是 Shell?常见的 Shell 类型有哪些?

Shell 是用户与操作系统内核之间的命令解释器,负责将用户输入的命令翻译给内核执行并返回结果。它同时也是一种脚本语言,可以将命令序列写入文件批量执行。Linux 默认 Shell 通常是 bash,macOS 从 Catalina 起默认切换为 zsh。生产环境中需关注兼容性:写可移植脚本时应以 POSIX sh 为基准,避免使用 bashism。Debian/Ubuntu 的 /bin/sh 实际指向 dash,执行速度比 bash 快但不支持其扩展语法。fish 交互体验好但语法不兼容 POSIX,不适合写通用脚本。追问1. /bin/sh 和 /bin/bash 有什么区别?为什么要区分?/bin/sh 是 POSIX 标准定义的 Shell,/bin/bash 在其基础上扩展了数组、[[ ]]、(( )) 等特性。脚本首行写 #!/bin/sh 意味着只使用 POSIX 语法,保证跨平台可移植;写 #!/bin/bash 则可以使用 bash 扩展但牺牲了可移植性。2. 怎么查看当前系统使用的 Shell?怎么临时切换?echo $SHELL 查看默认 Shell,echo $0 查看当前 Shell。临时切换直接输入 bash 或 zsh 即可,永久切换用 chsh -s /bin/zsh。3. zsh 相比 bash 有哪些优势?为什么 macOS 要切换?zsh 支持更强大的 Tab 补全(命令参数、文件路径)、右侧提示符、插件生态(oh-my-zsh)和递归通配符 */.sh。macOS 切换主因是 bash 3.2 受 GPL v3 许可限制无法升级,zsh 采用 MIT 许可更灵活。4. dash 为什么比 bash 快?适合什么场景?dash 代码量小、不加载交互功能,启动速度快约 4 倍。适合作为 /bin/sh 执行系统启动脚本(如 /etc/init.d/),不适合交互使用。写段代码#!/bin/sh# 检测当前 Shell 并给出建议case "$SHELL" in */bash) echo "当前: bash" ;; */zsh) echo "当前: zsh" ;; */dash) echo "当前: dash" ;; *) echo "当前: $SHELL" ;;esac
服务端阅读 05月29日 01:09

Mongoose 聚合管道有哪些常用阶段?$match 为什么放最前面?

Mongoose 聚合通过 Model.aggregate([...stages]) 执行管道,常用阶段:$match 过滤文档、$group 分组统计($sum 计数/求和、$avg 均值、$push 收集数组)、$project 投影和计算新字段、$sort 排序、$limit/$skip 分页、$lookup 关联其他集合(类似 SQL JOIN)、$unwind 展开数组为多条记录、$facet 并行执行多个子管道。$match 必须放最前面,因为它能利用索引减少进入后续阶段的数据量,管道是顺序执行的,越早过滤性能越好。$lookup 是左外连接,localField/foreignField 匹配或 pipeline 子查询方式,结果以数组形式放入 as 指定字段。追问$lookup 的 pipeline 子查询方式和 localField/foreignField 有什么区别? pipeline 方式支持在关联时加 $match、$project 等阶段过滤和塑形关联数据,更灵活且减少返回量;localField/foreignField 只做等值连接,无法过滤。pipeline 还支持 let 变量引用主文档字段。$unwind 会导致数据膨胀怎么办? 对大数组 unwind 再 group 会产生大量中间文档。替代方案:用 $reduce 或 $map 在数组上直接聚合,或在 unwind 前用 $project 只保留需要的字段减少每条文档体积。$facet 有什么限制? 各子管道不能引用其他子管道的结果,也不能包含 $out 或 $merge 阶段。facet 内每个子管道独立处理同一份输入数据,内存消耗是各子管道之和。聚合结果超过 100MB 内存限制怎么办? 加 allowDiskUse: true 选项让中间结果写入临时文件,或优化管道尽早 $match + $project 缩减数据量。写段代码const stats = await Order.aggregate([ { $match: { createdAt: { $gte: new Date('2025-01-01') } } }, { $group: { _id: '$productId', total: { $sum: '$amount' }, count: { $sum: 1 } } }, { $sort: { total: -1 } }, { $limit: 10 }]);
服务端阅读 05月29日 01:09

如何配置 Cypress 测试报告和 CI/CD 集成?

Cypress 测试报告配置分两步:选 reporter、配参数。最常用的是 Mochawesome,在 cypress.config.js 中设 reporter 为 'mochawesome',通过 reporterOptions 指定 reportDir、overwrite: false、html: true、chart: true。如需合并多个 spec 的报告,搭配 mochawesome-merge 工具合并 JSON 再生成单份 HTML。CI/CD 集成的关键是:用 npx cypress run --reporter mochawesome 在无头模式执行;通过 --parallel 参数配合 Cypress Cloud 实现并行测试加速;用 actions/upload-artifact 收集报告和失败时的截图/视频;在 workflow 触发条件中绑定 push/pull_request 事件。失败截图和视频默认保存在 cypress/screenshots 和 cypress/videos 目录,CI 中应作为 artifact 上传以便排查。追问mochawesome-merge 的作用是什么?为什么多个 spec 会生成多份报告?Cypress 的 --parallel 参数如何工作?不使用 Cypress Cloud 能实现并行吗?如何在 CI 中只在测试失败时才上传视频和截图?Allure 报告和 Mochawesome 相比各有什么优劣?什么场景该选 Allure?如何在 GitHub Actions 中设置定时跑 Cypress 测试(cron 触发)?写段代码# .github/workflows/cypress.ymlname: Cypresson: [push]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npx cypress run --reporter mochawesome - uses: actions/upload-artifact@v4 if: always() with: name: report path: cypress/results
服务端阅读 05月29日 01:09

Mongoose 鉴别器 Discriminator 怎么用?

Discriminator 是 Mongoose 的单集合继承机制,通过 Event.discriminator('Conference', confSchema) 在同一集合中存储不同结构的文档,用 discriminatorKey 字段区分类型。所有鉴别器共享基础 Schema 的字段和索引,各自扩展独有字段,适合多态数据模型。追问discriminatorKey 的作用是什么?它是 MongoDB 文档中用于标识类型的字段,默认叫 __t,可在基础 Schema 选项中自定义(如 kind、role)。创建鉴别器文档时自动写入,查询时 Mongoose 据此实例化正确的模型。基础 Schema 的中间件会作用于鉴别器吗?会。基础 Schema 的 pre/post 中间件对所有鉴别器生效,鉴别器自身的中间件只作用于该类型。利用这点可在基础 Schema 统一处理公共逻辑(如日志),鉴别器处理特有校验。嵌套鉴别器怎么实现?在子文档数组上调用 .discriminator():batchSchema.discriminator('ProductBatch', productBatchSchema),数组中的不同元素可以有不同的 Schema 结构,discriminatorKey 自动区分。鉴别器有什么局限?所有类型存在同一集合,无法对鉴别器特有字段建独立索引(索引会覆盖全集合);文档大小差异大时浪费存储;删除某类型文档需按 discriminatorKey 过滤;单集合数据量膨胀后影响所有类型的查询性能。鉴别器适合哪些场景?事件日志系统(Click/View/Purchase 共享时间戳基类)、内容管理(Article/Video/Image 共享标题和作者)、多角色用户(Admin/Customer 共享认证字段)。共同点:字段大量重叠、需要跨类型统一查询。写段代码const eventSchema = new Schema({ name: String, date: Date}, { discriminatorKey: 'kind' });const Event = mongoose.model('Event', eventSchema);const Conf = Event.discriminator('Conf', new Schema({ speakers: [String] }));await Conf.create({ name: 'JSConf', date: new Date(), speakers: ['Tom']});// MongoDB: { kind: 'Conf', name: 'JSConf', ... }
服务端阅读 05月29日 01:09

Mongoose Model 的 CRUD 方法有哪些,查询性能怎么优化?

Mongoose Model 常用 CRUD 方法:创建用 create() 和 insertMany();查询用 find()、findOne()、findById();更新用 updateOne()、findByIdAndUpdate(id, update, {new: true});删除用 deleteOne()、findByIdAndDelete()。链式查询支持 .select() 投影、.sort() 排序、.limit() 分页、.populate() 联查。findByIdAndUpdate 默认返回修改前文档,需传 {new: true} 获取更新后数据。追问find() 返回的结果和 lean() 有什么区别?find() 返回 Mongoose Document 实例,支持修改后 save()、虚拟字段、中间件;lean() 返回纯 JS 对象,内存占用低、查询速度快约 1.5-3 倍。只读展示场景务必用 lean(),需要修改并保存时才用 Document 实例。findByIdAndUpdate 能触发 save 中间件吗?不能。pre('save') 和 post('save') 只在 doc.save() 时触发。updateOne/findByIdAndUpdate 触发的是 pre('updateOne') 和 pre('findOneAndUpdate') 钩子。这是面试高频陷阱。批量操作用 insertMany 还是 bulkWrite?insertMany 只支持批量插入;bulkWrite 支持 insertOne/updateOne/deleteOne 混合操作,且底层走 MongoDB 的 bulk 协议,单次网络往返完成,性能更优。混合写操作场景优先 bulkWrite。countDocuments 和 estimatedDocumentCount 有什么区别?countDocuments 执行真实 COUNT 查询,支持过滤条件,结果精确;estimatedDocumentCount 读集合元数据估算,不支持条件,速度极快。无过滤条件的计数场景用 estimatedDocumentCount 更高效。写段代码// 批量混合操作await User.bulkWrite([ { updateOne: { filter: { name: 'Tom' }, update: { $inc: { age: 1 } } } }, { insertOne: { document: { name: 'Jerry', age: 20 } } }, { deleteOne: { filter: { name: 'Old' } } }]);// 只读查询用 leanconst users = await User.find({ age: { $gte: 18 } }) .select('name age').sort({ age: -1 }).lean();
前端阅读 05月29日 01:09

Web3 前端如何与后端服务协作?有哪些典型场景?

Web3 前端与后端的协作围绕链上和链下两条数据通路展开。链上交互通过钱包连接(window.ethereum)直接调用智能合约的 view/pure 方法读取状态、通过用户签名发送交易;链下交互则走传统 REST/GraphQL API,由后端代理聚合数据、管理会话、处理敏感逻辑。典型场景有五个:一是钱包身份验证——前端获取钱包地址并签名消息,后端验证签名后签发 JWT;二是读取合约状态——前端直接调用 view 函数或通过后端缓存聚合;三是发送交易——前端构造交易参数由用户在钱包确认签名,后端监听链上事件确认结果;四是事件监听——后端订阅合约事件(Transfer、Approval 等)通过 WebSocket 推送前端;五是链上数据索引——使用 The Graph 等索引服务将链上事件转为可查询的 GraphQL API,避免前端直接扫描区块。追问前端直接调用合约 view 函数和通过后端代理读取各有什么优劣?何时选哪种?用户签名消息的 EIP-712 标准是什么?比普通个人签名好在哪里?The Graph 的工作原理是什么?subgraph 如何定义和部署?如何处理后端服务宕机时前端的降级策略?能否直接切换到 RPC 节点?多链 DApp 中如何管理不同链的 provider 和合约实例?写段代码// 前端连接钱包并签名验证const accounts = await window.ethereum .request({ method: 'eth_requestAccounts' });const signer = new ethers.BrowserProvider( window.ethereum).getSigner();const signature = await signer .signMessage('login-nonce-123');// 将 address + signature 发给后端验证