Compare commits

..

1 Commits

Author SHA1 Message Date
cab5181fd3 branch1: root-path deployment variant with chat UI hidden
- BASE_PATH="" for deployment at www.ityb.me root (no /careerbot prefix)
- Root redirect route only registered when BASE is non-empty
- Add secondary /careerbot/uploads mount for backward compat with
  trunk-uploaded photo URLs (shared DB scenario)
- Hide chat FAB and chat panel on index.html (display:none)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 12:15:58 +08:00
5 changed files with 74 additions and 152 deletions

View File

@ -9,21 +9,11 @@ CareerBot 是一个个人职业展示网站,集成了 AI 智能对话助手。
### 线上地址 ### 线上地址
| 用途 | 地址 | 分支 | 特性 | | 用途 | 地址 |
|------|------|------|------| |------|------|
| 展示版主页(无聊天) | `https://www.ityb.me/` | branch1 | 隐藏对话按钮,纯展示 | | 主页 | `http://www.ityb.me/careerbot/` |
| 完整版主页(含聊天) | `https://career.ityb.me/careerbot/` | main (trunk) | 含 AI 对话、JD 匹配、简历生成 | | 管理后台 | `http://www.ityb.me/careerbot/admin/login` |
| 管理后台 | `https://career.ityb.me/careerbot/admin/login` | main (trunk) | 管理员入口 | | Gitea 代码仓库 | `https://git.ityb.me` |
| Gitea 代码仓库 | `https://git.ityb.me` | - | 自托管 Git |
### 分支策略
| 分支 | 部署域名 | BASE_PATH | 用途 |
|------|---------|-----------|------|
| `main`trunk | `career.ityb.me` | `/careerbot` | 主线开发,保留全部功能 |
| `branch1` | `www.ityb.me` / `ityb.me` | `""`(根路径) | 展示变体,隐藏 AI 对话按钮,共享 trunk 的数据库和上传目录 |
数据层面两分支共用同一 SQLite 数据库和上传目录,后端资料同步,仅前端展示差异。
### 技术栈 ### 技术栈

View File

