diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e4aa43c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +data/ +.git/ +.gitignore +*.log +__pycache__/ +.vscode/ +.DS_Store +node_modules/ +*.tmp +*.swp diff --git a/.trae/documents/国内部署加速方案(腾讯云).md b/.trae/documents/国内部署加速方案(腾讯云).md new file mode 100644 index 0000000..4ef3057 --- /dev/null +++ b/.trae/documents/国内部署加速方案(腾讯云).md @@ -0,0 +1,42 @@ +## 问题原因 +- 国内访问官方 `pypi.org`、Docker Hub 和部分 CDN 较慢,导致镜像构建阶段的依赖下载成为瓶颈。 + +## 加速策略 +### 1) Python 依赖镜像 +- 在镜像内配置 `pip` 国内源:腾讯云镜像或清华/阿里云镜像。 +- 建议:`PIP_INDEX_URL=https://mirrors.cloud.tencent.com/pypi/simple`,并写入 `/etc/pip.conf`,保证所有 `pip install` 都走国内源。 + +### 2) Docker 镜像拉取加速 +- 在服务器上配置 Docker daemon 的国内加速镜像: +- `daemon.json` 增加:`"registry-mirrors": ["https://mirror.ccs.tencentyun.com","https://hub-mirror.c.163.com","https://docker.mirrors.ustc.edu.cn"]`,重启 Docker。 + +### 3) 预构建并推送镜像 +- 在本地/构建机上构建好镜像并推送到腾讯云 TCR(容器镜像服务),服务器仅需拉取镜像,跳过远端构建阶段的依赖下载。 + +### 4) 构建层缓存优化 +- 保持 `COPY requirements.txt` 在前,先 `pip install`,再 `COPY app static`,避免代码改动频繁导致依赖层重建。 +- 新增 `.dockerignore`(忽略 `data/`、临时文件、`.git` 等)以减少上下文,提高构建速度。 + +### 5) (可选)APT 源加速 +- 如后续需要安装系统依赖(目前没有),切换为腾讯云/清华 APT 源:`mirrors.cloud.tencent.com` 或 `mirrors.tuna.tsinghua.edu.cn`。 + +## 拟改动内容 +1. 更新 `Dockerfile`: + - 写入 `/etc/pip.conf`(`index-url` 指向腾讯云或清华镜像) + - 设置 `ENV PIP_INDEX_URL` 与 `PIP_DEFAULT_TIMEOUT` + - 保持依赖安装层的顺序与缓存稳定 +2. 新增 `.dockerignore`:忽略不必要文件,缩短构建时间 +3. 更新《部署文档.md》: + - 添加 Docker daemon 镜像加速配置步骤(腾讯云镜像) + - 添加 TCR 推送/拉取流程示例 + - 标注国内 pip 源配置与回退方案(多镜像选择) +4. 更新 README:补充“国内部署加速”小节 + +## 交付后效果 +- 服务器端 `docker compose up --build` 明显提速; +- 或直接 `docker compose up -d` 拉取预构建镜像,部署用时较短; +- 构建层缓存更稳定,代码改动不触发依赖重装。 + +## 后续可选 +- 若你使用企业私有网络(VPC)限制外网,建议固定用 TCR 方案。 +- 如需离线部署,提供 `pip wheel` 预下载与本地源(后续扩展)。 diff --git a/Dockerfile b/Dockerfile index a47d9b3..120ae0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,9 @@ FROM python:3.11-slim WORKDIR /app +RUN mkdir -p /etc +RUN echo "[global]\nindex-url = https://mirrors.cloud.tencent.com/pypi/simple\n" > /etc/pip.conf +ENV PIP_INDEX_URL=https://mirrors.cloud.tencent.com/pypi/simple \ + PIP_DEFAULT_TIMEOUT=120 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app app diff --git a/README.md b/README.md index ebcaead..809b72f 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,11 @@ - 宝塔/Nginx(可选):反代到 `http://127.0.0.1:57777` 并启用 HTTPS - 详见《部署文档.md》 +## 国内部署加速(腾讯云) +- Docker 加速:在服务器 `/etc/docker/daemon.json` 配置 `registry-mirrors`(参见《部署文档.md》) +- Python 依赖:镜像已默认使用腾讯云 PyPI 源 `https://mirrors.cloud.tencent.com/pypi/simple` +- 预构建:在本地构建并推送到腾讯云 TCR,服务器直接拉取镜像,避免远端构建时慢下载 + ## 前端扫码 - 优先使用浏览器原生 `BarcodeDetector` 实时识别 - 非安全上下文或不支持时,自动切换为“拍照识别”方式 diff --git a/static/app.js b/static/app.js index b8aef4c..b37e8ac 100644 --- a/static/app.js +++ b/static/app.js @@ -16,6 +16,12 @@ let videoEl = null; let mediaStream = null; const scannerMessage = qs('#scannerMessage'); const captureInput = qs('#captureInput'); +const toggleTorchBtn = qs('#toggleTorch'); +const zoomInput = qs('#zoom'); +const zoomWrap = qs('#zoomWrap'); +let detector = null; +let videoTrack = null; +let loopReq = null; // 无分页模式 let currentTab = 'search'; @@ -93,8 +99,10 @@ async function openScanner(){ return; } if('BarcodeDetector' in window){ - const formats = ['ean_13','ean_8','code_128','code_39','upc_a','upc_e']; - const detector = new window.BarcodeDetector({ formats }); + const supported = await window.BarcodeDetector.getSupportedFormats(); + const wanted = ['ean_13','ean_8','code_128','code_39','upc_a','upc_e']; + const useFormats = wanted.filter(f=>supported.includes(f)); + detector = new window.BarcodeDetector({ formats: useFormats.length?useFormats:supported }); mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); videoEl = document.createElement('video'); videoEl.autoplay = true; @@ -105,9 +113,35 @@ async function openScanner(){ host.appendChild(videoEl); await videoEl.play(); scannerMessage.classList.add('hidden'); - const loop = async()=>{ + try{ + videoTrack = mediaStream.getVideoTracks()[0]; + const caps = videoTrack.getCapabilities ? videoTrack.getCapabilities() : {}; + if(caps.torch){ toggleTorchBtn.classList.remove('hidden'); } + else { toggleTorchBtn.classList.add('hidden'); } + if(caps.zoom){ + zoomWrap.classList.remove('hidden'); + const min = caps.zoom.min ?? 1; + const max = caps.zoom.max ?? 5; + zoomInput.min = String(min); + zoomInput.max = String(max); + zoomInput.value = String(min); + } else { + zoomWrap.classList.add('hidden'); + } + }catch(_){ } + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const tick = async()=>{ try{ - const codes = await detector.detect(videoEl); + const w = videoEl.videoWidth || 640; + const h = videoEl.videoHeight || 480; + const size = Math.min(w, h); + const sx = (w - size)/2; + const sy = (h - size)/2; + canvas.width = size; + canvas.height = size; + ctx.drawImage(videoEl, sx, sy, size, size, 0, 0, size, size); + const codes = await detector.detect(canvas); if(codes && codes.length){ const text = codes[0].rawValue; input.value = text; @@ -117,9 +151,9 @@ async function openScanner(){ return; } }catch(_){ } - if(!scannerModal.classList.contains('hidden')) requestAnimationFrame(loop); + if(!scannerModal.classList.contains('hidden')){ loopReq = requestAnimationFrame(tick); } }; - requestAnimationFrame(loop); + loopReq = requestAnimationFrame(tick); } else { scannerMessage.textContent = '浏览器不支持实时扫码,已切换为拍照识别方式'; captureInput.click(); @@ -135,6 +169,7 @@ function closeScannerFn(){ try{ if(mediaStream){ mediaStream.getTracks().forEach(t=>t.stop()); mediaStream=null; } }catch(_){ } try{ const host = document.getElementById('scanner'); if(host){ host.innerHTML=''; } }catch(_){ } try{ if(scannerMessage){ scannerMessage.textContent=''; scannerMessage.classList.add('hidden'); } }catch(_){ } + try{ if(loopReq){ cancelAnimationFrame(loopReq); loopReq=null; } }catch(_){ } scannerModal.classList.add('hidden'); } @@ -167,6 +202,23 @@ captureInput.addEventListener('change', async ()=>{ } }); +toggleTorchBtn.addEventListener('click', async ()=>{ + try{ + if(!videoTrack) return; + const settings = videoTrack.getSettings ? videoTrack.getSettings() : {}; + const torchOn = settings.torch === true; + await videoTrack.applyConstraints({ advanced: [{ torch: !torchOn }] }); + }catch(_){ } +}); + +zoomInput.addEventListener('input', async ()=>{ + try{ + if(!videoTrack) return; + const z = Number(zoomInput.value); + await videoTrack.applyConstraints({ advanced: [{ zoom: z }] }); + }catch(_){ } +}); + scanBtn.addEventListener('click', openScanner); closeScanner.addEventListener('click', closeScannerFn); diff --git a/static/index.html b/static/index.html index c496def..33164d0 100644 --- a/static/index.html +++ b/static/index.html @@ -43,7 +43,14 @@ diff --git a/static/styles.css b/static/styles.css index 5a36125..aa43aae 100644 --- a/static/styles.css +++ b/static/styles.css @@ -35,6 +35,9 @@ mark{background:#ffec99} .modal{position:fixed;inset:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center} .modal.hidden{display:none} .modal-content{background:#fff;border-radius:10px;padding:12px;width:92%;max-width:480px} +.scanner-area{position:relative} +.scanner-controls{display:flex;gap:10px;align-items:center;justify-content:flex-end;margin:8px 0} +.scanner-overlay{position:absolute;inset:0;border:2px dashed rgba(25,118,210,0.6);border-radius:8px;pointer-events:none} @media (max-width:600px){ .container{padding:12px} .tab{padding:8px 12px} diff --git a/部署文档.md b/部署文档.md index e413892..14f98ef 100644 --- a/部署文档.md +++ b/部署文档.md @@ -65,5 +65,33 @@ docker compose up --build -d - 宿主仅开放 80/443;`57777` 可通过回环访问并由 Nginx 反代对外 - 上传导入大小可在 Nginx/面板调整;建议限制文件类型为 Excel(`.xlsx/.xls`) +## 国内部署加速(腾讯云) +- Docker Hub 拉取加速: + - 编辑 `/etc/docker/daemon.json`: + ```json + { + "registry-mirrors": [ + "https://mirror.ccs.tencentyun.com", + "https://hub-mirror.c.163.com", + "https://docker.mirrors.ustc.edu.cn" + ] + } + ``` + - 重启 Docker:`systemctl restart docker` +- Python 依赖加速:镜像内已默认配置 `pip` 使用腾讯云源 `https://mirrors.cloud.tencent.com/pypi/simple` +- 预构建镜像: + - 在本地构建并推送到腾讯云 TCR(容器镜像服务): + ```bash + docker build -t ccr.ccs.tencentyun.com//:v1 . + docker login ccr.ccs.tencentyun.com + docker push ccr.ccs.tencentyun.com//:v1 + ``` + - 服务器直接拉取镜像并运行,跳过远端构建: + ```bash + docker pull ccr.ccs.tencentyun.com//:v1 + docker compose up -d + ``` + + --- 如需将 Compose 改为读取 `.env`(例如 `PORT`、令牌等),我可以补充 `.env.example` 与 `docker-compose.yml` 的 `env_file` 配置。当前方案保持简洁,端口固定为 57777。