Compare commits

..

3 Commits

Author SHA1 Message Date
wzy-warehouse 1b08f2c4a2 添加中文字体 2026-06-24 15:52:40 +08:00
wzy-warehouse 7163ca67f9 修改适配Linux 2026-06-24 14:16:23 +08:00
wzy-warehouse e5582bab5d 修改适配Linux 2026-06-24 13:03:15 +08:00
13 changed files with 297 additions and 76 deletions
+84 -45
View File
@@ -32,15 +32,11 @@ docker load -i qgis-34411.tar
```bash ```bash
# ---- Linux / macOS ---- # ---- Linux / macOS ----
PROJECT_ROOT=/home/xian/xian_algorithm_new
FILE_STORE=/data
docker run -d \ docker run -d \
--name qgis-server \ --name qgis-server \
--restart unless-stopped \ --restart unless-stopped \
-v "${PROJECT_ROOT}:/app:ro" \ -v "/www/wwwroot/xian_algorithm_new:/app:ro" \
-v "${FILE_STORE}:${FILE_STORE}" \ -v "/www/wwwroot/xian_algorithm_new/files:/files" \
qgis/qgis:3.44.11 \ qgis/qgis:3.44.11 \
sleep infinity sleep infinity
@@ -124,60 +120,103 @@ docker exec qgis-server ls -la /data/template/earthquake
- 容器重建后(`/data` 目录丢失) - 容器重建后(`/data` 目录丢失)
- 首次部署时 - 首次部署时
## 5. 安装中文字体(手动) ## 5. 安装中文字体(手动,必须执行
QGIS 模板使用了 SimHei(黑体)、SimSun(宋体)、Microsoft YaHei(微软雅黑)等 Windows 中文字体, QGIS 模板使用了 SimHei(黑体)、SimSun(宋体)、Microsoft YaHei(微软雅黑)等 Windows 中文字体,
Docker 镜像默认不包含这些字体,会导致中文全部乱码。**字体需手动安装,代码不会自动安装。** Docker 镜像默认不包含这些字体,会导致中文全部乱码。**字体需手动安装,代码不会自动安装。**
### 步骤 ### 5.1 准备字体文件
字体文件存放在项目根目录的 `fonts/` 目录下,已预置 4 个常用中文字体:
```
fonts/
├── simhei.ttf — 黑体(模板默认字体)
├── simsun.ttc — 宋体
├── msyh.ttc — 微软雅黑
└── msyhbd.ttc — 微软雅黑粗体
```
如果字体缺失,从 Windows 主机复制(`C:\Windows\Fonts\`):
```bash ```bash
# 1. 创建字体目录 # Linux 服务器用 SCP 从 Windows 传
docker exec qgis-server mkdir -p /usr/share/fonts/truetype/winfonts scp "C:\Windows\Fonts\simhei.ttf" root@服务器IP:/www/wwwroot/xian_algorithm_new/fonts/
scp "C:\Windows\Fonts\simsun.ttc" root@服务器IP:/www/wwwroot/xian_algorithm_new/fonts/
scp "C:\Windows\Fonts\msyh.ttc" root@服务器IP:/www/wwwroot/xian_algorithm_new/fonts/
scp "C:\Windows\Fonts\msyhbd.ttc" root@服务器IP:/www/wwwroot/xian_algorithm_new/fonts/
```
# 2. 从主机复制 Windows 中文字体 ### 5.2 一键安装到容器
# Windows 路径(根据实际字体文件位置调整):
docker cp "C:\Windows\Fonts\simhei.ttf" qgis-server:/usr/share/fonts/truetype/winfonts/
docker cp "C:\Windows\Fonts\simsun.ttc" qgis-server:/usr/share/fonts/truetype/winfonts/
docker cp "C:\Windows\Fonts\msyh.ttc" qgis-server:/usr/share/fonts/truetype/winfonts/
docker cp "C:\Windows\Fonts\msyhbd.ttc" qgis-server:/usr/share/fonts/truetype/winfonts/
# Linux 字体路径示例: ```bash
# docker cp /usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf qgis-server:/usr/share/fonts/truetype/winfonts/ python app/script/install_fonts_to_container.py
```
# 3. 刷新字体缓存 输出示例:
```
=== 安装中文字体到 Docker 容器 qgis-server ===
字体目录: /www/wwwroot/xian_algorithm_new/fonts
字体文件: 4 个
- msyh.ttc (4165 KB)
- msyhbd.ttc (4167 KB)
- simhei.ttf (2637 KB)
- simsun.ttc (10126 KB)
容器目标: qgis-server:/usr/share/fonts/truetype/winfonts
[1/3] 复制字体文件...
OK msyh.ttc
OK msyhbd.ttc
OK simhei.ttf
OK simsun.ttc
[2/3] 刷新字体缓存...
OK
[3/3] 验证字体...
中文字体: ['SimHei', 'SimSun', 'Microsoft YaHei', 'Microsoft YaHei UI']
=== 完成,耗时 3.2s ===
```
其他用法:
```bash
python app/script/install_fonts_to_container.py --dry-run # 仅查看信息
python app/script/install_fonts_to_container.py --container my # 指定容器名
```
### 5.3 持久化(推荐)
容器重建后字体丢失,**强烈建议挂载 `fonts/` 目录**。
```bash
# 停掉旧容器
docker stop qgis-server && docker rm qgis-server
# 重建容器,加上字体挂载
docker run -d \
--name qgis-server \
--restart unless-stopped \
-v "/www/wwwroot/xian_algorithm_new:/app:ro" \
-v "/www/wwwroot/xian_algorithm_new/files:/files" \
-v "/www/wwwroot/xian_algorithm_new/fonts:/usr/share/fonts/truetype/winfonts:ro" \
qgis/qgis:3.44.11 \
sleep infinity
# 挂载后只需刷新一次缓存
docker exec qgis-server fc-cache -fv docker exec qgis-server fc-cache -fv
# 4. 验证字体已识别
docker exec qgis-server python3 -c "
from PyQt5.QtGui import QFontDatabase
db = QFontDatabase()
zh = [f for f in db.families() if 'SimHei' in f or 'YaHei' in f or 'SimSun' in f]
print('中文字体:', zh)
"
``` ```
### 持久化方案 ### 5.4 无法获取 Windows 字体时的替代方案
容器重建后字体丢失。可选方案:
**方案 A:挂载单个字体文件**
```bash ```bash
docker run -d ... \ # Linux 服务器安装开源中文字体
-v "C:\Windows\Fonts\simhei.ttf:/usr/share/fonts/truetype/winfonts/simhei.ttf" \ yum install wqy-microhei-fonts # CentOS / RHEL
-v "C:\Windows\Fonts\simsun.ttc:/usr/share/fonts/truetype/winfonts/simsun.ttc" \ apt install fonts-wqy-microhei # Debian / Ubuntu
...
```
**方案 B:挂载整个字体目录(推荐)** # 将系统字体复制到项目 fonts/ 目录
```bash cp /usr/share/fonts/wqy-microhei/wqy-microhei.ttc /www/wwwroot/xian_algorithm_new/fonts/
# 先在主机创建字体目录,放入所需字体文件
mkdir -p /opt/qgis-fonts
cp /usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf /opt/qgis-fonts/
docker run -d ... \
-v "/opt/qgis-fonts:/usr/share/fonts/truetype/winfonts:ro" \
...
``` ```
## 6. 验证容器 ## 6. 验证容器
+1 -1
View File
@@ -316,7 +316,7 @@ def _background_export(inference_id: int) -> None:
status = "FAIL" if "error" in r else "OK" status = "FAIL" if "error" in r else "OK"
logger.info(f"[Pool] {status} {r.get('name', '?')}: {r.get('error', r.get('output', ''))[:100]}") logger.info(f"[Pool] {status} {r.get('name', '?')}: {r.get('error', r.get('output', ''))[:100]}")
if inference_id and file_store and "error" not in r: if inference_id and file_store and "error" not in r:
_write_single_path(inference_id, r.get("output", ""), file_store, db_lock) _write_single_path(inference_id, r["output"], file_store, db_lock)
results, summary = qgis_pool.submit_job(config, container_models, _on_progress) results, summary = qgis_pool.submit_job(config, container_models, _on_progress)
+13 -13
View File
@@ -39,26 +39,26 @@ class AppLauncher:
check_virtualenv(self.project_root) check_virtualenv(self.project_root)
# 检查是否正在使用虚拟环境运行 # 检查是否正在使用虚拟环境运行
import platform # sys.prefix != sys.base_prefix 是 Python 检测 venv 的标准方式
import sys # 不依赖路径解析,Windows/Linux 均适用
venv_path = self.project_root / ".venv" in_venv = hasattr(sys, 'real_prefix') or (
os_name = platform.system() hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix
)
if os_name == 'Windows': if not in_venv:
venv_python = venv_path / "Scripts" / "python.exe" import platform
else: # Linux/Mac venv_path = self.project_root / ".venv"
venv_python = venv_path / "bin" / "python3" os_name = platform.system()
# 如果当前不是使用虚拟环境的Python,则重新启动 if os_name == 'Windows':
current_python = Path(sys.executable).resolve() venv_python = venv_path / "Scripts" / "python.exe"
venv_python_resolved = venv_python.resolve() else: # Linux/Mac
venv_python = venv_path / "bin" / "python3"
if current_python != venv_python_resolved:
print("\n" + "=" * 50) print("\n" + "=" * 50)
print("检测到未使用虚拟环境,正在切换到虚拟环境...") print("检测到未使用虚拟环境,正在切换到虚拟环境...")
print("=" * 50) print("=" * 50)
# 使用虚拟环境的Python重新启动应用(不传递参数避免重复检查)
import subprocess import subprocess
cmd = [str(venv_python)] + sys.argv cmd = [str(venv_python)] + sys.argv
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
+4 -8
View File
@@ -6,8 +6,7 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from app.utils.api_deps import get_rainfall_model, get_earthquake_model, is_model_loaded from app.utils.api_deps import get_rainfall_model, get_earthquake_model
from app.schemas.api_schemas import HealthResponse
from app.config.paths import get_logger from app.config.paths import get_logger
from config import settings from config import settings
@@ -57,6 +56,9 @@ def create_app() -> FastAPI:
start = time.time() start = time.time()
response = await call_next(request) response = await call_next(request)
elapsed = time.time() - start elapsed = time.time() - start
# 静默健康检查探针
if request.url.path == "/" and request.method == "GET":
return response
logger.info(f"{request.method} {request.url.path} -> {response.status_code} ({elapsed:.3f}s)") logger.info(f"{request.method} {request.url.path} -> {response.status_code} ({elapsed:.3f}s)")
return response return response
@@ -64,10 +66,4 @@ def create_app() -> FastAPI:
from app.api import register_routers from app.api import register_routers
register_routers(application) register_routers(application)
@application.get("/health", response_model=HealthResponse, tags=["系统"])
async def health_check():
"""健康检查"""
status = is_model_loaded()
return HealthResponse(status="ok", **status)
return application return application
+159
View File
@@ -0,0 +1,159 @@
"""
将项目 fonts/ 目录下的中文字体安装到 Docker QGIS 容器。
QGIS 官方 Docker 镜像不包含中文字体,模板中的 SimHei/SimSun/YaHei 字体会显示为方块。
本脚本将 fonts/ 目录下的字体文件复制到容器内并刷新字体缓存。
用法:
python app/script/install_fonts_to_container.py [--container qgis-server] [--dry-run]
前置条件:
- Docker 容器已启动(docker start qgis-server
- 项目根目录下 fonts/ 目录包含所需的 .ttf/.ttc 字体文件
"""
import argparse
import subprocess
import sys
import time
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
FONTS_DIR = PROJECT_ROOT / "fonts"
# 容器内字体目录
CONTAINER_FONT_DIR = "/usr/share/fonts/truetype/winfonts"
def _run(cmd, timeout=10, **kwargs):
"""subprocess.run 封装,统一 UTF-8 编码"""
return subprocess.run(
cmd, capture_output=True, timeout=timeout,
encoding="utf-8", errors="replace", **kwargs,
)
def _load_config():
"""从 settings.toml 读取配置"""
try:
from config import settings
return {
"container": getattr(settings, "QGIS_DOCKER_CONTAINER", "qgis-server"),
}
except ImportError:
pass
cfg = {"container": "qgis-server"}
toml_path = PROJECT_ROOT / "settings.toml"
if toml_path.exists():
for line in toml_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith("QGIS_DOCKER_CONTAINER") and "=" in line:
cfg["container"] = line.split("=", 1)[1].strip().strip('"').strip("'")
return cfg
def _check_container(container: str):
"""检查容器是否运行"""
result = _run(["docker", "inspect", "--format={{.State.Running}}", container], timeout=5)
if (result.stdout or "").strip() != "true":
raise RuntimeError(f"容器 {container} 未运行,请先 docker start {container}")
def install_fonts(container: str = None, dry_run: bool = False):
"""将 fonts/ 目录下的字体安装到容器"""
cfg = _load_config()
if container is None:
container = cfg["container"]
_check_container(container)
# 扫描字体文件
if not FONTS_DIR.is_dir():
print(f"字体目录不存在: {FONTS_DIR}")
print(f"请先在项目根目录下创建 fonts/ 目录,并放入 .ttf/.ttc 字体文件。")
print(f"Windows 字体路径: C:\\Windows\\Fonts\\")
print(f" simhei.ttf — 黑体(模板默认字体)")
print(f" simsun.ttc — 宋体")
print(f" msyh.ttc — 微软雅黑")
print(f" msyhbd.ttc — 微软雅黑粗体")
sys.exit(1)
font_files = [f for f in FONTS_DIR.iterdir()
if f.is_file() and f.suffix.lower() in (".ttf", ".ttc", ".otf")]
if not font_files:
print(f"字体目录为空或无字体文件: {FONTS_DIR}")
print(f"请放入 .ttf/.ttc/.otf 字体文件后重试。")
sys.exit(1)
print(f"=== 安装中文字体到 Docker 容器 {container} ===\n")
print(f" 字体目录: {FONTS_DIR}")
print(f" 字体文件: {len(font_files)}")
for f in sorted(font_files):
size_kb = f.stat().st_size / 1024
print(f" - {f.name} ({size_kb:.0f} KB)")
print(f" 容器目标: {container}:{CONTAINER_FONT_DIR}")
print()
if dry_run:
print(" [dry-run] 跳过安装")
return
# 1. 在容器内创建字体目录
t0 = time.time()
_run(["docker", "exec", container, "mkdir", "-p", CONTAINER_FONT_DIR])
# 2. 逐个复制字体文件到容器
print(" [1/3] 复制字体文件...")
for f in sorted(font_files):
result = _run(
["docker", "cp", str(f), f"{container}:{CONTAINER_FONT_DIR}/{f.name}"],
timeout=30,
)
if result.returncode != 0:
print(f" FAIL {f.name}: {result.stderr.strip()}")
else:
print(f" OK {f.name}")
# 3. 刷新字体缓存
print("\n [2/3] 刷新字体缓存...")
result = _run(["docker", "exec", container, "fc-cache", "-fv"], timeout=30)
if result.returncode != 0:
print(f" WARN fc-cache 输出: {result.stderr.strip()}")
else:
print(" OK")
# 4. 验证字体
print("\n [3/3] 验证字体...")
verify_script = (
"from PyQt5.QtGui import QFontDatabase; "
"db = QFontDatabase(); "
"zh = [f for f in db.families() if any(k in f for k in "
"['SimHei','YaHei','SimSun','WenQuanYi','Noto Sans CJK'])]; "
"print('中文字体:', zh if zh else '未安装!')"
)
result = _run(
["docker", "exec", container, "python3", "-c", verify_script],
timeout=10,
)
print(f" {(result.stdout or '').strip()}")
elapsed = time.time() - t0
print(f"\n=== 完成,耗时 {elapsed:.1f}s ===")
def main():
parser = argparse.ArgumentParser(description="将中文字体安装到 Docker QGIS 容器")
parser.add_argument("--container", default=None, help="Docker 容器名称")
parser.add_argument("--dry-run", action="store_true", help="仅显示信息,不实际安装")
args = parser.parse_args()
try:
install_fonts(container=args.container, dry_run=args.dry_run)
except Exception as e:
print(f"错误: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
+1 -1
View File
@@ -139,7 +139,7 @@ class QgisPool:
for line in worker.proc.stderr: for line in worker.proc.stderr:
line = line.strip() line = line.strip()
if line: if line:
logger.debug(f"[Worker-{worker.worker_id}] stderr: {line}") logger.info(f"[Worker-{worker.worker_id}] {line}")
except Exception: except Exception:
pass pass
+8 -1
View File
@@ -188,4 +188,11 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() try:
main()
except Exception as e:
# 捕获 main() 未处理的异常,确保输出到 stderr
import traceback
print(f"[worker] 未捕获异常: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
sys.exit(1)
+20
View File
@@ -23,6 +23,19 @@ class RedisHelper:
'socket_timeout': 5, # 读写超时时间(秒) 'socket_timeout': 5, # 读写超时时间(秒)
} }
self._client: Optional[redis.Redis] = None self._client: Optional[redis.Redis] = None
self._logged_config = False # 避免重复打印配置
def _log_config_once(self):
"""首次连接失败时打印配置信息,便于排查"""
if not self._logged_config:
from app.utils.logger import get_logger
_logger = get_logger("redis")
_logger.warning(
f"Redis 连接配置: host={self.redis_config['host']}, "
f"port={self.redis_config['port']}, db={self.redis_config['db']}, "
f"password={'***' if self.redis_config['password'] else 'None'}"
)
self._logged_config = True
@property @property
def client(self) -> redis.Redis: def client(self) -> redis.Redis:
@@ -32,8 +45,15 @@ class RedisHelper:
self._client = redis.Redis(**self.redis_config) self._client = redis.Redis(**self.redis_config)
# 测试连接 # 测试连接
self._client.ping() self._client.ping()
except redis.AuthenticationError as e:
self._log_config_once()
raise ConnectionError(f"Redis 认证失败(密码错误): {e}")
except redis.ConnectionError as e: except redis.ConnectionError as e:
self._log_config_once()
raise ConnectionError(f"无法连接到 Redis 服务器: {e}") raise ConnectionError(f"无法连接到 Redis 服务器: {e}")
except Exception as e:
self._log_config_once()
raise ConnectionError(f"Redis 连接异常: {e}")
return self._client return self._client
def close(self): def close(self):
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.