从零部署国产高精度OCR:DeepSeek-OCR-WEBUI集成实践指南

从零部署国产高精度OCR:DeepSeek-OCR-WEBUI集成实践指南

在数字化办公和智能文档处理日益普及的今天,高效、精准的OCR(光学字符识别)能力已成为企业自动化流程中的关键一环。尤其对于中文场景下的复杂文本识别——如票据、表格、手写体等——传统OCR工具往往力不从心。

而近期由DeepSeek开源推出的DeepSeek-OCR-WEBUI镜像,为这一难题提供了极具竞争力的国产化解决方案。它不仅具备强大的多语言识别能力,更在中文长文本、结构化内容(如表格与公式)识别上表现出色,且支持一键部署、Web交互与OpenAI协议兼容调用。

本文将带你从零开始完整部署 DeepSeek-OCR-WEBUI,涵盖环境准备、服务启动、前端使用及API集成全过程,确保即使你是AI新手也能快速上手,真正实现“本地运行、即插即用”的高精度OCR体验。


1. 为什么选择 DeepSeek-OCR?

在介绍如何部署之前,先来看看这款模型究竟解决了哪些痛点:

  • 中文识别精度高:针对中文排版、字体、语义优化,远超通用OCR引擎。
  • 复杂场景鲁棒性强:倾斜、模糊、低分辨率图像仍能准确提取文字。
  • 结构还原能力强:可保留标题层级、列表、代码块、数学公式等格式信息。
  • 轻量化部署:单张显卡(如4090D)即可运行,适合本地或边缘设备。
  • 双模式访问:既可通过网页直接操作,也支持程序化API调用。
  • 完全开源可控:无数据外泄风险,适用于金融、政务等敏感领域。
简而言之:如果你需要一个中文强、速度快、格式保真度高、本地可运行的OCR系统,DeepSeek-OCR 是目前最值得尝试的选择之一。

2. 部署前准备:软硬件要求与依赖安装

2.1 硬件建议

组件推荐配置
GPUNVIDIA RTX 4090D 或同等性能及以上显卡(显存 ≥ 24GB)
CPU多核处理器(Intel i7 / AMD Ryzen 7 及以上)
内存≥ 32GB
存储≥ 50GB 可用空间(含模型缓存)
注:若仅用于测试,也可在CPU模式下运行,但推理速度会显著下降。

2.2 软件环境

  • 操作系统:Linux(Ubuntu 20.04+)或 Windows WSL2
  • Python版本:3.12+
  • 包管理工具:推荐使用 condavenv

2.3 创建独立虚拟环境并安装依赖

# 创建虚拟环境 conda create -n deepseekocr python=3.12.9 conda activate deepseekocr # 安装核心依赖 pip install torch==2.6.0 transformers==4.46.3 tokenizers==0.20.3 \ einops addict easydict python-multipart uvicorn fastapi \ Pillow torchvision requests 
提示:若你的GPU支持Flash Attention,可额外安装 flash-attn 以提升推理效率并降低显存占用。

3. 项目结构搭建:组织代码与静态资源

我们采用简洁清晰的目录结构来管理整个OCR服务:

deepseek-ocr-project/ ├── app.py # FastAPI后端主程序 ├── static/ │ └── ui.html # 前端网页界面 └── README.md # 说明文档(可选) 

你可以通过以下命令快速创建该结构:

mkdir -p deepseek-ocr-project/static cd deepseek-ocr-project touch app.py 

接下来我们将分别填充 app.pyui.html 文件内容。


4. 后端服务搭建:基于 FastAPI 的 OpenAI 兼容接口

我们将使用 FastAPI 构建一个兼容 OpenAI 协议的OCR服务,使其不仅能被网页调用,还能无缝接入现有AI工作流。

4.1 编写 app.py 主程序

将以下完整代码保存至 app.py

