Compare commits

..

3 Commits

Author SHA1 Message Date
shijing 96241790dd feat: FastAPI 直接加载 SSL,ssl 目录纳入 gitignore
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-10 15:55:02 +08:00
shijing 6eb91076c5 fix: run.bat 错误分支加 pause;前端生产用 window.location.origin
- run.bat: :error 分支加 pause,避免创建 venv/装依赖失败时窗口一闪即关
- frontend app store: 开发用 127.0.0.1:8775,生产回落到 window.location.origin,跟后端同源直取

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-10 15:48:58 +08:00
shijing 49f1fb6089 feat: 后端服务前端 dist 并统一使用 8775 端口
- backend/app/main.py 挂载 dist/ 作为 SPA 静态资源,/assets 走 StaticFiles,其它路径回落 index.html
- 前后端 API base 统一切换到 127.0.0.1:8775,避开 8000 占用
- run.bat 自动创建/复用 backend/.venv,用 venv 启动 uvicorn
- .gitignore 忽略 backend/.venv/
- 新增 deploy/finai-backend.service

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-10 15:29:03 +08:00
10 changed files with 163 additions and 15 deletions

3
.gitignore vendored
View File

@ -21,6 +21,7 @@ Thumbs.db
!.env.example
*.pem
*.key
ssl/
# ============ Backend (Python) ============
# 字节码 / 缓存
@ -37,7 +38,7 @@ backend/.eggs/
backend/wheels/
# 虚拟环境
# backend/.venv/
backend/.venv/
backend/venv/
backend/env/
backend/ENV/

View File

@ -1,7 +1,7 @@
APP_NAME=财务大模型智能问答客服系统
ENVIRONMENT=development
# DATABASE_URL=postgresql+psycopg://finai:finai@127.0.0.1:5432/finai
DATABASE_URL=postgresql+psycopg://postgres:zcDsj2026@127.0.0.1:5432/finai
DATABASE_URL=postgresql+psycopg://finance_ai:finance_ai@127.0.0.1:5432/finance_ai
# DATABASE_URL=postgresql+psycopg://postgres:zcDsj2026@127.0.0.1:5432/finai
JWT_SECRET_KEY=vWQqGzVAi01hq126fXZdHV4W4S2Tx4BiOvWuwe5m8ubAPPitLpSh-Y4mXC50d7lM
JWT_EXPIRE_MINUTES=480
SMS_PROVIDER=tencent

View File

@ -1,11 +1,20 @@
from fastapi import FastAPI
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from app.api import api_router
from app.config import get_settings
from app.db import SessionLocal, init_db
from app.services.user_service import ensure_default_admin
FRONTEND_DIST = Path(__file__).resolve().parent.parent / "dist"
SSL_DIR = Path(__file__).resolve().parent.parent.parent / "ssl"
SSL_CERT_FILE = SSL_DIR / "finai.ctc-zc.com.pem"
SSL_KEY_FILE = SSL_DIR / "finai.ctc-zc.com.key"
def create_app() -> FastAPI:
settings = get_settings()
@ -33,7 +42,52 @@ def create_app() -> FastAPI:
def health() -> dict:
return {"status": "ok", "app": settings.app_name}
_mount_frontend(app)
return app
def _mount_frontend(app: FastAPI) -> None:
if not FRONTEND_DIST.is_dir():
return
assets_dir = FRONTEND_DIST / "assets"
if assets_dir.is_dir():
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
index_file = FRONTEND_DIST / "index.html"
@app.get("/", include_in_schema=False)
def _spa_root() -> FileResponse:
return FileResponse(index_file)
@app.get("/{full_path:path}", include_in_schema=False)
def _spa_fallback(full_path: str) -> FileResponse:
if full_path.startswith(("api/", "assets/", "health")):
raise HTTPException(status_code=404)
candidate = FRONTEND_DIST / full_path
if candidate.is_file():
return FileResponse(candidate)
return FileResponse(index_file)
app = create_app()
if __name__ == "__main__":
import uvicorn
ssl_kwargs: dict = {}
if SSL_CERT_FILE.is_file() and SSL_KEY_FILE.is_file():
ssl_kwargs = {
"ssl_certfile": str(SSL_CERT_FILE),
"ssl_keyfile": str(SSL_KEY_FILE),
}
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8775,
reload=True,
**ssl_kwargs,
)

View File

@ -1,11 +1,26 @@
@echo off
@echo off
chcp 65001 >nul
setlocal
cd /d "%~dp0"
echo [backend] 启动 uvicorn app.main:app http://127.0.0.1:8000
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
if not exist ".venv\Scripts\python.exe" (
echo [backend] 未找到 .venv正在创建虚拟环境...
python -m venv .venv || goto :error
echo [backend] 正在安装依赖...
".venv\Scripts\python.exe" -m pip install --upgrade pip || goto :error
".venv\Scripts\python.exe" -m pip install fastapi "uvicorn[standard]" sqlalchemy "psycopg[binary]" pydantic-settings "python-jose[cryptography]" "passlib[bcrypt]" python-multipart httpx tencentcloud-sdk-python-lke || goto :error
)
endlocal
echo [backend] 启动 uvicorn app.main:app https://0.0.0.0:8775 (SSL)
".venv\Scripts\python.exe" -m app.main
goto :end
:error
echo [backend] 启动失败
pause
exit /b 1
:end
endlocal
pause

