diff --git a/.dockerignore b/.dockerignore index 62957036..5a807be9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,7 +15,12 @@ __pycache__ # 虚拟环境 venv .venv +# 环境变量文件:必须排除所有真实密钥文件(.env 只能匹配到名为 .env 的文件, +# 匹配不到 .env.prod / .env.dev / .env.test —— 它们含真实口令与 API Key) .env +**/.env +**/.env.* +!**/.env.example # 编辑器/IDE .vscode diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml new file mode 100644 index 00000000..2a8a8268 --- /dev/null +++ b/.github/workflows/backend-ci.yml @@ -0,0 +1,36 @@ +name: backend + +on: + push: + paths: + - "backend/**" + - ".github/workflows/backend-ci.yml" + pull_request: + paths: + - "backend/**" + - ".github/workflows/backend-ci.yml" + workflow_dispatch: + +defaults: + run: + working-directory: backend + +jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: 安装 uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: 安装依赖(含 sqlite 与 mysql 驱动;测试用 sqlite + fakeredis) + run: uv sync --extra sqlite --extra mysql + + - name: 代码检查(ruff) + run: uv run ruff check + + - name: 单元测试(pytest) + run: uv run pytest -q diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml new file mode 100644 index 00000000..e17a2bce --- /dev/null +++ b/.github/workflows/frontend-ci.yml @@ -0,0 +1,66 @@ +name: frontend + +on: + push: + paths: + - "frontend/**" + - ".github/workflows/frontend-ci.yml" + pull_request: + paths: + - "frontend/**" + - ".github/workflows/frontend-ci.yml" + workflow_dispatch: + +jobs: + web: + name: web(类型检查 + 单元测试) + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend/web + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: frontend/web/pnpm-lock.yaml + + - name: 安装依赖 + run: pnpm install --frozen-lockfile + + - name: 类型检查 + run: pnpm run type-check + + - name: 单元测试 + run: pnpm run test + + app: + name: app(类型检查) + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend/app + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: frontend/app/pnpm-lock.yaml + + - name: 安装依赖 + run: pnpm install --frozen-lockfile + + - name: 类型检查 + run: pnpm run type-check diff --git a/.gitignore b/.gitignore index b22dbf4a..a0b35295 100755 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,6 @@ CLAUDE.md AGENTS.md .codebuddy .agent -.workbuddy \ No newline at end of file +.workbuddy +# AgentTeams 协作运行期状态(本地,不入库) +.agent-teams/ diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 00000000..f53337a2 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,119 @@ +# FastapiAdmin 部署运维手册 + +> 适用环境:阿里云单机 Docker 部署(`service.fastapiadmin.com`) +> 最近更新:2026-10-04(本轮密钥轮换与加固之后) + +--- + +## 1. 架构与目录 + +| 组件 | 说明 | 对外端口 | +| --- | --- | --- | +| nginx | TLS 终止、静态站点、反向代理 `/api/v1` | 80 / 443 | +| backend | FastAPI(uvicorn,1 worker,非 root 运行 uid=1000) | 仅 127.0.0.1:8001 | +| mysql | MySQL 8.0,数据卷 `mysql_data` | 仅 127.0.0.1:3306 | +| redis | Redis 7(会话/缓存/限流) | 仅 127.0.0.1:6379 | + +服务器布局(`/home/FastapiAdmin`): + +``` +docker/ # 唯一部署目录,不含业务源码 +├── docker-compose.yaml # 服务编排(端口仅 127.0.0.1,密钥由 .env 注入) +├── docker-compose.override.yaml # nginx 版本固定 + 缓存配置挂载 + ACME 目录 +├── .env # 真实凭据(600,不入库) +├── .env.example # 占位模板(入库) +├── backend/Dockerfile # 源码构建路径(正式部署不用它) +├── nginx/ +│ ├── nginx.conf # 站点配置(含文档封堵、隐藏文件拒绝、ACME 放行) +│ ├── cache-headers.inc # 静态资源长缓存片段 +│ ├── acme/ # ACME http-01 校验目录 +│ ├── ssl/ # 证书与私钥(600,不入库) +│ └── {web,app,docs}/dist # 三端静态成品(由本地构建后上传) +└── mysql/ redis/ # 初始化 SQL 与 redis.conf +backups/ # 部署前自动备份(每类保留 5 份) +``` + +仓库侧:`deploy-artifacts.sh`(本机执行的成品部署脚本)、`backend/requirements/`(依赖分组)、`.github/workflows/`(CI)。 + +--- + +## 2. 日常部署 + +在本机仓库根目录执行(需要本机可免密 SSH 到服务器、且本机 Docker 可用——`colima start`): + +```bash +./deploy-artifacts.sh web # 构建并部署 Web 管理后台 +./deploy-artifacts.sh app # 构建并部署移动端 H5 +./deploy-artifacts.sh backend # 本地构建 MySQL 版镜像 → 传输 → 只重建 backend +./deploy-artifacts.sh all # 三样全发 +./deploy-artifacts.sh verify # 公网四端验证 +./deploy-artifacts.sh backups # 列出服务器备份 +./deploy-artifacts.sh prune # 轮转清理旧备份与旧镜像 +./deploy-artifacts.sh rollback web|app|backend +``` + +要点: + +- **服务器不接收源码、不执行构建**:后端以 `docker save → ssh → docker load` 交付镜像,前端以 tar 管道交付成品。 +- 每次部署前自动备份到 `backups/<标签>-<时间戳>`,备份失败即中止,不会带着假备份上线。 +- 只重建变化的容器;MySQL/Redis/Nginx 不受影响。 +- PostgreSQL 版本镜像:`./deploy-artifacts.sh backend-pg`(**只构建与自检,不切换线上**)。切换需人工把 `BACKEND_IMAGE_TAG` 指到该镜像并确保 compose 的数据库配置已改。 + +--- + +## 3. 密钥与口令 + +| 项 | 位置 | 说明 | +| --- | --- | --- | +| `SECRET_KEY` | 服务器 `docker/.env` | JWT 签名密钥,64 位 hex,随机生成 | +| `DATA_ENCRYPTION_KEY` | 服务器 `docker/.env` | 落库敏感数据加密主密钥 | +| `DATA_ENCRYPTION_OLD_KEYS` | 服务器 `docker/.env` | 轮换前的旧密钥,仅用于解密历史密文 | +| 种子账号口令 | 服务器 `/root/.fa-seed-passwords`(600) | `super`/`admin`/`user`,登录后请自行修改并删除该文件 | + +**轮换密钥的正确步骤**(不可跳步,否则历史密文无法解密): + +1. 把当前 `DATA_ENCRYPTION_KEY`(未设置时取当前 `SECRET_KEY`)追加到 `DATA_ENCRYPTION_OLD_KEYS`; +2. 生成新值:`openssl rand -hex 32`,写入 `SECRET_KEY` / `DATA_ENCRYPTION_KEY`; +3. `docker compose up -d --force-recreate backend`; +4. 验证历史密文仍可解密(例如存储源口令),再删除 `OLD_KEYS` 中不再需要的旧值。 + +> 注意:更换 `SECRET_KEY` 会使已签发的令牌全部失效,用户需重新登录。 + +--- + +## 4. 运行状态与巡检 + +- **每日巡检**:`systemctl list-timers fa-health-check.timer`(每天 08:30,带 5 分钟随机延迟) +- 巡检内容:4 个容器健康、`/api/v1/monitor/health/check` 与 `/web/`、`/app/` 可用性、证书剩余天数、磁盘、swap、备份与镜像堆积 +- 结果:`/var/log/fa-health.log`;有告警时写入 syslog(`journalctl -t fa-health`)且服务退出码非 0 +- 手动执行:`systemctl start fa-health-check.service && tail -n 30 /var/log/fa-health.log` + +常用排查命令: + +```bash +cd /home/FastapiAdmin/docker +docker compose ps +docker logs --tail 100 backend +docker inspect backend --format '{{.State.Health.Status}}' +ss -tlnp | grep docker-proxy # 确认对外只有 80/443 +``` + +--- + +## 5. 证书 + +- 当前证书:`docker/nginx/ssl/server.pem`(`CN=service.fastapiadmin.com`),由巡检脚本监控到期天数 +- 续期(重新签发后):替换 `server.pem` 与 `server.key` → `docker exec nginx nginx -s reload` +- 已放行 ACME http-01 校验路径(`/.well-known/acme-challenge/` → `docker/nginx/acme`),如需切换到 acme.sh 自动续期,把校验文件写入该目录即可,无需再改 nginx 配置 + +--- + +## 6. 已知遗留问题 + +安全与质量债的完整清单见 `AUDIT-REPORT.md` 与各分报告(`backend/audit-*.md`、`frontend/audit-*.md`、`docker/audit-deploy.md`)。当前仍待处理的高优先级项: + +1. 会话失效机制:停用/删除/改密后旧会话仍有效(最长 7 天)——需应用侧改造; +2. 数据权限注入不完整:`CRUDBase` 的删除/清空/改状态路径未注入数据权限; +3. Web i18n 与通知状态枚举:`85/101` 个页面未接入 i18n;通知状态枚举与后端语义冲突; +4. 测试覆盖:后端仅 2 个用例,夹具掩盖缺陷(见 `backend/audit-testing.md`); +5. 多租户需求与实现错位(见 `backend/audit-requirements.md`)。 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 09366cd9..bd12c10e 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -32,11 +32,7 @@ dependencies = [ "ua-parser==1.0.2", # 解析 User-Agent 获取 OS/浏览器 "uvicorn==0.49.0", # uvicorn web 框架 "websockets>=16.0,<17.0", # websocket 通信 - "aiosqlite==0.22.1", - "aiomysql>=0.2.0", - "asyncpg==0.31.0", - "psycopg[binary]==3.3.2", - "pymysql==1.2.0", + "aiosqlite==0.22.1", # 内嵌知识库(app/modules/ai/chat)固定使用 sqlite,故列入核心 "openai==2.46.0", "pydantic[email]>=2.12.5", "cryptography==50.0.0", # Fernet 对称加密(存储源密码加密) @@ -47,10 +43,28 @@ dependencies = [ "cos-python-sdk-v5==1.9.44", ] +# 数据库驱动按需安装:uv sync --extra mysql / --extra postgres / --extra sqlite +# 默认(不指定 extra)只装核心依赖,启动时会提示缺少对应驱动 [project.optional-dependencies] -mysql = ["aiomysql>=0.2.0"] -postgres = ["asyncpg==0.31.0"] -sqlite = ["aiosqlite==0.22.1"] +mysql = [ + "aiomysql>=0.2.0", # 异步驱动(ASYNC_DB_URI) + "pymysql==1.2.0", # 同步驱动(DB_URI) +] +postgres = [ + "asyncpg==0.31.0", # 异步驱动(ASYNC_DB_URI) + "psycopg[binary]==3.3.2", # 同步驱动(DB_URI,自带 libpq) +] +sqlite = [ + "aiosqlite==0.22.1", +] +# 一个进程同时支持三种库(体积最大,适合本地开发) +all-db = [ + "aiomysql>=0.2.0", + "pymysql==1.2.0", + "asyncpg==0.31.0", + "psycopg[binary]==3.3.2", + "aiosqlite==0.22.1", +] [dependency-groups] dev = [ diff --git a/backend/requirements.txt b/backend/requirements.txt index d09a0a70..0bb421cc 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,41 +1,14 @@ -# 与 pyproject.toml [project] dependencies 一致(生产 pip;不含 dev 组:pytest / ruff / fakeredis) -agno==2.5.8 # AI agent框架 -aiofiles==24.1.0 # 文件操作 -aiosqlite==0.22.1 # sqlite 异步操作数据库 -alembic==1.18.4 # 数据库迁移 -apscheduler==3.11.0 # 定时任务 -croniter==6.2.4 # cron 表达式校验 -aiomysql>=0.2.0 # 异步mysql操作数据库 -asyncpg==0.31.0 # 异步postgresql操作数据库 -bleach==6.4.0 # 过滤 HTML 标签 -tinycss2==1.5.1 # bleach CSS 清洗依赖 -click==8.1.7 # 命令行参数解析 -fastapi==0.138.2 # fastapi 框架 -greenlet==3.5.2 # 协程核心依赖(SQLAlchemy 异步) -httpx==0.28.1 # HTTP 客户端 -jinja2==3.1.6 # 模板引擎 -loguru==0.7.3 # 日志 -openai==2.46.0 # OpenAI API -openpyxl==3.1.5 # Excel -psutil==7.2.2 # 系统信息 -py-ip2region==3.0.4 # IP归属地离线查询(ip2region官方binding,内置xdb本地解析) -psycopg[binary]==3.3.2 # postgresql 同步驱动(含预编译 libpq,开箱即用) -pydantic[email]>=2.12.5 # 邮箱校验 -pydantic-settings>=2.6.1,<3 # 配置设置 -pyjwt==2.13.0 # OAuth2 -pymysql==1.2.0 # MySQL 异步操作数据库 -python-multipart==0.0.32 # request.form() 对表单进行「解析」时安装 -redis==7.1.0 # Redis 异步操作数据库 -rich==15.0.0 # 终端打印美化 -sqlalchemy==2.0.51 # 数据库ORM -sqlglot[rs]==27.8.0 # sql 解析 -typer==0.26.7 # 命令行工具 -ua-parser==1.0.2 # 获取用户UA -uvicorn==0.49.0 # ASGI 框架 -websockets>=16.0,<17.0 # websocket 通信 -cryptography==50.0.0 # Fernet 对称加密(存储源密码加密) -paramiko==5.0.0 # SFTP 协议 -boto3==1.43.72 # S3 协议 -esdk-obs-python==3.26.6 # 华为云 OBS 协议 -alibabacloud-oss-v2==1.3.2 # 阿里云 OSS 协议 -cos-python-sdk-v5==1.9.44 # 腾讯云 COS 协议 +# ============================================================================ +# 完整依赖入口(核心 + 全部数据库驱动 + 对象存储) +# 适用于:源码部署、本机开发、以及需要「一个镜像同时支持 mysql/postgres/sqlite」的场景。 +# +# 想要更小的生产镜像,请按需组合,例如: +# MySQL : -r requirements/base.txt -r requirements/db/mysql.txt -r requirements/storage.txt +# Postgres : -r requirements/base.txt -r requirements/db/postgres.txt -r requirements/storage.txt +# SQLite : -r requirements/base.txt -r requirements/db/sqlite.txt +# ============================================================================ +-r requirements/base.txt +-r requirements/db/mysql.txt +-r requirements/db/postgres.txt +-r requirements/db/sqlite.txt +-r requirements/storage.txt diff --git a/backend/requirements/base.txt b/backend/requirements/base.txt new file mode 100644 index 00000000..44b7e2fc --- /dev/null +++ b/backend/requirements/base.txt @@ -0,0 +1,34 @@ +# ============================================================================ +# 核心依赖(与数据库、对象存储无关,任何部署方式都需要) +# 用法:pip install -r requirements/base.txt -r requirements/db/<你的数据库>.txt +# ============================================================================ +agno==2.5.8 # AI agent 框架 +aiosqlite==0.22.1 # AI 模块内嵌知识库固定用 sqlite(与主库类型无关),故列入核心 +aiofiles==24.1.0 # 异步文件操作 +alembic==1.18.4 # 数据库迁移 +apscheduler==3.11.0 # 定时任务 +bleach==6.4.0 # HTML 过滤 +click==8.1.7 # 命令行参数解析(typer 依赖) +croniter==6.2.4 # cron 表达式校验 +fastapi==0.138.2 # Web 框架 +greenlet==3.5.2 # SQLAlchemy 异步必需 +httpx==0.28.1 # HTTP 客户端 +jinja2==3.1.6 # 模板引擎(代码生成) +loguru==0.7.3 # 日志 +openai==2.46.0 # OpenAI 兼容接口 +openpyxl==3.1.5 # Excel 导入导出 +psutil==7.2.2 # 系统信息采集 +py-ip2region==3.0.4 # IP 归属地离线查询 +pydantic[email]>=2.12.5 # 数据校验 + 邮箱 +pydantic-settings>=2.6.1,<3 # 配置管理 +pyjwt==2.13.0 # 令牌签发校验 +python-multipart==0.0.32 # 表单解析 +redis==7.1.0 # 缓存 / 会话 +rich==15.0.0 # 终端输出美化 +sqlalchemy==2.0.51 # ORM +sqlglot[rs]==27.8.0 # SQL 解析 +tinycss2==1.5.1 # bleach 依赖 +typer==0.26.7 # CLI 入口(main.py) +ua-parser==1.0.2 # UA 解析 +uvicorn==0.49.0 # ASGI 服务器 +websockets>=16.0,<17.0 # WebSocket diff --git a/backend/requirements/db/mysql.txt b/backend/requirements/db/mysql.txt new file mode 100644 index 00000000..b50b8e82 --- /dev/null +++ b/backend/requirements/db/mysql.txt @@ -0,0 +1,7 @@ +# ============================================================================ +# MySQL 驱动(DATABASE_TYPE=mysql 时必需) +# aiomysql / pymysql:异步与同步连接,分别对应 ASYNC_DB_URI 与 DB_URI +# 用法:pip install -r requirements/base.txt -r requirements/db/mysql.txt +# ============================================================================ +aiomysql>=0.2.0 # 异步驱动(ASYNC_DB_URI: mysql+aiomysql://) +pymysql==1.2.0 # 同步驱动(DB_URI: mysql+pymysql://,迁移与后台任务用) diff --git a/backend/requirements/db/postgres.txt b/backend/requirements/db/postgres.txt new file mode 100644 index 00000000..31e8a0f6 --- /dev/null +++ b/backend/requirements/db/postgres.txt @@ -0,0 +1,9 @@ +# ============================================================================ +# PostgreSQL 驱动(DATABASE_TYPE=postgres 时必需) +# asyncpg:异步驱动,对应 ASYNC_DB_URI(postgresql+asyncpg://) +# psycopg[binary]:同步驱动(含预编译 libpq),对应 DB_URI(postgresql+psycopg://) +# 注意:psycopg[binary] 自带 libpq,无需在系统里额外安装 postgresql 客户端库 +# 用法:pip install -r requirements/base.txt -r requirements/db/postgres.txt +# ============================================================================ +asyncpg==0.31.0 # 异步驱动 +psycopg[binary]==3.3.2 # 同步驱动 diff --git a/backend/requirements/db/sqlite.txt b/backend/requirements/db/sqlite.txt new file mode 100644 index 00000000..c0af0c85 --- /dev/null +++ b/backend/requirements/db/sqlite.txt @@ -0,0 +1,6 @@ +# ============================================================================ +# SQLite 驱动(DATABASE_TYPE=sqlite 时必需) +# 适用于本地开发与单机小规模部署;AINI 的 AI 模块内嵌知识库也复用该驱动 +# 用法:pip install -r requirements/base.txt -r requirements/db/sqlite.txt +# ============================================================================ +aiosqlite==0.22.1 # 异步驱动(sqlite+aiosqlite:///) diff --git a/backend/requirements/storage.txt b/backend/requirements/storage.txt new file mode 100644 index 00000000..65a19aea --- /dev/null +++ b/backend/requirements/storage.txt @@ -0,0 +1,11 @@ +# ============================================================================ +# 对象存储与文件传输(本地存储源之外的能力;存储源配置里选用 S3 / OSS / COS / OBS / SFTP 时必需) +# 仅使用「本地存储源」的部署可以不装这一组,镜像可再小约 45MB +# 用法:pip install -r requirements/base.txt -r requirements/db/mysql.txt -r requirements/storage.txt +# ============================================================================ +boto3==1.43.72 # S3 及兼容协议 +alibabacloud-oss-v2==1.3.2 # 阿里云 OSS +cos-python-sdk-v5==1.9.44 # 腾讯云 COS +esdk-obs-python==3.26.6 # 华为云 OBS +paramiko==5.0.0 # SFTP +cryptography==50.0.0 # 存储源密码的 Fernet 加密 diff --git a/backend/uv.lock b/backend/uv.lock index 5b1e2b05..50cc3684 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -184,12 +184,10 @@ source = { virtual = "." } dependencies = [ { name = "agno" }, { name = "aiofiles" }, - { name = "aiomysql" }, { name = "aiosqlite" }, { name = "alembic" }, { name = "alibabacloud-oss-v2" }, { name = "apscheduler" }, - { name = "asyncpg" }, { name = "bleach" }, { name = "boto3" }, { name = "click" }, @@ -206,12 +204,10 @@ dependencies = [ { name = "openpyxl" }, { name = "paramiko" }, { name = "psutil" }, - { name = "psycopg", extra = ["binary"] }, { name = "py-ip2region" }, { name = "pydantic", extra = ["email"] }, { name = "pydantic-settings" }, { name = "pyjwt" }, - { name = "pymysql" }, { name = "python-multipart" }, { name = "redis" }, { name = "rich" }, @@ -225,11 +221,20 @@ dependencies = [ ] [package.optional-dependencies] +all-db = [ + { name = "aiomysql" }, + { name = "aiosqlite" }, + { name = "asyncpg" }, + { name = "psycopg", extra = ["binary"] }, + { name = "pymysql" }, +] mysql = [ { name = "aiomysql" }, + { name = "pymysql" }, ] postgres = [ { name = "asyncpg" }, + { name = "psycopg", extra = ["binary"] }, ] sqlite = [ { name = "aiosqlite" }, @@ -247,14 +252,15 @@ dev = [ requires-dist = [ { name = "agno", specifier = "==2.5.8" }, { name = "aiofiles", specifier = "==24.1.0" }, - { name = "aiomysql", specifier = ">=0.2.0" }, + { name = "aiomysql", marker = "extra == 'all-db'", specifier = ">=0.2.0" }, { name = "aiomysql", marker = "extra == 'mysql'", specifier = ">=0.2.0" }, { name = "aiosqlite", specifier = "==0.22.1" }, + { name = "aiosqlite", marker = "extra == 'all-db'", specifier = "==0.22.1" }, { name = "aiosqlite", marker = "extra == 'sqlite'", specifier = "==0.22.1" }, { name = "alembic", specifier = "==1.18.4" }, { name = "alibabacloud-oss-v2", specifier = "==1.3.2" }, { name = "apscheduler", specifier = "==3.11.0" }, - { name = "asyncpg", specifier = "==0.31.0" }, + { name = "asyncpg", marker = "extra == 'all-db'", specifier = "==0.31.0" }, { name = "asyncpg", marker = "extra == 'postgres'", specifier = "==0.31.0" }, { name = "bleach", specifier = "==6.4.0" }, { name = "boto3", specifier = "==1.43.72" }, @@ -272,12 +278,14 @@ requires-dist = [ { name = "openpyxl", specifier = "==3.1.5" }, { name = "paramiko", specifier = "==5.0.0" }, { name = "psutil", specifier = "==7.2.2" }, - { name = "psycopg", extras = ["binary"], specifier = "==3.3.2" }, + { name = "psycopg", extras = ["binary"], marker = "extra == 'all-db'", specifier = "==3.3.2" }, + { name = "psycopg", extras = ["binary"], marker = "extra == 'postgres'", specifier = "==3.3.2" }, { name = "py-ip2region", specifier = "==3.0.4" }, { name = "pydantic", extras = ["email"], specifier = ">=2.12.5" }, { name = "pydantic-settings", specifier = ">=2.6.1,<3" }, { name = "pyjwt", specifier = "==2.13.0" }, - { name = "pymysql", specifier = "==1.2.0" }, + { name = "pymysql", marker = "extra == 'all-db'", specifier = "==1.2.0" }, + { name = "pymysql", marker = "extra == 'mysql'", specifier = "==1.2.0" }, { name = "python-multipart", specifier = "==0.0.32" }, { name = "redis", specifier = "==7.1.0" }, { name = "rich", specifier = "==15.0.0" }, @@ -289,7 +297,7 @@ requires-dist = [ { name = "uvicorn", specifier = "==0.49.0" }, { name = "websockets", specifier = ">=16.0,<17.0" }, ] -provides-extras = ["mysql", "postgres", "sqlite"] +provides-extras = ["mysql", "postgres", "sqlite", "all-db"] [package.metadata.requires-dev] dev = [ diff --git a/deploy-artifacts.sh b/deploy-artifacts.sh new file mode 100755 index 00000000..be33fdd7 --- /dev/null +++ b/deploy-artifacts.sh @@ -0,0 +1,369 @@ +#!/usr/bin/env bash +# ============================================================================ +# FastapiAdmin 成品部署脚本(本地构建 → 服务器只接收成品) +# +# 设计目标: +# 1. 服务器上不放业务源码、不跑构建,只接收构建好的成品; +# 2. 后端用「镜像」交付(docker save | ssh docker load),前端用 tar 管道交付; +# 3. 每次上线前先在服务器 backups/ 留一份备份,可一键回滚; +# 4. 只重建变化的那一个容器,MySQL/Redis/Nginx 不重启。 +# +# 用法: +# ./deploy-artifacts.sh web # Web 管理后台(nginx /web) +# ./deploy-artifacts.sh app # 移动端 H5(nginx /app) +# ./deploy-artifacts.sh backend # 构建并部署 MySQL 版后端镜像(切换线上) +# ./deploy-artifacts.sh backend-pg # 只构建并自检 PostgreSQL 版镜像,不切换线上 +# ./deploy-artifacts.sh all # 三样全发(后端默认 MySQL 版) +# ./deploy-artifacts.sh verify # 公网四端验证 +# ./deploy-artifacts.sh backups # 列出服务器上的备份 +# ./deploy-artifacts.sh rollback web # 回滚 Web 到最近一次备份 +# ./deploy-artifacts.sh rollback app # 回滚 H5 到最近一次备份 +# +# 前置条件: +# - 本机 SSH 密钥可免密登录 root@$FA_SERVER +# - backend 目标需要本机 docker(Colima / Docker Desktop 均可) +# - frontend/web、frontend/app 依赖已安装(pnpm install) +# +# 环境变量覆盖:FA_SERVER、FA_REMOTE_ROOT、FA_BACKUP_DIR +# ============================================================================ +set -euo pipefail + +SERVER="${FA_SERVER:-8.137.99.5}" +REMOTE_ROOT="${FA_REMOTE_ROOT:-/home/FastapiAdmin}" +BACKUP_DIR="${FA_BACKUP_DIR:-/home/FastapiAdmin/backups}" +BACKUP_KEEP="${FA_BACKUP_KEEP:-5}" # 每个标签最多保留的备份份数 +IMAGE_KEEP="${FA_IMAGE_KEEP:-2}" # 每类镜像最多保留的版本数(当前 + 回滚点) +STAMP="$(date +%Y%m%d-%H%M%S)" +KEY="$HOME/.ssh/id_ed25519" +SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15) + +log() { printf '\033[0;34m[%s]\033[0m %s\n' "$(date +%H:%M:%S)" "$*"; } +ok() { printf '\033[0;32m ✅ %s\033[0m\n' "$*"; } +die() { printf '\033[0;31m ❌ %s\033[0m\n' "$*"; exit 1; } + +ssh_do() { ssh "${SSH_OPTS[@]}" -i "$KEY" "root@${SERVER}" "$@"; } + +# 把本地目录内容流式发到服务器目标目录。 +# 服务器侧流程:解包到暂存区 → 校验入口文件 → 备份旧目录到 backups/