From ce331a4d0d6eb7c68780045f28237b79ce4a414f Mon Sep 17 00:00:00 2001 From: CaasianVale <1544257291@qq.com> Date: Sat, 8 Mar 2025 03:59:25 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E6=9E=84=E5=BB=BA=E5=8D=95=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 优化路由改为createWebHashHistory 2. 优化Dockerfile --- Dockerfile | 29 ++++++- docker-compose.prod.yml | 48 ----------- docker-compose.yml | 23 +----- frontend/Dockerfile | 36 --------- frontend/nginx.conf | 94 ---------------------- frontend/src/components/ApiConfigPanel.vue | 1 - frontend/src/router/index.ts | 4 +- frontend/src/services/api.ts | 20 ++--- web_server.py | 49 ++++------- 9 files changed, 54 insertions(+), 250 deletions(-) delete mode 100644 docker-compose.prod.yml delete mode 100644 frontend/Dockerfile delete mode 100644 frontend/nginx.conf diff --git a/Dockerfile b/Dockerfile index f9a3cd5..5fba799 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,23 @@ -# 使用 Python 3.10 作为基础镜像 -FROM python:3.10-slim as builder +# 阶段一: 构建Vue前端 +FROM node:18-alpine as frontend-builder + +# 设置工作目录 +WORKDIR /app/frontend + +# 复制前端项目文件 +COPY frontend/package*.json ./ + +# 安装依赖 +RUN npm ci + +# 复制前端源代码 +COPY frontend/ ./ + +# 构建前端应用 +RUN npm run build + +# 阶段二: 构建Python后端 +FROM python:3.10-slim as backend-builder # 设置工作目录 WORKDIR /app @@ -17,7 +35,7 @@ COPY requirements.txt /app/ # 安装 Python 依赖 RUN pip install --no-cache-dir --user -r requirements.txt -# 第二阶段:运行阶段 +# 阶段三: 运行阶段 FROM python:3.10-slim # 设置工作目录 @@ -30,7 +48,7 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # 从构建阶段复制Python依赖 -COPY --from=builder /root/.local /root/.local +COPY --from=backend-builder /root/.local /root/.local # 确保脚本路径在PATH中 ENV PATH=/root/.local/bin:$PATH @@ -41,6 +59,9 @@ ENV PYTHONPATH=/app # 复制应用代码 COPY . /app/ +# 从前端构建阶段复制生成的静态文件到后端的前端目录 +COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist + # 暴露端口 EXPOSE 8888 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 5c18530..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,48 +0,0 @@ -version: '3.8' - -services: - backend: - image: ${DOCKERHUB_USERNAME}/stock-scanner-backend:${TAG:-latest} - container_name: stock-scanner-backend - ports: - - "8888:8888" - environment: - - API_KEY=${API_KEY} - - API_URL=${API_URL} - - API_MODEL=${API_MODEL} - - API_TIMEOUT=${API_TIMEOUT} - - LOGIN_PASSWORD=${LOGIN_PASSWORD} - - ANNOUNCEMENT_TEXT=${ANNOUNCEMENT_TEXT} - volumes: - - ./logs:/app/logs - - ./.env:/app/.env - restart: unless-stopped - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8888/config"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 5s - networks: - - stock-scanner-network - - frontend: - image: ${DOCKERHUB_USERNAME}/stock-scanner-frontend:${TAG:-latest} - container_name: stock-scanner-frontend - ports: - - "80:80" - depends_on: - - backend - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 5s - networks: - - stock-scanner-network - -networks: - stock-scanner-network: - driver: bridge \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 57ddb0b..68c9990 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,11 @@ version: '3.8' services: - backend: + app: build: context: . dockerfile: Dockerfile - container_name: stock-scanner-backend + container_name: stock-scanner-app ports: - "8888:8888" environment: @@ -27,25 +27,6 @@ services: networks: - stock-scanner-network - frontend: - build: - context: ./frontend - dockerfile: Dockerfile - container_name: stock-scanner-frontend - ports: - - "80:80" - depends_on: - - backend - restart: unless-stopped - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 5s - networks: - - stock-scanner-network - networks: stock-scanner-network: driver: bridge diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index 1b8d3b4..0000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -# 构建阶段 -FROM node:18-alpine as build-stage - -# 设置工作目录 -WORKDIR /app - -# 复制 package.json 和 package-lock.json(如果有) -COPY package*.json ./ - -# 安装依赖 -RUN npm ci - -# 复制项目文件 -COPY . . - -# 构建应用 -RUN npm run build - -# 生产阶段 -FROM nginx:stable-alpine as production-stage - -# 复制自定义nginx配置 -COPY nginx.conf /etc/nginx/conf.d/default.conf - -# 从构建阶段复制构建结果到nginx服务目录 -COPY --from=build-stage /app/dist /usr/share/nginx/html - -# 暴露80端口 -EXPOSE 80 - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --quiet --tries=1 --spider http://localhost:80 || exit 1 - -# 设定启动命令 -CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf deleted file mode 100644 index 3808961..0000000 --- a/frontend/nginx.conf +++ /dev/null @@ -1,94 +0,0 @@ -server { - listen 80; - server_name localhost; - - #access_log /var/log/nginx/host.access.log main; - - root /usr/share/nginx/html; - index index.html; - - # 缓存静态资源 - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, max-age=31536000, immutable"; - } - - # API请求代理到后端服务 - 使用相对路径 - location /api/ { - proxy_pass http://backend:8888/api/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - 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_cache_bypass $http_upgrade; - proxy_read_timeout 300s; - } - - location /login { - proxy_pass http://backend:8888/login; - proxy_http_version 1.1; - 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_cache_bypass $http_upgrade; - } - - location /check_auth { - proxy_pass http://backend:8888/check_auth; - proxy_http_version 1.1; - 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_cache_bypass $http_upgrade; - } - - location /need_login { - proxy_pass http://backend:8888/need_login; - proxy_http_version 1.1; - 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_cache_bypass $http_upgrade; - } - - location /config { - proxy_pass http://backend:8888/config; - proxy_http_version 1.1; - 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_cache_bypass $http_upgrade; - } - - location /analyze { - proxy_pass http://backend:8888/analyze; - proxy_http_version 1.1; - 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_cache_bypass $http_upgrade; - proxy_read_timeout 300s; - } - - location /test_api_connection { - proxy_pass http://backend:8888/test_api_connection; - proxy_http_version 1.1; - 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_cache_bypass $http_upgrade; - } - - # 所有其他路由返回index.html(SPA应用需要) - location / { - try_files $uri $uri/ /index.html; - } - - # 错误页面 - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } -} \ No newline at end of file diff --git a/frontend/src/components/ApiConfigPanel.vue b/frontend/src/components/ApiConfigPanel.vue index 3b02555..1d60d02 100644 --- a/frontend/src/components/ApiConfigPanel.vue +++ b/frontend/src/components/ApiConfigPanel.vue @@ -205,7 +205,6 @@ import { NFormItem, NInput, NInputNumber, - NSwitch, NAlert, NDivider, NDropdown, diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index e2bd7f0..cbf8877 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,4 +1,4 @@ -import { createRouter, createWebHistory } from 'vue-router'; +import { createRouter, createWebHashHistory } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router'; import { apiService } from '@/services/api'; import StockAnalysisApp from '@/components/StockAnalysisApp.vue'; @@ -24,7 +24,7 @@ const routes: Array = [ ]; const router = createRouter({ - history: createWebHistory(), + history: createWebHashHistory(), routes }); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 082f02f..0dcdc87 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,8 +1,8 @@ import axios from 'axios'; import type { AnalyzeRequest, TestApiRequest, TestApiResponse, SearchResult, LoginRequest, LoginResponse } from '@/types'; -// 在开发环境中使用完整URL -const API_PREFIX = ''; +// API前缀 +const API_PREFIX = '/api'; // 创建axios实例 const axiosInstance = axios.create({ @@ -42,7 +42,7 @@ export const apiService = { // 用户登录 login: async (request: LoginRequest): Promise => { try { - const response = await axios.post(`${API_PREFIX}/login`, request); + const response = await axiosInstance.post('/login', request); if (response.data.access_token) { localStorage.setItem('token', response.data.access_token); } @@ -64,7 +64,7 @@ export const apiService = { // 检查认证状态 checkAuth: async (): Promise => { try { - const response = await axiosInstance.get(`${API_PREFIX}/check_auth`); + const response = await axiosInstance.get('/check_auth'); return response.data.authenticated === true; } catch (error) { // 认证失败,清除token @@ -82,7 +82,7 @@ export const apiService = { // 分析股票 analyzeStocks: async (request: AnalyzeRequest) => { - return axiosInstance.post(`${API_PREFIX}/analyze`, request, { + return axiosInstance.post('/analyze', request, { responseType: 'stream' }); }, @@ -90,7 +90,7 @@ export const apiService = { // 测试API连接 testApiConnection: async (request: TestApiRequest): Promise => { try { - const response = await axiosInstance.post(`${API_PREFIX}/test_api_connection`, request); + const response = await axiosInstance.post('/test_api_connection', request); return response.data; } catch (error: any) { if (error.response) { @@ -106,7 +106,7 @@ export const apiService = { // 搜索美股 searchUsStocks: async (keyword: string): Promise => { try { - const response = await axiosInstance.get(`${API_PREFIX}/search_us_stocks`, { + const response = await axiosInstance.get('/search_us_stocks', { params: { keyword } }); return response.data.results || []; @@ -119,14 +119,14 @@ export const apiService = { // 获取配置 getConfig: async () => { try { - const response = await axios.get(`${API_PREFIX}/config`); + const response = await axiosInstance.get('/config'); return response.data; } catch (error) { console.error('获取配置时出错:', error); return { announcement: '', default_api_url: '', - default_api_model: 'gpt-3.5-turbo', + default_api_model: '', default_api_timeout: '60' }; } @@ -135,7 +135,7 @@ export const apiService = { // 检查是否需要登录 checkNeedLogin: async (): Promise => { try { - const response = await axios.get(`${API_PREFIX}/need_login`); + const response = await axiosInstance.get('/need_login'); return response.data.require_login; } catch (error) { console.error('检查是否需要登录时出错:', error); diff --git a/web_server.py b/web_server.py index 4682dae..44cebe6 100644 --- a/web_server.py +++ b/web_server.py @@ -54,7 +54,10 @@ app.add_middleware( # 设置静态文件 frontend_dist = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'frontend', 'dist') if os.path.exists(frontend_dist): - app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dist, "assets")), name="assets") + # 直接挂载整个dist目录 + app.mount("/", StaticFiles(directory=frontend_dist, html=True), name="static") +else: + logger.warning("前端构建目录不存在,仅API功能可用") # 初始化异步服务 # StockAnalyzerService 不需要全局初始化,在 /analyze 接口中按需创建 @@ -139,7 +142,7 @@ async def verify_token(token: Optional[str] = Depends(optional_oauth2_scheme)): raise credentials_exception # 用户登录接口 -@app.post("/login") +@app.post("/api/login") async def login(request: LoginRequest): """用户登录接口""" # 如果未设置密码,表示不需要登录 @@ -160,13 +163,13 @@ async def login(request: LoginRequest): return {"access_token": access_token, "token_type": "bearer"} # 检查用户认证状态 -@app.get("/check_auth") +@app.get("/api/check_auth") async def check_auth(username: str = Depends(verify_token)): """检查用户认证状态""" return {"authenticated": True, "username": username} # 获取系统配置 -@app.get("/config") +@app.get("/api/config") async def get_config(): """返回系统配置信息""" config = { @@ -178,7 +181,7 @@ async def get_config(): return config # AI分析股票 -@app.post("/analyze") +@app.post("/api/analyze") async def analyze(request: AnalyzeRequest, username: str = Depends(verify_token)): try: logger.info("开始处理分析请求") @@ -266,7 +269,7 @@ async def analyze(request: AnalyzeRequest, username: str = Depends(verify_token) raise HTTPException(status_code=500, detail=error_msg) # 搜索美股代码 -@app.get("/search_us_stocks") +@app.get("/api/search_us_stocks") async def search_us_stocks(keyword: str = "", username: str = Depends(verify_token)): try: if not keyword: @@ -281,7 +284,7 @@ async def search_us_stocks(keyword: str = "", username: str = Depends(verify_tok raise HTTPException(status_code=500, detail=str(e)) # 搜索基金代码 -@app.get("/search_funds") +@app.get("/api/search_funds") async def search_funds(keyword: str = "", market_type: str = "", username: str = Depends(verify_token)): try: if not keyword: @@ -296,7 +299,7 @@ async def search_funds(keyword: str = "", market_type: str = "", username: str = raise HTTPException(status_code=500, detail=str(e)) # 获取美股详情 -@app.get("/us_stock_detail/{symbol}") +@app.get("/api/us_stock_detail/{symbol}") async def get_us_stock_detail(symbol: str, username: str = Depends(verify_token)): try: if not symbol: @@ -311,7 +314,7 @@ async def get_us_stock_detail(symbol: str, username: str = Depends(verify_token) raise HTTPException(status_code=500, detail=str(e)) # 获取基金详情 -@app.get("/fund_detail/{symbol}") +@app.get("/api/fund_detail/{symbol}") async def get_fund_detail(symbol: str, market_type: str = "ETF", username: str = Depends(verify_token)): try: if not symbol: @@ -326,7 +329,7 @@ async def get_fund_detail(symbol: str, market_type: str = "ETF", username: str = raise HTTPException(status_code=500, detail=str(e)) # 测试API连接 -@app.post("/test_api_connection") +@app.post("/api/test_api_connection") async def test_api_connection(request: TestAPIRequest, username: str = Depends(verify_token)): """测试API连接""" try: @@ -395,33 +398,11 @@ async def test_api_connection(request: TestAPIRequest, username: str = Depends(v ) # 检查是否需要登录 -@app.get("/need_login") +@app.get("/api/need_login") async def need_login(): """检查是否需要登录""" return {"require_login": REQUIRE_LOGIN} -# 前端路由处理,必须放在所有API路由之后 -@app.get("/{full_path:path}") -async def serve_frontend(full_path: str, request: Request): - """处理所有前端路由请求,返回index.html""" - # 排除API路径和静态资源 - if full_path.startswith(("api/", "assets/", "docs", "openapi.json")) or \ - full_path in ["check_auth", "config", "analyze", - "search_us_stocks", "search_funds", - "test_api_connection", "us_stock_detail", - "fund_detail"]: - # 对于API路径,让FastAPI继续处理 - raise HTTPException(status_code=404, detail="API路径不存在") - - # 检查是否使用前端构建版本 - if os.path.exists(frontend_dist): - index_file = os.path.join(frontend_dist, 'index.html') - return FileResponse(index_file) - else: - # 不再使用模板渲染,而是重定向到API文档页面 - logger.warning("前端构建目录不存在,重定向到API文档页面") - return RedirectResponse(url="/docs") if __name__ == '__main__': - logger.info("股票AI分析系统启动") - uvicorn.run("web_server:app", host="0.0.0.0", port=8888, reload=True) \ No newline at end of file + uvicorn.run("web_server:app", host="127.0.0.1", port=8888, reload=True) \ No newline at end of file