@ -37,9 +37,8 @@ ssh root@39.106.14.107 # root 直连
| 系统 | 地址 | 账号 | 密码 | | 系统 | 地址 | 账号 | 密码 |
|------|------|------|------| |------|------|------|------|
| CareerBot 展示版branch1 | `https://www.ityb.me/` | (访问令牌或匿名) | - | | CareerBot 主页 | `http://www.ityb.me/careerbot/` | (访问令牌或匿名) | - |
| CareerBot 完整版trunk | `https://career.ityb.me/careerbot/` | (访问令牌或匿名) | - | | CareerBot 管理后台 | `http://www.ityb.me/careerbot/admin/login` | `ln0422@gmail.com` | `qshs123456` |
| CareerBot 管理后台 | `https://career.ityb.me/careerbot/admin/login` | `ln0422@gmail.com` | `qshs123456` |
| Gitea 代码管理 | `https://git.ityb.me` | `ln0422` | `Qshs123456_` | | Gitea 代码管理 | `https://git.ityb.me` | `ln0422` | `Qshs123456_` |
### 2.3 ECS 用户密码 ### 2.3 ECS 用户密码
@ -55,39 +54,28 @@ ssh root@39.106.14.107 # root 直连
## 3. 服务架构 ## 3. 服务架构
``` ```
外部访问 外部访问 (www.ityb.me)
┌──────────────────────────────────────────────────────────────── ┌──────────────────────────────────────────────────────┐
│ Nginx (:80 / :443) │ Nginx (:80) │
│ 配置: /etc/nginx/sites-enabled/ │ │ 配置: /etc/nginx/sites-enabled/ │
│ 日志: /var/log/nginx/ │ │ 日志: /var/log/nginx/ │
├────────────────────────────────────────────────────────────────┤ ├──────────────────────────────────────────────────────┤
│ https://www.ityb.me/ (主域,展示版 branch1) │ │ http://www.ityb.me/ │
│ https://ityb.me/ ┤ │ │ → 302 重定向到 /careerbot/ │
│ → proxy_pass http://127.0.0.1:8001/ │
│ → CareerBot branch1 (uvicorn, BASE_PATH="", FAB 隐藏) │
│ │ │ │
│ https://www.ityb.me/careerbot/* → 404已主动屏蔽 │ http://www.ityb.me/careerbot/ │
│ 仅 /careerbot/uploads/* 由 branch1 后端兜底(供 trunk 上传 │
│ 的头像 URL 在 branch1 页面正常显示) │
├────────────────────────────────────────────────────────────────┤
│ https://career.ityb.me/careerbot/ (完整版 trunk) │
│ → proxy_pass http://127.0.0.1:8000/careerbot/ │ │ → proxy_pass http://127.0.0.1:8000/careerbot/ │
│ → CareerBot main (uvicorn, BASE_PATH="/careerbot", 完整功能) │ → CareerBot (uvicorn, FastAPI prefix=/careerbot) │
├──────────────────────────────────────────────────────────────── ├──────────────────────────────────────────────────────┤
│ https://git.ityb.me │ │ https://git.ityb.me │
│ → proxy_pass http://127.0.0.1:3000 │ │ → proxy_pass http://127.0.0.1:3000 │
│ → Gitea │ │ → Gitea │
│ │ │ │
│ http://39.106.14.107/careerbot/ (IP 直连兼容 trunk) │ │ http://39.106.14.107:3000 (向后兼容) │
│ http://39.106.14.107:3000 (IP 直连兼容 Gitea) │ │ → Gitea (直接监听端口) │
└────────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────┘
两个 CareerBot 实例共享同一 DB + uploads
/home/deploy/apps/CareerBot/careerbot.db ← 权威数据源
/home/deploy/apps/CareerBot/uploads/ ← 权威上传目录
branch1 通过 systemd 环境变量 DATABASE_URL / UPLOAD_DIR 指向这里
``` ```
### 端口使用 ### 端口使用
@ -95,37 +83,30 @@ ssh root@39.106.14.107 # root 直连
| 端口 | 服务 | 监听地址 | 防火墙 | 安全组 | | 端口 | 服务 | 监听地址 | 防火墙 | 安全组 |
|------|------|---------|--------|--------| |------|------|---------|--------|--------|
| 22 | SSH | 0.0.0.0 | 已放行 | 已放行 | | 22 | SSH | 0.0.0.0 | 已放行 | 已放行 |
| 80 | Nginx (HTTP → HTTPS) | 0.0.0.0 | 已放行 | 已放行 | | 80 | Nginx → CareerBot | 0.0.0.0 | 已放行 | 已放行 |
| 443 | Nginx (HTTPS) | 0.0.0.0 | 已放行 | 已放行 | | 443 | HTTPS (预留) | - | 已放行 | 已放行 |
| 3000 | Gitea | 0.0.0.0 | 已放行 | 已放行 | | 3000 | Gitea | 0.0.0.0 | 已放行 | 已放行 |
| 8000 | CareerBot trunk (uvicorn) | 127.0.0.1 | 内部 | 无需 | | 8000 | CareerBot (uvicorn) | 127.0.0.1 | 内部 | 无需 |
| 8001 | CareerBot branch1 (uvicorn) | 127.0.0.1 | 内部 | 无需 |
--- ---
## 4. 文件目录结构 ## 4. 文件目录结构
``` ```
/home/deploy/apps/CareerBot/ ← Trunk 项目根目录main 分支,:8000 /home/deploy/apps/CareerBot/ ← 项目根目录
├── venv/ ← Python 虚拟环境 ├── venv/ ← Python 虚拟环境
├── app/ ← 应用代码 ├── app/ ← 应用代码
├── templates/ ← 页面模板 ├── templates/ ← 页面模板
├── careerbot.db ← SQLite 数据库(主数据源branch1 也读写这里 ├── careerbot.db ← SQLite 数据库(重要数据!
├── uploads/ ← 用户上传文件主目录branch1 也访问这里) ├── uploads/ ← 用户上传文件
├── requirements.txt ├── requirements.txt
├── init_data.py ├── init_data.py
├── start.bat / stop.bat ← Windows 本地启动脚本(线上不用)
├── DESIGN.md ← 设计文档 ├── DESIGN.md ← 设计文档
└── OPS_MANUAL.md ← 本文档 └── OPS_MANUAL.md ← 本文档
/home/deploy/apps/CareerBot-branch1/ ← Branch1 项目根目录branch1 分支,:8001
├── venv/ ← 独立虚拟环境
├── app/ ← BASE_PATH="" 的代码
├── templates/ ← 隐藏 FAB 的 index.html
└── (无独立 careerbot.db通过环境变量指向 trunk 的)
/etc/systemd/system/ /etc/systemd/system/
├── careerbot.service ← CareerBot trunk systemd 服务 ├── careerbot.service ← CareerBot systemd 服务
├── careerbot-branch1.service ← CareerBot branch1 systemd 服务
└── gitea.service ← Gitea systemd 服务 └── gitea.service ← Gitea systemd 服务
/etc/nginx/ /etc/nginx/
@ -226,7 +207,7 @@ ssh ecs "ss -tlnp"
### 6.1 CareerBot systemd 服务 ### 6.1 CareerBot systemd 服务
**Trunkmain 分支)**`/etc/systemd/system/careerbot.service` 文件`/etc/systemd/system/careerbot.service`
```ini ```ini
[Unit] [Unit]
Description=CareerBot Web Application Description=CareerBot Web Application
@ -245,103 +226,47 @@ Environment=PATH=/home/deploy/apps/CareerBot/venv/bin:/usr/bin
WantedBy=multi-user.target WantedBy=multi-user.target
``` ```
**Branch1branch1 分支,共享 trunk 数据)**`/etc/systemd/system/careerbot-branch1.service` 关键参数:
```ini - `--host 127.0.0.1`:只监听本地,由 Nginx 代理外部访问
[Unit]
Description=CareerBot branch1 (display-only, shared DB with trunk)
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/apps/CareerBot-branch1
ExecStart=/home/deploy/apps/CareerBot-branch1/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8001
Restart=always
Environment=PATH=/home/deploy/apps/CareerBot-branch1/venv/bin:/usr/bin
Environment=DATABASE_URL=sqlite:////home/deploy/apps/CareerBot/careerbot.db
Environment=UPLOAD_DIR=/home/deploy/apps/CareerBot/uploads
Environment=BASE_PATH=
[Install]
WantedBy=multi-user.target
```
关键差异:
- **端口**trunk 用 8000branch1 用 8001
- **数据共享**branch1 通过 `DATABASE_URL``UPLOAD_DIR` 环境变量指向 trunk 的数据库和上传目录
- **BASE_PATH**branch1 设置为空串,应用部署在根路径
- **FAB 显示**branch1 的 `templates/index.html` 已修改,对话按钮 `style="display:none;"`
- `Restart=always`:崩溃后自动重启 - `Restart=always`:崩溃后自动重启
- `RestartSec=5`:重启间隔 5 秒
### 6.2 Nginx 站点配置 ### 6.2 Nginx 站点配置
文件:`/etc/nginx/sites-available/careerbot.conf`(含三段 server block + certbot 自动追加的 HTTPS 块) 文件:`/etc/nginx/sites-available/careerbot.conf`
```nginx ```nginx
# ===== TRUNK: career.ityb.me (完整版,/careerbot/ 子路径) =====
server {
server_name career.ityb.me;
client_max_body_size 10M;
location /careerbot/ {
proxy_pass http://127.0.0.1:8000/careerbot/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off; proxy_cache off; proxy_read_timeout 300s;
}
location = / { return 302 /careerbot/; }
listen 443 ssl; # by certbot
ssl_certificate /etc/letsencrypt/live/career.ityb.me/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/career.ityb.me/privkey.pem;
}
# ===== BRANCH1: www.ityb.me + ityb.me (展示版,根路径) =====
server { server {
listen 80; listen 80;
server_name www.ityb.me ityb.me; server_name www.ityb.me ityb.me 39.106.14.107;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name www.ityb.me ityb.me;
client_max_body_size 10M;
ssl_certificate /etc/letsencrypt/live/www.ityb.me/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.ityb.me/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8001/; # branch1
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off; proxy_cache off; proxy_read_timeout 300s;
}
}
# ===== IP 直连向后兼容HTTP only =====
server {
listen 80 default_server;
server_name 39.106.14.107;
client_max_body_size 10M; client_max_body_size 10M;
location /careerbot/ { location /careerbot/ {
proxy_pass http://127.0.0.1:8000/careerbot/; proxy_pass http://127.0.0.1:8000/careerbot/;
# ... 同 trunk ... proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE 流式响应支持(关键!关闭缓冲)
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
}
# 根路径重定向到 CareerBot
location = / {
return 302 /careerbot/;
} }
location = / { return 302 /careerbot/; }
} }
``` ```
关键配置说明: 关键配置说明:
- **双域双实例**career 子域名走 trunk:8000主域名走 branch1:8001 - **子路径部署**CareerBot 部署在 `/careerbot/` 子路径下,后续其他项目可使用其他路径
- **SSL 证书**`career.ityb.me` 和 `www.ityb.me+ityb.me` 分别独立证书(均由 certbot 自动申请续期) - `proxy_pass` 尾部带 `/careerbot/`:将完整路径透传给 FastAPIFastAPI 通过 `prefix="/careerbot"` 匹配路由)
- **主域路径隔离**`www.ityb.me/careerbot/*` 返回 404nginx 层不做任何特殊处理,请求透传到 branch1 后端branch1 没有 /careerbot 前缀路由因此 404`/careerbot/uploads/*` 能命中 branch1 的二级挂载,用于显示 trunk 管理员上传的头像
- `proxy_buffering off` + `proxy_cache off`**必须关闭**,否则 SSE 流式对话无法实时返回 - `proxy_buffering off` + `proxy_cache off`**必须关闭**,否则 SSE 流式对话无法实时返回
- `proxy_read_timeout 300s`LLM 长回复可能需要较长时间 - `proxy_read_timeout 300s`LLM 长回复可能需要较长时间
- `client_max_body_size 10M`:允许上传最大 10MB 文件
- 静态文件和上传文件通过 FastAPI StaticFiles 挂载在 `/careerbot/static/``/careerbot/uploads/`,无需单独 Nginx location
### 6.3 Gitea 站点配置 ### 6.3 Gitea 站点配置

View File

@ -8,7 +8,7 @@ class Settings(BaseSettings):
ALGORITHM: str = "HS256" ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 480 ACCESS_TOKEN_EXPIRE_MINUTES: int = 480
UPLOAD_DIR: str = "./uploads" UPLOAD_DIR: str = "./uploads"
BASE_PATH: str = "/careerbot" BASE_PATH: str = ""
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"} model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}

View File

@ -10,7 +10,7 @@ from app.models import AdminUser, Profile
from app.routers import auth, public, admin, chat from app.routers import auth, public, admin, chat
from app.routers.auth import hash_password from app.routers.auth import hash_password
BASE = settings.BASE_PATH BASE = settings.BASE_PATH # "" for root deployment, or "/careerbot" for subpath
app = FastAPI(title="CareerBot", description="Personal Career Showcase with AI Assistant") app = FastAPI(title="CareerBot", description="Personal Career Showcase with AI Assistant")
@ -19,6 +19,12 @@ app.mount(f"{BASE}/static", StaticFiles(directory="app/static"), name="static")
os.makedirs(settings.UPLOAD_DIR, exist_ok=True) os.makedirs(settings.UPLOAD_DIR, exist_ok=True)
app.mount(f"{BASE}/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads") app.mount(f"{BASE}/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads")
# When BASE="" and DB is shared with a /careerbot deployment, photo_url values
# stored by the sibling deployment begin with "/careerbot/uploads/...".
# Add a secondary mount so those URLs keep resolving here.
if BASE == "":
app.mount("/careerbot/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads_legacy")
# Include routers with BASE_PATH prefix # Include routers with BASE_PATH prefix
app.include_router(auth.router, prefix=BASE) app.include_router(auth.router, prefix=BASE)
app.include_router(public.router, prefix=BASE) app.include_router(public.router, prefix=BASE)
@ -30,9 +36,10 @@ admin.templates.env.globals["base"] = BASE
public.templates.env.globals["base"] = BASE public.templates.env.globals["base"] = BASE
# Root redirect to CareerBot # Root redirect only needed when deployed under a subpath (otherwise "/" is already the app root)
@app.get("/") if BASE:
def root_redirect(): @app.get("/")
def root_redirect():
return RedirectResponse(url=f"{BASE}/", status_code=302) return RedirectResponse(url=f"{BASE}/", status_code=302)

View File

@ -34,8 +34,8 @@
<div class="accordion" id="experience-container"></div> <div class="accordion" id="experience-container"></div>
</div> </div>
<!-- Chat Widget --> <!-- Chat Widget (FAB hidden in branch1: display-only variant for main domain www.ityb.me) -->
<button class="chat-fab" id="chat-fab" title="智能职业助手"> <button class="chat-fab" id="chat-fab" title="智能职业助手" style="display:none;">
<span class="fab-inner"> <span class="fab-inner">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
@ -44,7 +44,7 @@
</span> </span>
</button> </button>
<div class="chat-panel" id="chat-panel"> <div class="chat-panel" id="chat-panel" style="display:none;">
<div class="chat-header"> <div class="chat-header">
<span>智能职业助手</span> <span>智能职业助手</span>
<button id="chat-close">&times;</button> <button id="chat-close">&times;</button>