优化国内构建的时间
This commit is contained in:
parent
9bfefd635c
commit
e0fb6381d2
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@ -0,0 +1,10 @@
|
||||
data/
|
||||
.git/
|
||||
.gitignore
|
||||
*.log
|
||||
__pycache__/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
node_modules/
|
||||
*.tmp
|
||||
*.swp
|
||||
42
.trae/documents/国内部署加速方案(腾讯云).md
Normal file
42
.trae/documents/国内部署加速方案(腾讯云).md
Normal file
@ -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` 预下载与本地源(后续扩展)。
|
||||
@ -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
|
||||
|
||||
@ -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` 实时识别
|
||||
- 非安全上下文或不支持时,自动切换为“拍照识别”方式
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -43,7 +43,14 @@
|
||||
<div id="scannerModal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div id="scannerMessage" class="scanner-message hidden"></div>
|
||||
<div id="scanner"></div>
|
||||
<div id="scanner" class="scanner-area"></div>
|
||||
<div class="scanner-controls">
|
||||
<button id="toggleTorch" class="hidden">开灯</button>
|
||||
<label id="zoomWrap" class="hidden">
|
||||
缩放
|
||||
<input id="zoom" type="range" min="1" max="5" step="0.1" value="1" />
|
||||
</label>
|
||||
</div>
|
||||
<button id="closeScanner">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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}
|
||||
|
||||
28
部署文档.md
28
部署文档.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/<namespace>/<repo>:v1 .
|
||||
docker login ccr.ccs.tencentyun.com
|
||||
docker push ccr.ccs.tencentyun.com/<namespace>/<repo>:v1
|
||||
```
|
||||
- 服务器直接拉取镜像并运行,跳过远端构建:
|
||||
```bash
|
||||
docker pull ccr.ccs.tencentyun.com/<namespace>/<repo>:v1
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
如需将 Compose 改为读取 `.env`(例如 `PORT`、令牌等),我可以补充 `.env.example` 与 `docker-compose.yml` 的 `env_file` 配置。当前方案保持简洁,端口固定为 57777。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user