diff --git a/idcard-api/.env b/idcard-api/.env new file mode 100644 index 0000000..61f1f17 --- /dev/null +++ b/idcard-api/.env @@ -0,0 +1,42 @@ +# 复制为 .env 后按需修改:cp .env.example .env +# .env 会被 docker compose 自动读取,不要提交到公开仓库 + +# ============ 鉴权(三选一,推荐第 1 种) ============ +# 生成 token:openssl rand -hex 24 或 python -c "import secrets;print(secrets.token_urlsafe(32))" + +# 1) 多 token(推荐):名称:token,名称2:token2 +# 每个调用方发一把,吊销时只删对应的那一段,不影响别人。 +# 日志里会打印调用方名称,方便排查是谁在调。 +API_TOKENS=frontend:c419f1c903ac03c4f4329f21a8f49af57118fc1bce6660f2,win-service:3444df79189bbf16e872d109c5d56bc7684e24645c39b416 + +# 2) 旧版单密钥(兼容保留,不推荐):所有调用方共用一把 +# API_KEY= + +# 3) 短期 token(可选):设置后启用 POST /api/v1/auth/token +# 调用方用 client_secret 换有效期 TOKEN_TTL 秒的签名 token, +# 适合前端——页面里不硬编码长期密钥,只持有短时 token。 +# CLIENT_ID=default +# CLIENT_SECRET= +# TOKEN_TTL=3600 + +# ============ 跨域 ============ +# 允许跨域的前端来源,逗号分隔。浏览器里的 Vue/React 页面调这个接口必须配。 +# * = 允许所有来源(方便开发,但任何网页都能读结果,生产环境务必收紧) +# 例:CORS_ORIGINS=http://10.1.1.163:5173,http://10.1.1.163:8080 +# 后端到后端调用(curl / requests / 其他容器)不受此项影响。 +CORS_ORIGINS=* + +# ============ 基础镜像源 ============ +# 默认国内公共源(fnOS 自带加速源需授权,常 401)。 +# 备选: +# BASE_IMAGE=docker.1ms.run/library/python:3.11-slim +# BASE_IMAGE=mirror.ccs.tencentyun.com/library/python:3.11-slim +# 离线导入基础镜像(docker load)后改用本地 tag: +# BASE_IMAGE=python:3.11-slim +# BASE_IMAGE=docker.m.daocloud.io/library/python:3.11-slim + +# —— 以下一般不用改,低配吃紧时再动 —— +# MAX_SIDE=960 +# DET_LIMIT_SIDE_LEN=736 +# MAX_DIMENSION=6000 +# MAX_IMAGE_BYTES=5242880 diff --git a/idcard-api/NAS-操作手册.md b/idcard-api/NAS-操作手册.md new file mode 100644 index 0000000..46f3df0 --- /dev/null +++ b/idcard-api/NAS-操作手册.md @@ -0,0 +1,276 @@ +# Windows 操作 NAS Docker · 命令手册 + +针对本项目的实际环境整理(身份证识别 API 部署在飞牛 fnOS)。 + +## 本机参数 + +| 项 | 值 | +|---|---| +| NAS IP | `10.1.1.163` | +| 项目目录 | `/vol1/1000/Docker/idcard-api` | +| compose 项目名 | `icd`(镜像名 `icd-idcard-api`) | +| 容器名 | `idcard-api` | +| 服务端口 | `8000`(容器内 8000 映射到宿主机 8000) | +| 宿主机用户 | `owen`(**不在 docker 组,docker 命令都要 `sudo`**) | + +--- + +## 一、三个操作平面(最容易搞错的地方) + +``` +┌─────────────────────────────────────────────┐ +│ ① Windows PowerShell(你的电脑) │ +│ 能做:curl.exe 测接口、WinSCP 传文件 │ +│ 不能:任何 docker 命令 │ +│ 提示符:PS C:\Users\Administrator> │ +├─────────────────────────────────────────────┤ +│ ② NAS 宿主机(SSH 连进去) │ +│ 能做:docker / docker compose / sudo / 文件操作 │ +│ 登录:ssh owen@10.1.1.163 │ +│ 提示符:owen@youjiala:/$ │ +├─────────────────────────────────────────────┤ +│ ③ 容器内部(fnOS Docker 界面「终端」按钮) │ +│ 能做:容器内的 ls / grep / cat / env │ +│ 不能:docker 命令(容器里没装 docker CLI) │ +│ 提示符:root@c5bbd0b03ba1:/# │ +└─────────────────────────────────────────────┘ +``` + +**判断口诀**:命令里有 `docker` 或要动 NAS 上的文件 → 平面 ②;只是想试试接口通不通 → 平面 ①;想偷懒看容器里某个文件 → 平面 ③(但别在里面敲 docker)。 + +--- + +## 二、快速判断表 + +| 你要做的事 | 在哪里执行 | 需要登录吗 | +|---|---|---| +| 测接口通不通(curl) | Windows PowerShell | ❌ 不需要 | +| 上传/覆盖代码文件 | Windows(WinSCP / 资源管理器) | ❌ 不需要(SFTP) | +| `docker ps` / `docker logs` | NAS 宿主机 | ✅ `ssh owen@10.1.1.163` | +| `docker compose build/up` | NAS 宿主机 | ✅ SSH | +| 看镜像创建时间 | NAS 宿主机 | ✅ SSH | +| 改 `.env` | WinSCP 或 SSH 均可 | 视习惯 | +| 查看容器内 `main.py` | NAS 宿主机 `docker exec` | ✅ SSH | +| 快速瞄一眼容器内文件 | fnOS Docker 界面「终端」 | ❌ 界面点开即可 | +| 加 docker 组免 sudo | NAS 宿主机 | ✅ SSH | + +--- + +## 三、Windows 连接方式 + +### SSH 登录宿主机 + +```powershell +ssh owen@10.1.1.163 +# 首次会问 yes/no,输 yes,然后输 owen 的密码 +# 退出:exit +``` + +看到提示符 `owen@youjiala:/$` 就说明进去了。 + +### 传文件(WinSCP / FileZilla) + +- 协议:`SFTP` +- 主机:`10.1.1.163`,端口 `22` +- 用户:`owen` +- 目标目录:`/vol1/1000/Docker/idcard-api` + +或者在 fnOS 网页的「我的文件 → Docker → idcard-api」里直接上传。 + +### 一次性免 sudo(推荐做一次) + +```bash +sudo usermod -aG docker $USER +newgrp docker # 当前会话立即生效,不用退出重登 +``` + +之后所有 docker 命令都不用加 `sudo` 了。本文档仍按**带 sudo** 写,两种都能用。 + +--- + +## 四、命令速查 + +### 1. 测试接口(Windows PowerShell) + +> ⚠️ PowerShell 里 `curl` 是 `Invoke-WebRequest` 的别名,**必须写 `curl.exe`**。 + +```powershell +# 健康检查 +curl.exe -s http://10.1.1.163:8000/api/v1/idcard/health + +# 根路径(服务信息 + 端点清单) +curl.exe -s http://10.1.1.163:8000/ + +# 识别(带 token) +curl.exe -X POST http://10.1.1.163:8000/api/v1/idcard/recognize ^ + -H "Authorization: Bearer 你的token" ^ + -F "file=@C:/path/idcard.jpg" + +# 识别(旧版 X-API-Key 头) +curl.exe -X POST http://10.1.1.163:8000/api/v1/idcard/recognize ^ + -H "X-API-Key: 你的key" ^ + -F "file=@C:/path/idcard.jpg" + +# 反向验证:不带 token 应该 401 +curl.exe -X POST http://10.1.1.163:8000/api/v1/idcard/recognize ^ + -F "file=@C:/path/idcard.jpg" +``` + +> `^` 是 CMD 的续行符。PowerShell 用反引号 `` ` ``,或者直接写成一行。 + +### 2. 首次部署(SSH) + +```bash +# ① 建目录(WinSCP 已传过文件可跳过) +mkdir -p /vol1/1000/Docker/idcard-api +cd /vol1/1000/Docker/idcard-api + +# ② 确认文件都在 +ls -la + +# ③ 配 .env(必须有 .env,.env.example 不生效) +cp .env.example .env +vi .env # 或 WinSCP 直接编辑 + +# ④ 检查 compose 解析结果 +sudo docker compose -p icd config | grep -A2 API_TOKENS + +# ⑤ 首次构建(会装 numpy/onnxruntime,3-5 分钟) +sudo docker compose -p icd up -d --build + +# ⑥ 跟踪日志 +sudo docker logs -f idcard-api +``` + +### 3. 日常更新代码(SSH) + +```bash +cd /vol1/1000/Docker/idcard-api +sudo docker compose -p icd up -d --build # 增量构建,10-30 秒 +``` + +或者用项目自带脚本(会校验镜像里的代码真的更新了): + +```bash +bash /vol1/1000/Docker/idcard-api/deploy.sh +``` + +**只改了 `.env` 或 `docker-compose.yml`**(不需要 build): + +```bash +sudo docker compose -p icd up -d +``` + +**怀疑缓存坏了**(全量重建,3-5 分钟): + +```bash +sudo docker compose -p icd build --no-cache +sudo docker compose -p icd up -d --force-recreate +``` + +### 4. 排查"改了代码没生效" + +这是本项目最容易踩的坑:**fnOS 的「重新部署」只重建容器,不重建镜像**。 + +```bash +# ① 容器里的代码是新版吗?0=旧代码,≥1=新代码 +sudo docker exec idcard-api grep -c auth_enabled /app/app/main.py + +# ② 镜像什么时候建的? +sudo docker images --format "{{.Repository}}:{{.Tag}} {{.ID}} 创建于 {{.CreatedSince}}" | grep -i idcard + +# ③ compose 项目真实工作目录(和你上传的目录一致吗?) +sudo docker inspect idcard-api --format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' + +# ④ 环境变量注入情况 +sudo docker exec idcard-api env | grep -E "API_TOKENS|API_KEY|CLIENT_SECRET" + +# ⑤ 容器状态与端口 +sudo docker ps --filter name=idcard-api +``` + +### 5. 日常运维(SSH) + +```bash +# 实时日志 +sudo docker logs -f idcard-api + +# 最近 50 行 +sudo docker logs --tail 50 idcard-api + +# 过滤调用记录(看谁在调、耗时多少) +sudo docker logs --tail 100 idcard-api | grep "recognize by=" + +# 内存占用(关键:1C2G 机器要盯这个) +sudo docker stats --no-stream idcard-api + +# 重启(不重新读 .env,改配置请用 up -d) +sudo docker restart idcard-api + +# 停掉并删除容器 + 镜像 +sudo docker compose -p icd down --rmi local + +# 进容器内部 shell(在宿主机执行) +sudo docker exec -it idcard-api bash +``` + +### 6. 容器内部(fnOS 界面「终端」里执行) + +```bash +grep -c auth_enabled /app/app/main.py # 代码版本 +ls -l --time-style=long-iso /app/app/ # 文件时间 +echo $API_TOKENS # 环境变量 +``` + +> 这里**不能**跑 `docker` 命令。要跑 docker,回宿主机 SSH。 + +--- + +## 五、完整流程:从改代码到生效 + +```bash +# ① Windows:改本地文件(WorkBuddy 生成) + +# ② Windows:WinSCP 上传到 /vol1/1000/Docker/idcard-api +# 注意保持目录结构:app/、Dockerfile、docker-compose.yml 等 + +# ③ SSH 登录 NAS +ssh owen@10.1.1.163 + +# ④ 构建并启动 +cd /vol1/1000/Docker/idcard-api +sudo docker compose -p icd up -d --build + +# ⑤ 校验镜像里的代码真的换了(关键!别跳过) +sudo docker exec idcard-api grep -c 你的新特征字符串 /app/app/main.py + +# ⑥ 退出 SSH,回 Windows 测接口 +exit +``` + +```powershell +curl.exe -s http://10.1.1.163:8000/api/v1/idcard/health +``` + +--- + +## 六、错误对照表(本对话踩过的坑) + +| 报错 | 在哪发生 | 原因 | 解决 | +|---|---|---|---| +| `docker: command not found` | 容器内部 | 在容器里跑 docker 命令 | 回宿主机 SSH,或去掉 `docker exec` 前缀用容器内路径 | +| `permission denied ... docker.sock` | 宿主机 | 用户不在 docker 组 | 命令前加 `sudo`,或 `usermod -aG docker` | +| `401 Unauthorized ... docker.fnnas.com` | 宿主机 build | fnOS 加速源不授权,且会覆盖 daemon.json | Dockerfile 用 `ARG BASE_IMAGE=国内仓库全路径`,绕开 daemon 配置 | +| `could not find ... dockerfile` | 宿主机 build | Dockerfile 首行 `# syntax=` 触发前端镜像拉取 | 删掉该行 | +| health 返回缺字段 / 行为没变化 | Windows 测接口 | 镜像没重建,跑的还是旧镜像 | 必须 `up -d --build`,不能只「重新部署」 | +| `.env` 改了不生效 | 宿主机 | ①文件叫 `.env.example` ②只 `restart` 了 | 创建真正的 `.env`,然后 `up -d`(不是 restart) | +| `curl: 无法将...识别为 cmdlet` | Windows | `curl` 是 PowerShell 别名 | 用 `curl.exe` | +| `\r: command not found` | 宿主机 | Windows CRLF 换行 | `sed -i 's/\r$//' 脚本名.sh` | +| 识别返回空,前端提示"未识别到信息" | C# 调用方 | 401 被静默吞掉(没检查状态码) | C# 加鉴权头 + 检查 `IsSuccessStatusCode` | + +--- + +## 七、两条铁律 + +1. **改代码必须 build**。`docker compose up -d`(不带 `--build`)在镜像已存在时不会重新构建,只重建容器。代码改动一点都进不去。 +2. **`-p icd` 不能省**。项目名不是目录名,不带这个参数会创建新项目,容器和镜像都对不上。 diff --git a/idcard-api/README.md b/idcard-api/README.md index 524b8ac..6789499 100644 --- a/idcard-api/README.md +++ b/idcard-api/README.md @@ -130,10 +130,38 @@ docker stats idcard-api # 看内存峰值是 ### 第 5 步:接入调用方 - **同 NAS 上的其他 Docker 容器**:把调用方容器加入 `idcard-api_idcard-api` 网络(或在调用方 compose 里加 `external_links` / 同一自定义网络),地址写 `http://idcard-api:8000`;图省事直接用 `http://飞牛IP:8000`。 -- **局域网其他设备**:`http://飞牛IP:8000`,带 `X-API-Key` 头。 +- **局域网其他设备**:`http://飞牛IP:8000`,带 `Authorization: Bearer ` 头。 - **要域名 + HTTPS**:用 fnOS 自带的反向代理/远程访问把域名指到 `127.0.0.1:8000`,不要在路由器上映射公网端口直连。 - **ARM 机型(aarch64)**:python:3.11-slim 和 opencv-headless 有 ARM 版,但 onnxruntime 需 ≥1.19 才有完善的 ARM wheel,先在 NAS上 `docker compose build` 试;若 onnxruntime 装不上,只能换 x86 机型或用云侧部署。 +### 第 6 步:以后更新代码怎么发(重要) + +**fnOS 的「重新部署 / 重建容器」只会 recreate 容器,不会 rebuild 镜像**——environment 变了会把新变量注进新容器,但代码改动完全不会进去。所以改完代码必须显式 build。 + +```bash +cd /vol1/1000/Docker/idcard-api # 换成你的实际目录 +sudo docker compose -p icd up -d --build +``` + +> `-p icd` 不能省:镜像名 `icd-idcard-api` 说明 compose 项目名是 `icd`,不带这个参数会建成另一个项目,跟现有容器对不上。宿主机用户如果不在 docker 组,命令前都要 `sudo`(或执行一次 `sudo usermod -aG docker $USER && newgrp docker`)。 + +| 你改了什么 | 命令 | 耗时 | +|---|---|---| +| `app/*.py` | `up -d --build` | 10–30 秒 | +| `requirements.txt` | `up -d --build`(pip 层失效重跑) | 1–3 分钟 | +| `.env` / `docker-compose.yml` | `up -d`(自动 recreate,无需 build) | 秒级 | +| 怀疑缓存脏 / 要刷基础镜像 | `build --no-cache` + `up -d --force-recreate` | 3–5 分钟 | + +**`--no-cache` 不是日常操作**:Docker 的层缓存按内容哈希失效,`COPY app ./app` 依赖代码内容,代码一变这一层和后续层必然重跑,前面的 apt/pip 层照常命中缓存。 + +也可以直接用 `deploy.sh`(已随项目提供),它会增量构建、等就绪、校验镜像内代码真的更新了,失败自动降级全量重建: + +```bash +bash /vol1/1000/Docker/idcard-api/deploy.sh +``` + +> 如果脚本报 `\r: command not found`,是 Windows 换行符问题,执行 `sed -i 's/\r$//' deploy.sh` 修一下。 + ## 调用 ```bash @@ -175,6 +203,37 @@ curl -X POST http://127.0.0.1:8000/api/v1/idcard/recognize/base64 \ Python 示例见 `client_example.py`。 +### C# / .NET 调用示例(server-to-server) + +要点三个:**必须带鉴权头**、**必须检查 HTTP 状态码**(否则 401 会被当成"未识别到"静默吞掉)、**超时要给足**(低端 NAS 首张要加载模型 10–20 秒)。 + +```csharp +// 配置里加一项 IdCardOcrToken,与服务端 .env 的 API_TOKENS 对应 +string token = ServiceSettingsPolicy.GetIdCardOcrToken(service); + +using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); +if (!string.IsNullOrWhiteSpace(token)) +{ + // 新版服务(推荐):标准 Bearer 头 + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + // 或旧版服务(仅设置了 API_KEY):改用下面这行 + // request.Headers.TryAddWithoutValidation("X-API-Key", token); +} +request.Content = form; + +using var response = client.SendAsync(request).GetAwaiter().GetResult(); + +// 关键:先判状态码,别让 401/413/422 掉进 JSON 解析分支变成"静默失败" +if (!response.IsSuccessStatusCode) +{ + string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + Console.WriteLine($"[UploadIdCardFile] OCR HTTP {(int)response.StatusCode}: {body}"); + return data; // 前端提示"未识别到信息",后端日志里有真实原因 +} +``` + +`HttpClient` 建议用 `IHttpClientFactory` 或静态单例,不要每次 `new`——高频调用会耗尽 socket。 + ## 鉴权(Token 验证) 支持三种方式,全部用**标准库实现**(HMAC-SHA256),不引入 PyJWT 等依赖,对 1C/2G 无额外开销。 diff --git a/idcard-api/app/main.py b/idcard-api/app/main.py index 5320cfb..9415bc8 100644 --- a/idcard-api/app/main.py +++ b/idcard-api/app/main.py @@ -99,6 +99,17 @@ def _parse_tokens() -> dict: _TOKEN_MAP = _parse_tokens() +# 弱口令提醒:示例占位符(REPLACE_ME_*)或过短的 token 不该出现在正式环境。 +# 只告警不阻止启动,避免影响联调;换成真随机串后这条日志自动消失。 +_weak_clients = sorted( + {name for tok, name in _TOKEN_MAP.items() if "REPLACE_ME" in tok.upper() or len(tok) < 12} +) +if _weak_clients: + log.warning( + "token 过弱或仍是示例占位符,请尽快更换:%s(生成:openssl rand -hex 24)", + ", ".join(_weak_clients), + ) + _engine = None diff --git a/idcard-api/deploy.sh b/idcard-api/deploy.sh new file mode 100644 index 0000000..6d6c124 --- /dev/null +++ b/idcard-api/deploy.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# ------------------------------------------------------------------ +# fnOS 一键更新部署 +# +# 用法(在 NAS 宿主机): +# bash /vol1/1000/Docker/idcard-api/deploy.sh +# +# 逻辑:增量构建(只重跑失效层,通常 10-30s)→ 等就绪 → 校验镜像内代码 +# 是否真的更新了 → 打印 health。校验失败自动降级为 --no-cache 全量重建。 +# ------------------------------------------------------------------ +set -uo pipefail + +# 切到脚本所在目录,避免路径写死 +cd "$(dirname "$0")" || exit 1 + +PROJECT_NAME="icd" # compose 项目名(镜像名 icd-idcard-api 的前缀) +SERVICE="idcard-api" # container_name +PORT=8000 +MARKER="auth_enabled" # 用它能判断镜像里是新代码 + +echo "==> 目录: $(pwd)" + +# ---------- 1. 增量构建 + 启动 ---------- +echo "==> 1/4 构建并启动(增量,通常 10-30 秒)" +if ! sudo docker compose -p "$PROJECT_NAME" up -d --build; then + echo "!! 构建失败。常见原因:基础镜像源 401,检查 Dockerfile 的 BASE_IMAGE" + exit 1 +fi + +# ---------- 2. 等服务就绪 ---------- +echo "==> 2/4 等待服务就绪" +ready=0 +for _ in $(seq 1 30); do + if curl -sf "http://127.0.0.1:$PORT/api/v1/idcard/health" >/dev/null 2>&1; then + ready=1 + break + fi + sleep 2 +done +if [ "$ready" -ne 1 ]; then + echo "!! 服务 60 秒内未就绪,看日志:sudo docker logs --tail 50 $SERVICE" + exit 1 +fi + +# ---------- 3. 校验镜像内代码是否真的更新 ---------- +echo "==> 3/4 校验容器内代码版本" +count=$(sudo docker exec "$SERVICE" grep -c "$MARKER" /app/app/main.py 2>/dev/null || echo 0) +if [ "${count:-0}" -lt 1 ]; then + echo "!! 容器内仍是旧代码,降级为 --no-cache 全量重建(需要几分钟)" + sudo docker compose -p "$PROJECT_NAME" build --no-cache || exit 1 + sudo docker compose -p "$PROJECT_NAME" up -d --force-recreate || exit 1 + count=$(sudo docker exec "$SERVICE" grep -c "$MARKER" /app/app/main.py 2>/dev/null || echo 0) + if [ "${count:-0}" -lt 1 ]; then + echo "!! 全量重建后代码仍是旧的。检查 compose 项目目录是否就是本目录:" + sudo docker inspect "$SERVICE" --format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' + exit 1 + fi +fi +echo " 容器内代码已更新(匹配 $count 处)" + +# ---------- 4. 健康检查 ---------- +echo "==> 4/4 健康检查" +curl -s "http://127.0.0.1:$PORT/api/v1/idcard/health" +echo +echo +echo "部署完成。" +echo " 看日志:sudo docker logs -f $SERVICE" +echo " 看内存:sudo docker stats --no-stream $SERVICE"