View File

@ -0,0 +1,77 @@
# /etc/systemd/system/finai-backend.service
#
# 财务大模型智能问答客服系统 · FastAPI 后端
# 代码目录:/home/ubuntu/finai/backend
# 虚拟环境:/home/ubuntu/finai/backend/.venv
# .env /home/ubuntu/finai/backend/.env (DSN / JWT / ADP 等)
# 监听 127.0.0.1:8775 (Nginx /api/ 反代到这里)
#
# 部署:
# sudo cp /home/ubuntu/finai/deploy/systemd/finai-backend.service \
# /etc/systemd/system/finai-backend.service
# sudo systemctl daemon-reload
# sudo systemctl enable --now finai-backend
#
# 常用:
# sudo systemctl status finai-backend
# sudo systemctl restart finai-backend
# journalctl -u finai-backend -f
[Unit]
Description=Finai Backend (FastAPI)
Documentation=https://finai.ctc-zc.com
After=network-online.target postgresql.service redis-server.service
Wants=network-online.target postgresql.service redis-server.service
[Service]
Type=simple
User=ubuntu
Group=ubuntu
WorkingDirectory=/home/ubuntu/finai/backend
EnvironmentFile=/home/ubuntu/finai/backend/.env
ExecStart=/home/ubuntu/finai/backend/.venv/bin/uvicorn app.main:app \
--host 127.0.0.1 \
--port 8775 \
--workers 2 \
--proxy-headers \
--forwarded-allow-ips=127.0.0.1
# 收到 SIGTERM 后给 uvicorn 15s 优雅退出(结束在途 SSE 流)
KillSignal=SIGTERM
TimeoutStopSec=15
Restart=always
RestartSec=3
StartLimitBurst=5
StartLimitIntervalSec=60
# 让 stdout/stderr 走 journaljournalctl -u finai-backend 直接看)
StandardOutput=journal
StandardError=journal
SyslogIdentifier=finai-backend
# ─── 基础加固 ────────────────────────────────────────────────
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
# 工作目录在 /home/ubuntu必须放开 /home 保护
ProtectHome=false
# 精确开放需要写的路径storage 落图片、SQLite/日志 等)
ReadWritePaths=/home/ubuntu/finai/backend
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictRealtime=true
# 只允许 IPv4/IPv6/UNIX屏蔽奇葩地址簇
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# ─── 资源限制 ────────────────────────────────────────────────
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target

View File

@ -26,7 +26,7 @@ http://127.0.0.1:5173/index.html
## 登录
- API 地址:`http://127.0.0.1:8000`
- API 地址:`http://127.0.0.1:8775`
- 默认管理员手机号:`13800000000`
- Mock 验证码:`123456`

View File

@ -1,5 +1,5 @@
const state = {
apiBase: localStorage.getItem("financeAiApiBase") || "http://127.0.0.1:8000",
apiBase: localStorage.getItem("financeAiApiBase") || "http://127.0.0.1:8775",
token: localStorage.getItem("financeAiToken") || "",
currentUser: null,
currentSessionId: null,
@ -62,7 +62,7 @@ async function request(path, options = {}) {
}
function setApiBase(value) {
state.apiBase = value.trim() || "http://127.0.0.1:8000";
state.apiBase = value.trim() || "http://127.0.0.1:8775";
localStorage.setItem("financeAiApiBase", state.apiBase);
}

View File

@ -33,7 +33,7 @@
<div id="loginForm">
<label>
<span>API 地址</span>
<input id="apiBaseInput" value="http://127.0.0.1:8000" />
<input id="apiBaseInput" value="http://127.0.0.1:8775" />
</label>
<label>
<span>手机号</span>

View File

@ -3,7 +3,8 @@ import { computed, ref } from 'vue'
import { notify } from '@/utils/notify'
export const useAppStore = defineStore('app', () => {
const apiBase = ref(localStorage.getItem('financeAiApiBase') || 'http://127.0.0.1:8000')
const defaultApiBase = import.meta.env.DEV ? 'http://127.0.0.1:8775' : window.location.origin
const apiBase = ref(localStorage.getItem('financeAiApiBase') || defaultApiBase)
const token = ref(localStorage.getItem('financeAiToken') || '')
const currentUser = ref(null)
const healthy = ref(false)
@ -17,7 +18,7 @@ export const useAppStore = defineStore('app', () => {
}
function setApiBase(value) {
apiBase.value = (value || '').trim() || 'http://127.0.0.1:8000'
apiBase.value = (value || '').trim() || defaultApiBase
localStorage.setItem('financeAiApiBase', apiBase.value)
}

View File

@ -14,7 +14,7 @@ export default defineConfig({
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
target: 'http://127.0.0.1:8775',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},