项目实践14—全球证件智能识别系统(切换回SQLite数据库并基于Docker实现离线部署和日常管理)

目录

  • 一、任务概述
  • 二、后端架构重构:回归SQLite
    • [2.1 修改数据库连接配置](#2.1 修改数据库连接配置)
    • [2.2 修改数据模型定义](#2.2 修改数据模型定义)
    • [2.3 修改特征提取器](#2.3 修改特征提取器)
    • [2.4 修改主服务逻辑](#2.4 修改主服务逻辑)
    • [2.5 修改初始化数据库](#2.5 修改初始化数据库)
    • [2.6 数据库初始化与迁移重置](#2.6 数据库初始化与迁移重置)
  • 三、运维管理工具集
    • [3.1 快速模板检索工具](#3.1 快速模板检索工具)
    • [3.2 样证数据批量导出工具](#3.2 样证数据批量导出工具)
    • [3.3 数据库重置工具](#3.3 数据库重置工具)
    • [3.4 大模型测试工具](#3.4 大模型测试工具)
  • 四、构建离线Docker镜像
    • [4.1 拉取基础镜像并启动容器](#4.1 拉取基础镜像并启动容器)
    • [4.2 容器内环境初始化](#4.2 容器内环境初始化)
    • [4.3 部署代码与环境](#4.3 部署代码与环境)
    • [4.4 导出离线镜像](#4.4 导出离线镜像)
  • 四、内网环境离线部署
  • 五、总结

一、任务概述

在前序的开发工作中,全球证件智能识别系统已完成全链路的功能开发。在尝试引入PostgreSQL作为生产数据库进行内网部署的实践中,发现由于内网环境的网络策略限制、权限管控以及依赖库(如libpqpsycopg2)的兼容性问题,导致数据库服务的连接与维护异常复杂。

针对单机离线部署且并发量可控的业务场景,SQLite数据库凭借其"单文件、零配置、无服务进程"的特性,展现出了极高的便携性与稳定性。为了降低内网部署的复杂度,确保系统"开箱即用",本篇博客将对后端架构进行轻量化重构。

任务目标包含三个方面:

  1. 架构回退:将FastAPI后端服务的数据持久化层从PostgreSQL完整回退至SQLite。这将涉及数据模型定义、数据库连接配置以及特征向量存储方式(从原生数组回退至二进制序列化)的全面修改。
  2. 镜像构建:基于Docker容器技术,构建包含系统OS、Python环境、依赖库及SQLite数据库文件的全量镜像,并导出为离线包。
  3. 运维工具适配:开发适配SQLite架构的运维管理脚本,涵盖样证模板检索、数据导出及数据库重置功能。

二、后端架构重构:回归SQLite

由于SQLite不支持PostgreSQL特有的ARRAY数据类型,且对高维向量的直接存储支持有限,必须对代码进行深度调整。核心策略是恢复使用Python的pickle库,将特征向量序列化为二进制数据(Bytes)存入数据库的BLOB字段。

2.1 修改数据库连接配置

修改database.py,移除PostgreSQL相关的连接逻辑,配置SQLite连接参数,并开启多线程共享支持。

代码清单:database.py

python 复制代码
import os
from sqlmodel import create_engine, Session

# 定义数据库文件路径
# 在Docker容器中,建议使用绝对路径
SQLITE_FILE_NAME = "card_db.sqlite"
DATABASE_URL = f"sqlite:///{SQLITE_FILE_NAME}"

# 创建数据库引擎
# connect_args={"check_same_thread": False} 对于FastAPI多线程环境是必须的
engine = create_engine(DATABASE_URL, echo=False, connect_args={"check_same_thread": False})

def get_session():
    """
    FastAPI 依赖注入函数,用于获取数据库会话
    """
    with Session(engine) as session:
        yield session

2.2 修改数据模型定义

修改models.py,将特征向量字段的类型从浮点数列表恢复为字节流(bytes),以便存储序列化后的NumPy数组。

代码清单:models.py

python 复制代码
from datetime import datetime
from typing import Optional, List
from sqlmodel import Field, Relationship, SQLModel

class Country(SQLModel, table=True):
    """
    国家数据模型
    """
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True, description="国家中文名称")
    code: str = Field(unique=True, description="ISO 3166-1三位数字代码")
    certificate_templates: List["CertificateTemplate"] = Relationship(back_populates="country")

class CertificateTemplate(SQLModel, table=True):
    """
    证件模板数据模型
    """
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True, description="证件模板名称")
    description: str = Field(description="证件模板的详细描述")

    # 图像数据,以二进制格式存储
    image_front_white: bytes = Field(description="正面白光样证图")
    image_front_uv: bytes = Field(description="正面紫外样证图")
    image_back_white: bytes = Field(description="反面白光样证图")
    image_back_uv: bytes = Field(description="反面紫外样证图")

    # 特征向量数据,使用pickle序列化为bytes存储
    feature_front_white: bytes = Field(description="正面白光图特征向量(Pickled)")
    feature_front_uv: bytes = Field(description="正面紫外图特征向量(Pickled)")
    feature_back_white: bytes = Field(description="反面白光图特征向量(Pickled)")
    feature_back_uv: bytes = Field(description="反面紫外图特征向量(Pickled)")

    country_id: Optional[int] = Field(default=None, foreign_key="country.id")
    country: Optional[Country] = Relationship(back_populates="certificate_templates")

class SampleRecord(SQLModel, table=True):
    """
    样证上传记录表
    """
    id: Optional[int] = Field(default=None, primary_key=True)
    country_code: str = Field(index=True)
    device_sn: str = Field(index=True)
    created_at: datetime = Field(default_factory=datetime.now)
    is_suspicious: bool = Field()
    remarks: Optional[str] = Field(default=None)

    image_front_white: bytes
    image_front_uv: bytes
    image_front_ir: bytes
    image_back_white: bytes
    image_back_uv: bytes
    image_back_ir: bytes

2.3 修改特征提取器

修改feature_extractor.py,恢复使用pickle对提取出的特征向量进行序列化,返回bytes类型。

代码清单:feature_extractor.py

python 复制代码
import io
import pickle
import numpy as np
import torch
import torch.nn as nn
from PIL import Image
from torchvision import models, transforms

class EmbeddingNet(nn.Module):
    def __init__(self, base_model):
        super(EmbeddingNet, self).__init__()
        self.features = nn.Sequential(*list(base_model.children())[:-1])
        self.flatten = nn.Flatten()

    def forward(self, x):
        x = self.features(x)
        x = self.flatten(x)
        return x

class ImageFeatureExtractor:
    def __init__(self, model_path="mobilenetv3_finetuned.pth"):
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        base_model = models.mobilenet_v3_large()
        self.model = EmbeddingNet(base_model).to(self.device)
        
        # 加载权重,处理可能的map_location问题
        if torch.cuda.is_available():
            self.model.load_state_dict(torch.load(model_path))
        else:
            self.model.load_state_dict(torch.load(model_path, map_location=torch.device('cpu')))
            
        self.model.eval()

        self.preprocess = transforms.Compose([
            transforms.Resize((224, 224)),
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
        ])

    def extract_features(self, image_bytes: bytes) -> bytes:
        """
        返回序列化后的二进制特征向量
        """
        try:
            image = Image.open(io.BytesIO(image_bytes)).convert("L").convert("RGB")
            input_tensor = self.preprocess(image)
            input_batch = input_tensor.unsqueeze(0).to(self.device)

            with torch.no_grad():
                output_features = self.model(input_batch)

            feature_np = output_features.cpu().numpy().flatten()
            # 使用pickle序列化
            return pickle.dumps(feature_np)
        except Exception as e:
            print(f"特征提取失败: {e}")
            return b''

2.4 修改主服务逻辑

修改main.py中的检索逻辑,增加pickle反序列化步骤,将从数据库读取的二进制数据还原为NumPy数组进行计算。

代码清单:main.py (部分核心逻辑)

python 复制代码
# ... (保留原有导入)
import pickle # 必须导入
# ...

@app.post("/api/recognize", response_model=RecognitionResponse)
async def recognize_document(request: RecognitionRequest, session: Session = Depends(get_session)):
    # ... (省略前置日志和解码代码)

    # 1. 提取待查询图像特征 (extractor返回的是bytes)
    # 需反序列化为numpy数组用于计算
    query_feature_front = pickle.loads(extractor.extract_features(front_white_bytes))
    query_feature_back = pickle.loads(extractor.extract_features(back_white_bytes))

    # ... (数据库检索代码)

    for template in templates:
        # 2. 反序列化数据库中的特征向量
        template_feature_front = pickle.loads(template.feature_front_white)
        template_feature_back = pickle.loads(template.feature_back_white)

        sim_front = cosine_similarity(query_feature_front, template_feature_front)
        sim_back = cosine_similarity(query_feature_back, template_feature_back)
        
        # ... (后续比对逻辑不变)
    # 5. 二次校验:紫外荧光图像相似度检查
        # ...
            # 提取待查询图像的紫外特征
            query_feature_uv_front = pickle.loads(extractor.extract_features(query_uv_front_bytes))
            query_feature_uv_back = pickle.loads(extractor.extract_features(query_uv_back_bytes))
            
            # 反序列化样证的紫外特征
            template_feature_uv_front = pickle.loads(best_match_template.feature_front_uv)
            template_feature_uv_back = pickle.loads(best_match_template.feature_back_uv)

    
    # ... (后续防伪检测与大模型调用逻辑不变)

2.5 修改初始化数据库

修改init_db.py文件:

python 复制代码
feature_front_white = extractor.extract_features(front_white_bytes) if front_white_bytes else b''
feature_back_white = extractor.extract_features(back_white_bytes) if back_white_bytes else b''
feature_front_uv = extractor.extract_features(front_uv_bytes) if front_uv_bytes else b''
feature_back_uv = extractor.extract_features(back_uv_bytes) if back_uv_bytes else b''

上述代码表示,如果图像缺失,返回的默认值应该改为b''。

2.6 数据库初始化与迁移重置

由于数据库类型变更,原有的迁移脚本已失效。需要删除旧的迁移记录并重新生成。

  1. 清理环境 :删除项目目录下的 alembic/versions 文件夹内的所有 .py 文件,删除旧的 database.dbcard_db.sqlite 文件。
  2. 修改 alembic/env.py:确保其指向新的 SQLite 配置。

代码清单:alembic/env.py (修改部分)

python 复制代码
# ...
from database import DATABASE_URL, engine # 导入

def run_migrations_online() -> None:
    connectable = engine # 直接使用database.py中创建好的engine
    
    with connectable.connect() as connection:
        context.configure(
            connection=connection, target_metadata=target_metadata
        )
        with context.begin_transaction():
            context.run_migrations()
  1. 生成新迁移脚本并应用
bash 复制代码
alembic revision --autogenerate -m "init_sqlite"
# (可选) 手动检查生成的版本文件,确保包含 import sqlmodel
alembic upgrade head
  1. 数据初始化
    执行init_db.py(该脚本需确认已适配 pickle 序列化,与models.py一致)。
bash 复制代码
python3 init_db.py

至此,后端代码已完全切换回SQLite架构。

三、运维管理工具集

为了方便后续管理,需要添加一些运维管理工具脚本。

3.1 快速模板检索工具

脚本名称:tool_search_template.py

python 复制代码
import argparse
import pickle
import numpy as np
from sqlmodel import Session, select
from database import engine
from models import CertificateTemplate
from feature_extractor import ImageFeatureExtractor

def cosine_similarity(vec1: np.ndarray, vec2: np.ndarray) -> float:
    norm_vec1 = vec1 / np.linalg.norm(vec1)
    norm_vec2 = vec2 / np.linalg.norm(vec2)
    return float(np.dot(norm_vec1, norm_vec2))

def main():
    parser = argparse.ArgumentParser(description="快速样证模板检索工具(SQLite版)")
    parser.add_argument("--front", required=True, help="证件正面图像路径")
    parser.add_argument("--back", required=True, help="证件反面图像路径")
    args = parser.parse_args()

    extractor = ImageFeatureExtractor(model_path="mobilenetv3_finetuned.pth")

    try:
        with open(args.front, "rb") as f:
            front_bytes = f.read()
        with open(args.back, "rb") as f:
            back_bytes = f.read()
    except FileNotFoundError as e:
        print(f"错误:{e}")
        return

    # 提取特征并反序列化
    query_feature_front = pickle.loads(extractor.extract_features(front_bytes))
    query_feature_back = pickle.loads(extractor.extract_features(back_bytes))

    best_match = None
    max_score = -1.0

    with Session(engine) as session:
        templates = session.exec(select(CertificateTemplate)).all()
        for template in templates:
            # 数据库字段直接反序列化
            feat_front = pickle.loads(template.feature_front_white)
            feat_back = pickle.loads(template.feature_back_white)

            score_front = cosine_similarity(query_feature_front, feat_front)
            score_back = cosine_similarity(query_feature_back, feat_back)
            avg_score = (score_front + score_back) / 2.0

            if avg_score > max_score:
                max_score = avg_score
                best_match = template

    if best_match:
        print(f"匹配结果:成功 | 模板:{best_match.name} | 分数:{max_score:.4f}")
    else:
        print("未找到匹配模板")

if __name__ == "__main__":
    main()

使用时可以使用下面的命令:

bash 复制代码
python3 tool_search_template.py --front ./test/12_06_57_front_white.jpg --back ./test/12_06_57_back_white.jpg

3.2 样证数据批量导出工具

脚本名称:tool_export_samples.py

python 复制代码
import argparse
import os
from datetime import datetime
from pathlib import Path
from sqlmodel import Session, select
from database import engine
from models import SampleRecord

def save_image(data: bytes, path: Path):
    if data:
        with open(path, "wb") as f:
            f.write(data)

def main():
    parser = argparse.ArgumentParser(description="样证导出工具")
    parser.add_argument("--start", default="1970-01-01")
    parser.add_argument("--end", default="2099-12-31")
    parser.add_argument("--out", default="./exported_samples")
    args = parser.parse_args()

    start_date = datetime.strptime(args.start, "%Y-%m-%d")
    end_date = datetime.strptime(args.end, "%Y-%m-%d").replace(hour=23, minute=59, second=59)
    output_root = Path(args.out)
    output_root.mkdir(parents=True, exist_ok=True)

    with Session(engine) as session:
        statement = select(SampleRecord).where(
            SampleRecord.created_at >= start_date,
            SampleRecord.created_at <= end_date
        )
        records = session.exec(statement).all()

        for record in records:
            date_dir = output_root / record.created_at.strftime("%Y-%m-%d")
            date_dir.mkdir(exist_ok=True)
            base_name = f"{record.id}_{record.country_code}"
            
            save_image(record.image_front_white, date_dir / f"{base_name}_front_white.jpg")
            save_image(record.image_back_white, date_dir / f"{base_name}_back_white.jpg")
            save_image(record.image_front_uv, date_dir / f"{base_name}_front_uv.jpg")
            save_image(record.image_back_uv, date_dir / f"{base_name}_back_uv.jpg")
            
            # 4. 生成详细的描述文本文件
            txt_path = date_dir / f"{base_name}_info.txt"
            with open(txt_path, "w", encoding="utf-8") as f:
                f.write(f"记录ID: {record.id}\n")
                f.write(f"国家代码: {record.country_code}\n")
                f.write(f"设备序列号 (SN): {record.device_sn}\n")
                # 格式化时间显示
                f.write(f"上传时间: {record.created_at.strftime('%Y-%m-%d %H:%M:%S')}\n")
                # 将布尔值转换为易读的中文状态
                f.write(f"人工核验结果: {'存疑' if record.is_suspicious else '正常'}\n")
                # 处理备注为空的情况
                f.write(f"详细备注: {record.remarks if record.remarks else '无'}\n")

    print(f"导出完成,共 {len(records)} 条。")

if __name__ == "__main__":
    main()

使用时可以使用下面的命令:

bash 复制代码
python3 tool_export_samples.py --start 2019-08-01 --end 2026-08-01 --out ./exported_samples

3.3 数据库重置工具

针对SQLite,重置数据库较为简单,直接删除文件重建即可。

脚本名称:tool_reset_db.py

python 复制代码
import os
import sys
from database import SQLITE_FILE_NAME
import init_db

def main():
    # 1. 删除数据库文件
    if os.path.exists(SQLITE_FILE_NAME):
        os.remove(SQLITE_FILE_NAME)
        print("数据库文件已删除。")

    # 2. 重新应用迁移 (确保 alembic.ini 配置正确)
    os.system("alembic upgrade head")

    # 3. 初始化数据
    init_db.main()
    print("数据库重置完成。")

if __name__ == "__main__":
    main()

如果模型重新训练了或者samples文件夹中的图像发生了很多变化,可以使用python3 tool_reset_db.py完成整个数据库的重建。

3.4 大模型测试工具

脚本名称:tool_test_llm.py

python 复制代码
import argparse
import base64
import json
import requests
import os
import sys

def encode_image(image_path):
    """
    读取图像文件并转换为Base64字符串
    """
    if not os.path.exists(image_path):
        print(f"错误:文件不存在 - {image_path}")
        sys.exit(1)
    
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

def main():
    parser = argparse.ArgumentParser(description="大模型版面识别接口验证工具")
    parser.add_argument("--front", required=True, help="证件正面白光图像路径")
    parser.add_argument("--back", required=True, help="证件反面白光图像路径")
    parser.add_argument("--host", default="http://127.0.0.1:8001", help="后端服务地址")
    args = parser.parse_args()

    print("正在读取并编码图像...")
    front_b64 = encode_image(args.front)
    back_b64 = encode_image(args.back)

    # 构建请求Payload
    # 注意:API要求提供完整的多光谱图像字段。
    # 为简化测试,此处将白光图像填充至紫外和红外字段,仅用于通过格式校验。
    payload = {
        "country_code": "840",  # 设置为国外代码(如美国)以触发LLM流程
        "enable_llm": True,     # 强制开启大模型识别
        "image_front_white": front_b64,
        "image_front_uv": front_b64,  # 占位填充
        "image_front_ir": front_b64,  # 占位填充
        "image_back_white": back_b64,
        "image_back_uv": back_b64,    # 占位填充
        "image_back_ir": back_b64     # 占位填充
    }

    api_url = f"{args.host}/api/recognize"
    print(f"正在向 {api_url} 发送请求,请稍候(大模型推理可能需要数秒)...")

    try:
        response = requests.post(api_url, json=payload, headers={"Content-Type": "application/json"})
        
        if response.status_code == 200:
            result = response.json()
            print("-" * 30)
            print("【请求成功】")
            print(f"状态码 (Code): {result.get('code')}")
            
            # 提取并打印核心消息
            message = result.get('message', '')
            print("\n>>> 识别结果详情 <<<")
            print(message)
            
            # 检查是否包含大模型返回的版面信息
            if "--- 版面信息 ---" in message:
                print("\n[验证结论]: 大模型调用成功,版面信息已返回。")
            else:
                print("\n[验证结论]: 警告 - 未检测到版面信息,请检查大模型服务状态或国家代码配置。")
                
        else:
            print("-" * 30)
            print(f"【请求失败】 HTTP状态码: {response.status_code}")
            print(f"错误信息: {response.text}")

    except requests.exceptions.ConnectionError:
        print(f"错误:无法连接到后端服务 {args.host},请确认服务已启动。")
    except Exception as e:
        print(f"发生未预期的错误: {e}")

if __name__ == "__main__":
    main()

使用时可以使用下面的命令:

bash 复制代码
python3 tool_test_llm.py --front ./test/12_06_57_front_white.jpg --back ./test/12_06_57_back_white.jpg --host http://127.0.0.1:8001

四、构建离线Docker镜像

采用"单体容器"策略,将FastAPI服务与SQLite数据库文件封装在同一镜像中,无需安装额外的数据库服务软件。

4.1 拉取基础镜像并启动容器

在联网机器上操作:

bash 复制代码
# 1. 拉取基础镜像(由于网络限制,暂定从hub.rat.dev来获取)
sudo docker pull hub.rat.dev/ubuntu:22.04

# 2. 启动构建容器
sudo docker run -dit --net=host --name card_system_build --shm-size="8g" hub.rat.dev/ubuntu:22.04 bash

# 3. 进入容器
sudo docker exec -it card_system_build bash

4.2 容器内环境初始化

在容器内部安装依赖。

bash 复制代码
# 1. 更新源
apt-get update

# 2. 安装Python及基础工具
apt-get install -y \
    python3 \
    python3-pip \
    python3-venv \
    vim \
    libgl1-mesa-glx \
    libglib2.0-0 \
    libgomp1 \
    tzdata

# 3. 设置时区
ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
dpkg-reconfigure -f noninteractive tzdata

4.3 部署代码与环境

bash 复制代码
# 1. 创建目录
mkdir -p /code/foreign_card

# --- 此时需在宿主机开启新终端,将代码复制进容器 ---
# cd ForeignCardDetec 
# sudo docker cp ./ card_system_build:/code/foreign_card/

# 2. 回到容器终端,安装Python依赖
cd /code/foreign_card
# 优先安装PyTorch CPU版以减小体积(需要参照torch官网的命令下载对应的cpu版本)
pip3 install torch torchvision --index-url https://download.pytorch.org/whl/cpu
# 安装其他依赖
pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

# 3. 初始化数据库
# 在容器内直接执行,生成 card_db.sqlite 文件
rm -rf alembic/versions/*  # 确保清理旧迁移
rm -f card_db.sqlite
alembic revision --autogenerate -m "init_docker_sqlite"
# 在生成的迁移脚本头部补充 import sqlmodel (可通过 sed 命令自动处理)
sed -i '1s/^/import sqlmodel\n/' alembic/versions/*.py
alembic upgrade head
python3 init_db.py

4.4 导出离线镜像

bash 复制代码
# 1. 提交镜像
sudo docker commit card_system_build card_system_sqlite:v1.0

# 2. 导出为 tar 包
sudo docker save -o card_system_sqlite_v1.0.tar card_system_sqlite:v1.0

# 3. 赋权
sudo chmod 777 card_system_sqlite_v1.0.tar

四、内网环境离线部署

card_system_sqlite_v1.0.tar 拷贝至内网服务器。

bash 复制代码
# 1. 加载镜像
sudo docker load -i card_system_sqlite_v1.0.tar

# 2. 启动容器
sudo docker run -dit --net=host --name card_system_prod --shm-size="8g" card_system_sqlite:v1.0 bash

# 3. 进入容器并移动数据库文件(首次部署)
sudo docker exec -it card_system_prod bash
# 如果挂载目录为空,将构建时生成的数据库移动过去,并建立软链接
if [ ! -f "/code/foreign_card/db_data/card_db.sqlite" ]; then
    mv /code/foreign_card/card_db.sqlite /code/foreign_card/db_data/
fi
# 确保代码读取的是挂载目录下的数据库
ln -sf /code/foreign_card/db_data/card_db.sqlite /code/foreign_card/card_db.sqlite

# 4. 启动服务
cd /code/foreign_card
nohup uvicorn main:app --host 0.0.0.0 --port 8001 > server.log 2>&1 &

五、总结

本篇博客通过回归SQLite数据库,极大地简化了"全球证件智能识别系统"在内网环境的部署架构。通过单体Docker容器的构建与分发,消除了复杂的数据库服务配置和依赖管理,实现了真正的"零配置"交付。配套开发的运维工具集,确保了系统上线后在数据维护、故障排查和模型迭代方面的可操作性,为项目的最终落地提供了坚实的工程保障。

相关推荐
济6172 小时前
linux(第七期)--gcc编译软件-- Ubuntu20.04
linux·运维·服务器
Sheffield3 小时前
今天浅浅的回顾一下Ansible吧
运维
DeepFlow 零侵扰全栈可观测4 小时前
3分钟定位OA系统GC瓶颈:DeepFlow全栈可观测平台实战解析
大数据·运维·人工智能·云原生·性能优化
一点晖光4 小时前
jenkins 流水线脚本
运维·jenkins
望舒同学4 小时前
Docker上云踩坑实录
docker·全栈
济6174 小时前
linux(第九期)--交叉编译器-- Ubuntu20.04
linux·运维·服务器
zxdzxdzzxd4 小时前
Tailscale Linux 登录指南
linux·运维·服务器
Knight_AL4 小时前
MinIO 入门实战:Docker 安装 + Spring Boot 文件上传(公有 / 私有)
spring boot·docker·容器
虚神界熊孩儿5 小时前
linux下创建用户和用户组常用命令
linux·运维·创建用户
咕噜签名-铁蛋5 小时前
云服务器GPU:释放AI时代的算力引擎
运维·服务器·人工智能