Compare commits
3 Commits
cd638d9a5c
...
1b08f2c4a2
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b08f2c4a2 | |||
| 7163ca67f9 | |||
| e5582bab5d |
+84
-45
@@ -32,15 +32,11 @@ docker load -i qgis-34411.tar
|
||||
|
||||
```bash
|
||||
# ---- Linux / macOS ----
|
||||
|
||||
PROJECT_ROOT=/home/xian/xian_algorithm_new
|
||||
FILE_STORE=/data
|
||||
|
||||
docker run -d \
|
||||
--name qgis-server \
|
||||
--restart unless-stopped \
|
||||
-v "${PROJECT_ROOT}:/app:ro" \
|
||||
-v "${FILE_STORE}:${FILE_STORE}" \
|
||||
-v "/www/wwwroot/xian_algorithm_new:/app:ro" \
|
||||
-v "/www/wwwroot/xian_algorithm_new/files:/files" \
|
||||
qgis/qgis:3.44.11 \
|
||||
sleep infinity
|
||||
|
||||
@@ -124,60 +120,103 @@ docker exec qgis-server ls -la /data/template/earthquake
|
||||
- 容器重建后(`/data` 目录丢失)
|
||||
- 首次部署时
|
||||
|
||||
## 5. 安装中文字体(手动)
|
||||
## 5. 安装中文字体(手动,必须执行)
|
||||
|
||||
QGIS 模板使用了 SimHei(黑体)、SimSun(宋体)、Microsoft YaHei(微软雅黑)等 Windows 中文字体,
|
||||
Docker 镜像默认不包含这些字体,会导致中文全部乱码。**字体需手动安装,代码不会自动安装。**
|
||||
|
||||
### 步骤
|
||||
### 5.1 准备字体文件
|
||||
|
||||
字体文件存放在项目根目录的 `fonts/` 目录下,已预置 4 个常用中文字体:
|
||||
|
||||
```
|
||||
fonts/
|
||||
├── simhei.ttf — 黑体(模板默认字体)
|
||||
├── simsun.ttc — 宋体
|
||||
├── msyh.ttc — 微软雅黑
|
||||
└── msyhbd.ttc — 微软雅黑粗体
|
||||
```
|
||||
|
||||
如果字体缺失,从 Windows 主机复制(`C:\Windows\Fonts\`):
|
||||
|
||||
```bash
|
||||
# 1. 创建字体目录
|
||||
docker exec qgis-server mkdir -p /usr/share/fonts/truetype/winfonts
|
||||
# Linux 服务器用 SCP 从 Windows 传
|
||||
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 中文字体
|
||||
# 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/
|
||||
### 5.2 一键安装到容器
|
||||
|
||||
# Linux 字体路径示例:
|
||||
# docker cp /usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf qgis-server:/usr/share/fonts/truetype/winfonts/
|
||||
```bash
|
||||
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
|
||||
|
||||
# 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
|
||||
docker run -d ... \
|
||||
-v "C:\Windows\Fonts\simhei.ttf:/usr/share/fonts/truetype/winfonts/simhei.ttf" \
|
||||
-v "C:\Windows\Fonts\simsun.ttc:/usr/share/fonts/truetype/winfonts/simsun.ttc" \
|
||||
...
|
||||
```
|
||||
# Linux 服务器安装开源中文字体
|
||||
yum install wqy-microhei-fonts # CentOS / RHEL
|
||||
apt install fonts-wqy-microhei # Debian / Ubuntu
|
||||
|
||||
**方案 B:挂载整个字体目录(推荐)**
|
||||
```bash
|
||||
# 先在主机创建字体目录,放入所需字体文件
|
||||
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" \
|
||||
...
|
||||
# 将系统字体复制到项目 fonts/ 目录
|
||||
cp /usr/share/fonts/wqy-microhei/wqy-microhei.ttc /www/wwwroot/xian_algorithm_new/fonts/
|
||||
```
|
||||
|
||||
## 6. 验证容器
|
||||
|
||||
@@ -316,7 +316,7 @@ def _background_export(inference_id: int) -> None:
|
||||
status = "FAIL" if "error" in r else "OK"
|
||||
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:
|
||||
_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)
|
||||
|
||||
|
||||
+17
-17
@@ -39,26 +39,26 @@ class AppLauncher:
|
||||
check_virtualenv(self.project_root)
|
||||
|
||||
# 检查是否正在使用虚拟环境运行
|
||||
import platform
|
||||
import sys
|
||||
venv_path = self.project_root / ".venv"
|
||||
os_name = platform.system()
|
||||
|
||||
if os_name == 'Windows':
|
||||
venv_python = venv_path / "Scripts" / "python.exe"
|
||||
else: # Linux/Mac
|
||||
venv_python = venv_path / "bin" / "python3"
|
||||
|
||||
# 如果当前不是使用虚拟环境的Python,则重新启动
|
||||
current_python = Path(sys.executable).resolve()
|
||||
venv_python_resolved = venv_python.resolve()
|
||||
|
||||
if current_python != venv_python_resolved:
|
||||
# sys.prefix != sys.base_prefix 是 Python 检测 venv 的标准方式
|
||||
# 不依赖路径解析,Windows/Linux 均适用
|
||||
in_venv = hasattr(sys, 'real_prefix') or (
|
||||
hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix
|
||||
)
|
||||
|
||||
if not in_venv:
|
||||
import platform
|
||||
venv_path = self.project_root / ".venv"
|
||||
os_name = platform.system()
|
||||
|
||||
if os_name == 'Windows':
|
||||
venv_python = venv_path / "Scripts" / "python.exe"
|
||||
else: # Linux/Mac
|
||||
venv_python = venv_path / "bin" / "python3"
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("检测到未使用虚拟环境,正在切换到虚拟环境...")
|
||||
print("=" * 50)
|
||||
|
||||
# 使用虚拟环境的Python重新启动应用(不传递参数避免重复检查)
|
||||
|
||||
import subprocess
|
||||
cmd = [str(venv_python)] + sys.argv
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
+4
-8
@@ -6,8 +6,7 @@ from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
|
||||
from app.utils.api_deps import get_rainfall_model, get_earthquake_model, is_model_loaded
|
||||
from app.schemas.api_schemas import HealthResponse
|
||||
from app.utils.api_deps import get_rainfall_model, get_earthquake_model
|
||||
from app.config.paths import get_logger
|
||||
from config import settings
|
||||
|
||||
@@ -57,6 +56,9 @@ def create_app() -> FastAPI:
|
||||
start = time.time()
|
||||
response = await call_next(request)
|
||||
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)")
|
||||
return response
|
||||
|
||||
@@ -64,10 +66,4 @@ def create_app() -> FastAPI:
|
||||
from app.api import register_routers
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
@@ -139,7 +139,7 @@ class QgisPool:
|
||||
for line in worker.proc.stderr:
|
||||
line = line.strip()
|
||||
if line:
|
||||
logger.debug(f"[Worker-{worker.worker_id}] stderr: {line}")
|
||||
logger.info(f"[Worker-{worker.worker_id}] {line}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -188,4 +188,11 @@ def 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)
|
||||
|
||||
@@ -10,7 +10,7 @@ from config import settings
|
||||
|
||||
class RedisHelper:
|
||||
"""Redis 数据库帮助类"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""初始化 Redis 连接配置"""
|
||||
self.redis_config = {
|
||||
@@ -23,7 +23,20 @@ class RedisHelper:
|
||||
'socket_timeout': 5, # 读写超时时间(秒)
|
||||
}
|
||||
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
|
||||
def client(self) -> redis.Redis:
|
||||
"""获取 Redis 客户端实例(单例模式)"""
|
||||
@@ -32,10 +45,17 @@ class RedisHelper:
|
||||
self._client = redis.Redis(**self.redis_config)
|
||||
# 测试连接
|
||||
self._client.ping()
|
||||
except redis.AuthenticationError as e:
|
||||
self._log_config_once()
|
||||
raise ConnectionError(f"Redis 认证失败(密码错误): {e}")
|
||||
except redis.ConnectionError as e:
|
||||
self._log_config_once()
|
||||
raise ConnectionError(f"无法连接到 Redis 服务器: {e}")
|
||||
except Exception as e:
|
||||
self._log_config_once()
|
||||
raise ConnectionError(f"Redis 连接异常: {e}")
|
||||
return self._client
|
||||
|
||||
|
||||
def close(self):
|
||||
"""关闭 Redis 连接"""
|
||||
if self._client:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user