import os import time import uuid import base64 import tempfile import mimetypes import logging from typing import Any, Dict, List, Optional, Tuple from urllib.parse import urlparse import requests import torch from fastapi import FastAPI, File, UploadFile, Form, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, HTMLResponse from fastapi.staticfiles import StaticFiles from transformers import AutoModel, AutoTokenizer # ---------------- logging ---------------- logging.basicConfig(level=logging.INFO) log = logging.getLogger("ocr-api") # ---------------- app & CORS ------------- app = FastAPI(title="Transformers模型服务 (OpenAI-Compatible)") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 静态目录(用于放置你的 ui.html) STATIC_DIR = os.getenv("STATIC_DIR", "static") os.makedirs(STATIC_DIR, exist_ok=True) app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") # 便捷入口:/ui -> /static/ui.html(可选) @app.get("/ui") async def ui_redirect(): html = '<meta http-equiv="refresh" content="0; url=/static/ui.html" />' return HTMLResponse(content=html, status_code=200) # ---------------- model load ------------- os.environ.setdefault("CUDA_VISIBLE_DEVICES", "0") MODEL_NAME = os.getenv("DEEPSEEK_OCR_PATH", "deepseek-ai/DeepSeek-OCR") # 支持本地路径或HuggingFace ID OPENAI_MODEL_ID = "deepseek-ocr" tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) model = AutoModel.from_pretrained( MODEL_NAME, trust_remote_code=True, use_safetensors=True, ) # 设备与精度设置 if torch.cuda.is_available(): device = torch.device("cuda:0") model = model.eval().to(device) try: model = model.to(torch.bfloat16) except Exception: try: model = model.to(torch.float16) log.info("BF16 不可用,已回退到 FP16") except Exception: model = model.to(torch.float32) log.info("FP16 不可用,已回退到 FP32") else: device = torch.device("cpu") model = model.eval().to(device) log.warning("未检测到 CUDA,将在 CPU 上推理。") # ---------------- helpers ---------------- def _now_ts() -> int: return int(time.time()) def _gen_id(prefix: str) -> str: return f"{prefix}_{uuid.uuid4().hex[:24]}" def _save_bytes_to_temp(data: bytes, suffix:) -> str: tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) tmp.write(data) tmp.flush() tmp.close() return tmp.name def _is_data_uri(url: str) -> bool: return isinstance(url, str) and url.startswith("data:") def _is_local_like(s: str) -> bool: if not isinstance(s, str): return False if s.startswith("file://"): return True parsed = urlparse(s) if parsed.scheme in ("http", "https", "data"): return False return True def _to_local_path(s: str) -> str: if s.startswith("file://"): return s[7:] return os.path.expanduser(s) def _download_to_temp(url: str) -> str: if not isinstance(url, str) or not url.strip(): raise HTTPException(status_code=400, detail="Empty image url") # 1) data: URI if _is_data_uri(url): try: header, b64 = url.split(",", 1) ext = ".bin" if "image/png" in header: ext = ".png" elif "image/jpeg" in header or "image/jpg" in header: ext = ".jpg" elif "image/webp" in header: ext = ".webp" raw = base64.b64decode(b64) path = _save_bytes_to_temp(raw, suffix=ext) log.info(f"[image] data-uri -> {path}") return path except Exception as e: raise HTTPException(status_code=400, detail=f"Invalid data URI: {e}") # 2) 本地文件 if _is_local_like(url): p = _to_local_path(url) if not os.path.isabs(p): p = os.path.abspath(p) if not os.path.isfile(p): raise HTTPException(status_code=400, detail=f"Local file not found or not a file: {p}") ext = os.path.splitext(p)[1] or ".img" try: with open(p, "rb") as f: data = f.read() except Exception as e: raise HTTPException(status_code=400, detail=f"Read local file failed: {p} ({e})") path = _save_bytes_to_temp(data, suffix=ext) log.info(f"[image] local -> {p} -> {path}") return path # 3) http(s) try: log.info(f"[image] http(s) -> {url}") resp = requests.get(url, timeout=30) resp.raise_for_status() ctype = resp.headers.get("Content-Type", "") ext = mimetypes.guess_extension(ctype) or ".img" path = _save_bytes_to_temp(resp.content, suffix=ext) log.info(f"[image] http(s) saved -> {path}") return path except Exception as e: raise HTTPException(status_code=400, detail=f"Download image failed: {e}") def _extract_text_and_first_image_from_messages(messages: List[Dict[str, Any]]) -> Tuple[str, Optional[str]]: all_text: List[str] = [] image_path: Optional[str] = None for msg in messages: content = msg.get("content") if content is None: continue if isinstance(content, str): all_text.append(content) continue if isinstance(content, list): for part in content: ptype = part.get("type") if ptype in ("text", "input_text"): txt = part.get("text", "") if isinstance(txt, str) and txt.strip(): all_text.append(txt) elif ptype in ("image_url", "input_image"): if image_path is None: image_field = part.get("image_url") or part.get("image") url = image_field.get("url") if isinstance(image_field, dict) else image_field if not url or not isinstance(url, str): raise HTTPException(status_code=400, detail="image_url is missing or invalid") image_path = _download_to_temp(url) prompt = "\n".join([t for t in all_text if t.strip()]) if all_text else "" return prompt, image_path def _run_ocr_infer(prompt: str, image_path: str) -> str: full_prompt = f"<image>\n{prompt}".strip() try: res = model.infer( tokenizer, prompt=full_prompt, image_file=image_path, output_path="./save", base_size=1024, image_size=640, crop_mode=True, save_results=False, test_compress=True, eval_mode=True, ) except Exception as e: raise HTTPException(status_code=500, detail=f"Infer failed: {e}") if isinstance(res, dict): for key in ("text", "result", "output", "ocr_text"): if key in res and isinstance(res[key], str): return res[key] return str(res) if isinstance(res, (list, tuple)): return "\n".join(map(str, res)) return str(res) def _token_count_approx(text: str) -> int: try: return len(tokenizer.encode(text)) except Exception: return max(1, len(text) // 4) # ---------------- routes ---------------- @app.get("/health") async def health_check(): return {"status": "healthy"} @app.post("/parserToText") async def parser_to_text(file: UploadFile = File(...), content: str = Form(...)): file_bytes = await file.read() suffix = os.path.splitext(file.filename or "")[1] or ".img" tmp_path = _save_bytes_to_temp(file_bytes, suffix=suffix) prompt = "<image>\n" + (content or "") try: res = model.infer( tokenizer, prompt=prompt, image_file=tmp_path, output_path="./save", base_size=1024, image_size=640, crop_mode=True, save_results=False, test_compress=True, eval_mode=True, ) return res except Exception as e: return {"status": "error", "message": str(e)} finally: if os.path.exists(tmp_path): try: os.unlink(tmp_path) except Exception: pass @app.get("/v1/models") async def list_models(): return { "object": "list", "data": [{"id": OPENAI_MODEL_ID, "object": "model", "created": _now_ts(), "owned_by": "owner"}], } @app.post("/v1/chat/completions") async def chat_completions(request: Request): payload = await request.json() messages = payload.get("messages") if not isinstance(messages, list) or not messages: raise HTTPException(status_code=400, detail="`messages` must be a non-empty list") prompt_text, image_path = _extract_text_and_first_image_from_messages(messages) if not image_path: raise HTTPException(status_code=400, detail="No image found in messages. Provide content with type='image_url'.") try: answer = _run_ocr_infer(prompt_text, image_path) finally: if image_path and os.path.exists(image_path): try: os.unlink(image_path) except Exception: pass prompt_tokens = _token_count_approx(prompt_text) completion_tokens = _token_count_approx(answer) return JSONResponse({ "id": _gen_id("chatcmpl"), "object": "chat.completion", "created": _now_ts(), "model": OPENAI_MODEL_ID, "choices": [{"index": 0, "message": {"role": "assistant", "content": answer}, "finish_reason": "stop"}], "usage": {"prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": prompt_tokens + completion_tokens}, }) # 根路由 @app.get("/") async def root(): return {"service": "OpenAI-Compatible OCR Service", "model": OPENAI_MODEL_ID, "ui": "/static/ui.html"} # ---------------- main ---------------- if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001) 

