ci: 构建单镜像版本
1. 优化路由改为createWebHashHistory 2. 优化Dockerfile
This commit is contained in:
29
Dockerfile
29
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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;"]
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,6 @@ import {
|
||||
NFormItem,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NSwitch,
|
||||
NAlert,
|
||||
NDivider,
|
||||
NDropdown,
|
||||
|
||||
@@ -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<RouteRecordRaw> = [
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
|
||||
@@ -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<LoginResponse> => {
|
||||
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<boolean> => {
|
||||
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<TestApiResponse> => {
|
||||
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<SearchResult[]> => {
|
||||
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<boolean> => {
|
||||
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);
|
||||
|
||||
@@ -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)
|
||||
uvicorn.run("web_server:app", host="127.0.0.1", port=8888, reload=True)
|
||||
Reference in New Issue
Block a user