5. 前端页面开发:构建简洁易用的 Web UI

5.1 创建 static/ui.html

static/ 目录下新建 ui.html 文件,并粘贴以下HTML代码:

<!doctype html> <html lang="zh"> <head> <meta charset="utf-8"> <title>DeepSeek-OCR • Web UI</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> :root { --bg:#0b1220; --fg:#e6edf3; --muted:#9aa4b2; --acc:#49b5ff; --card:#111a2e; --ok:#2ecc71; --err:#ff6b6b; } * { box-sizing: border-box; } body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial; background:var(--bg); color:var(--fg); } .wrap { max-width: 1000px; margin: 32px auto; padding: 0 16px; } h1 { font-weight:700; margin: 0 0 6px; } p.desc { color:var(--muted); margin: 0 0 16px; } .card { background:var(--card); border-radius:16px; padding:16px; box-shadow: 0 10px 30px rgba(0,0,0,.25); margin-bottom:16px; } .row { display:flex; gap:16px; flex-wrap:wrap; } .col { flex:1 1 360px; min-width:320px; } label { font-size:14px; color:var(--muted); display:block; margin:6px 0; } input[type="text"], textarea, select { width:100%; background:#0e1627; color:var(--fg); border:1px solid #1e2b44; border-radius:12px; padding:10px 12px; outline:none; font-size:14px; } textarea { min-height:120px; resize:vertical; } .btn { background:var(--acc); color:#001224; border:none; border-radius:12px; padding:10px 16px; font-weight:700; cursor:pointer; } .btn:disabled { opacity:.6; cursor:not-allowed; } .pill { display:inline-block; background:#0e1627; border:1px dashed #1e2b44; color:var(--muted); border-radius:999px; padding:6px 10px; font-size:12px; } #preview { max-width:100%; max-height:260px; border-radius:12px; border:1px solid #1e2b44; display:none; margin-top:8px; } .out { white-space:pre-wrap; background:#0e1627; border:1px solid #1e2b44; border-radius:12px; padding:12px; min-height:140px; } .tabs { display:flex; gap:8px; margin-top:8px; } .tabs button { background:#0e1627; color:var(--muted); border:1px solid #1e2b44; border-radius:10px; padding:6px 10px; cursor:pointer; } .tabs button.active { color:var(--fg); border-color:var(--acc); } a { color: var(--acc); text-decoration: none; } .row-compact { display:flex; gap:8px; align-items:center; flex-wrap:wrap; } .muted { color:var(--muted); font-size:12px; } </style> </head> <body> <div> <h1>DeepSeek-OCR Web UI</h1> <p>上传图片 + 输入提示,直接调用后端 <code>/v1/chat/completions</code>。默认预设:<span>返回 Markdown 识别结果</span></p> <div> <div> <div> <label>图片文件</label> <input type="file" accept="image/*"> <img alt="preview"> <div>前端会把图片转为 <code>data:</code> Base64 发送到后端。</div> </div> <div> <label>预设指令</label> <select> <option value="md" selected>返回 Markdown 识别结果(保留标题/列表/表格/代码块)</option> <option value="plain">返回纯文本(仅文字内容,去版式)</option> <option value="json">返回 JSON 结构:{title, paragraphs, tables[], figures[]}</option> </select> <label>自定义提示(可选,会拼接到预设后面)</label> <textarea placeholder="例如:表格务必用标准 Markdown 表格语法;公式用 $...$;图片题注前缀用 Figure:"></textarea> <div> <button>识别并生成</button> <span>就绪</span> </div> <div> 接口地址:<code>/v1/chat/completions</code>(同源部署可直接使用) </div> </div> </div> </div> <div> <div> <button>原始文本</button> <button>Markdown 预览</button> </div> <div></div> <div></div> </div> <div>API: <a href="/v1/models" target="_blank">/v1/models</a> · <a href="/health" target="_blank">/health</a></div> </div> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script> const fileEl = document.getElementById('file'); const preview = document.getElementById('preview'); const presetEl = document.getElementById('preset'); const promptEl = document.getElementById('prompt'); const runBtn = document.getElementById('run'); const statusEl = document.getElementById('status'); const rawEl = document.getElementById('raw'); const mdEl = document.getElementById('md'); const tabRaw = document.getElementById('tab-raw'); const tabMd = document.getElementById('tab-md'); function endpoint() { return '/v1/chat/completions'; } function presetText(key) { if (key === 'plain') { return "请输出纯文本的 OCR 结果,仅保留文字内容,去掉所有版式与装饰符号。"; } else if (key === 'json') { return "请以 JSON 返回 OCR 结果,字段为 {title, paragraphs, tables: [markdown_table], figures: [caption]},不要解释说明。"; } return "请以 Markdown 返回 OCR 结果,尽量还原版式:使用 # 标题、无序/有序列表、```代码块、表格用标准 Markdown 表格语法;无法识别的片段用 [UNCERTAIN] 标记。"; } function fileToDataURI(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(new Error('读取文件失败')); reader.onload = () => resolve(reader.result); reader.readAsDataURL(file); }); } function setTab(which) { if (which === 'raw') { tabRaw.classList.add('active'); tabMd.classList.remove('active'); rawEl.style.display = 'block'; mdEl.style.display = 'none'; } else { tabMd.classList.add('active'); tabRaw.classList.remove('active'); mdEl.style.display = 'block'; rawEl.style.display = 'none'; } } function setStatus(text, ok=true) { statusEl.textContent = text; statusEl.style.borderColor = ok ? '#1e2b44' : 'var(--err)'; statusEl.style.color = ok ? 'var(--muted)' : '#ffdede'; } fileEl.addEventListener('change', () => { const f = fileEl.files && fileEl.files[0]; if (!f) { preview.style.display = 'none'; return; } const url = URL.createObjectURL(f); preview.src = url; preview.style.display = 'block'; }); tabRaw.onclick = () => setTab('raw'); tabMd.onclick = () => setTab('md'); runBtn.addEventListener('click', async () => { try { const f = fileEl.files && fileEl.files[0]; if (!f) { alert('请先选择图片文件'); return; } const dataUri = await fileToDataURI(f); const preset = presetText(presetEl.value); const custom = (promptEl.value || '').trim(); const textMsg = custom ? (preset + "\n\n" + custom) : preset; const body = { model: "deepseek-ocr", messages: [ { role: "user", content: [ { type: "text", text: textMsg }, { type: "image_url", image_url: { url: dataUri } } ] } ], }; setStatus('识别中…', true); runBtn.disabled = true; rawEl.textContent = ''; mdEl.textContent = ''; const t0 = performance.now(); const resp = await fetch(endpoint(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const t1 = performance.now(); if (!resp.ok) { const errText = await resp.text(); setStatus('出错', false); rawEl.textContent = `HTTP ${resp.status}\n${errText}`; setTab('raw'); return; } const json = await resp.json(); const content = json?.choices?.[0]?.message?.content ?? ''; rawEl.textContent = content || '[空响应]'; if (window.marked && content) { mdEl.innerHTML = marked.parse(content); } else { mdEl.textContent = content; } setStatus(`完成(${((t1 - t0)/1000).toFixed(2)}s)`, true); } catch (e) { setStatus('出错', false); rawEl.textContent = String(e?.stack || e); setTab('raw'); } finally { runBtn.disabled = false; } }); </script> </body> </html> 

6. 启动服务并访问 WebUI

6.1 启动后端服务

确保你已激活虚拟环境并位于项目根目录:

python app.py 

服务默认监听 http://0.0.0.0:8001,启动成功后你会看到类似日志:

INFO: Started server process [PID] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8001 

6.2 访问 Web 界面

打开浏览器,访问:

http://localhost:8001/ui 

你将看到如下界面:

  • 左侧上传图片区域
  • 右侧选择预设指令(Markdown / 纯文本 / JSON)
  • 底部展示识别结果,支持原始文本与Markdown预览切换

点击【识别并生成】按钮,即可获得高质量OCR输出。


7. API 调用示例:Python客户端集成

除了网页使用,你还可以通过标准OpenAI SDK方式调用该服务。

7.1 安装 OpenAI Python 包

pip install openai 

7.2 调用代码示例

from openai import OpenAI client = OpenAI(base_url="http://127.0.0.1:8001/v1", api_key="sk-x") response = client.chat.completions.create( model="deepseek-ocr", messages=[ { "role": "user", "content": [ {"type": "text", "text": "请以Markdown格式输出OCR结果,保留表格与代码块"}, {"type": "image_url", "image_url": {"url": "file:///path/to/your/document.png"}} ] } ] ) print(response.choices[0].message.content) 
支持输入类型包括:data: Base64、本地路径、HTTP链接。

8. 使用技巧与常见问题

8.1 提升识别质量的小技巧

  • 优先使用高清图片:分辨率越高,识别越准。
  • 避免过度压缩JPEG:可能导致文字边缘失真。
  • 添加明确提示词:如“请识别为Markdown格式”、“保留原始段落结构”。
  • 对表格图片强调:“请用标准Markdown表格语法输出”。

8.2 常见问题排查

问题解决方案
启动时报错 ModuleNotFoundError检查是否安装了所有依赖包
图片上传后无响应查看控制台日志,确认临时文件路径权限
中文识别乱码或缺失确保使用的是官方 deepseek-ai/DeepSeek-OCR 模型
显存不足尝试关闭 flash_attention 或更换为FP32精度

9. 总结

通过本文的详细指导,你应该已经成功部署并运行了 DeepSeek-OCR-WEBUI,实现了以下目标:

  • 成功搭建本地OCR服务
  • 实现网页端可视化操作
  • 支持OpenAI协议API调用
  • 掌握实际使用技巧与调试方法

DeepSeek-OCR 不仅是当前国产OCR技术的佼佼者,更是企业级文档自动化、教育数字化、档案电子化的理想选择。其出色的中文识别能力和灵活的部署方式,让它成为替代商业OCR服务的有力竞争者。

下一步,你可以尝试将其集成进PDF批处理流水线、合同审核系统或知识库构建平台,真正发挥其价值。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 ZEEKLOG星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Read more

HTML前端也能接入大模型API:OpenAI兼容接口快速部署指南

HTML前端也能接入大模型API:OpenAI兼容接口快速部署指南 在智能应用开发日益普及的今天,越来越多开发者希望将大语言模型(LLM)的能力直接嵌入网页——比如让一个简单的HTML页面具备对话、写作甚至看图说话的功能。但现实往往令人却步:模型部署复杂、硬件要求高、前后端对接繁琐……尤其是对只熟悉JavaScript和浏览器环境的前端工程师来说,这些门槛几乎成了“技术鸿沟”。 有没有可能,不写一行后端代码,就能让一个纯静态网页调用本地大模型?答案是肯定的。借助 ms-swift 框架提供的 OpenAI 兼容接口,我们完全可以做到这一点。 设想这样一个场景:你正在开发一款企业内部的知识问答系统,出于数据安全考虑,不能使用公有云API。传统做法是搭建Node.js代理服务,把请求转发给本地模型,再处理响应返回给前端。整个流程涉及身份验证、错误重试、流式传输等多个环节,开发成本陡增。 而现在,只需一条命令启动推理服务,前端依然沿用原本调用 https://api.openai.com/v1/chat/completions 的逻辑,仅需将URL替换为 http:

【征文计划】玩转 Rokid JSAR:基于 Web 技术栈的 AR 开发环境搭建、核心 API 应用与 3D 时钟等创意项目全流程解析

【征文计划】玩转 Rokid JSAR:基于 Web 技术栈的 AR 开发环境搭建、核心 API 应用与 3D 时钟等创意项目全流程解析

【征文计划】玩转 Rokid JSAR:基于 Web 技术栈的 AR 开发环境搭建、核心 API 应用与 3D 时钟等创意项目全流程解析 前言 随着 AR 技术在消费级场景的普及,开发者对 “低门槛、高兼容” AR 开发工具需求愈发迫切,传统 AR 开发往往依赖专属引擎或复杂语法,导致 Web 开发者难以快速切入,而 Rokid 推出的 JSAR 技术,恰好打破了这一壁垒:以 “可嵌入空间的 Web 运行时” 为核心,让开发者无需学习新的开发范式,仅用 JavaScript/TypeScript 等熟悉的 Web 技术栈,就能快速开发出支持 3D 物体、

【Spring Boot开发实战手册】掌握Springboot开发技巧和窍门(十三)前端匹配界面、后端匹配WebSocket

【Spring Boot开发实战手册】掌握Springboot开发技巧和窍门(十三)前端匹配界面、后端匹配WebSocket

前言 在现代 Web 开发中,前端和后端的协作变得越来越重要,特别是在需要实时交互和数据更新的应用场景中。WebSocket 技术作为一种全双工通信协议,使得前端和后端之间的实时数据传输变得更加高效和稳定。本篇博客将会探讨如何设计和实现一个实时匹配系统,其中前端负责展示用户界面并与后端进行交互,而后端则通过 WebSocket 协议来处理数据通信。 前端 onMounted: 当组件被挂载的时候执行的函数 onUnmonted: 当组件被卸载的时候执行的函数 初步调试阶段,我们是将token传进user.id的 store/pk.js: import ModuleUser from'./user'exportdefault{state:{socket:null,//ws链接opponent_username:"",opponent_photo:"",status:"matching",//matching表示匹配界面,playing表示对战界面},getters:

什么是 Session?Web 开发中 Session 的使用与注意事项

什么是 Session?Web 开发中 Session 的使用与注意事项

✅ 引言 在 Web 开发中,HTTP 协议是无状态的,这意味着每次请求之间没有关联。为了实现用户登录、购物车、权限控制等功能,服务器需要一种机制来“记住”用户。Session(会话) 就是解决这一问题的核心技术之一。 本文将深入讲解: * 什么是 Session? * Session 的工作原理 * 在 Java Web 和 Spring Boot 中如何使用 Session * 使用 Session 的最佳实践与常见注意事项 * 安全风险与应对策略 并提供完整的 Java + Spring Boot 示例代码,帮助你全面掌握 Session 的使用。 📌 一、什么是 Session? 1.1 基本定义 Session(会话)是服务器端用于保存用户状态的一种机制。