backup
This commit is contained in:
@@ -62,11 +62,6 @@ COPY --from=frontend-builder /app/frontend/dist ./static
|
|||||||
# Create required directories
|
# Create required directories
|
||||||
RUN mkdir -p uploads logs scripts
|
RUN mkdir -p uploads logs scripts
|
||||||
|
|
||||||
# Create startup script using Gunicorn + eventlet for production
|
|
||||||
RUN echo '#!/bin/bash' > /app/start.sh && \
|
|
||||||
echo 'exec gunicorn -k eventlet -w 1 -b 0.0.0.0:12010 wsgi:app' >> /app/start.sh && \
|
|
||||||
chmod +x /app/start.sh
|
|
||||||
|
|
||||||
# Set permissions
|
# Set permissions
|
||||||
RUN useradd -m -u 1000 appuser && \
|
RUN useradd -m -u 1000 appuser && \
|
||||||
chown -R appuser:appuser /app && \
|
chown -R appuser:appuser /app && \
|
||||||
@@ -82,5 +77,5 @@ EXPOSE 12010
|
|||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD curl -f http://localhost:12010/api/v1/health || exit 1
|
CMD curl -f http://localhost:12010/api/v1/health || exit 1
|
||||||
|
|
||||||
# Start application
|
# Run with Gunicorn for production (supports high concurrency)
|
||||||
CMD ["/app/start.sh"]
|
CMD ["gunicorn", "--bind", "0.0.0.0:12010", "--worker-class", "gthread", "--workers", "4", "--threads", "8", "--timeout", "600", "--keep-alive", "10", "--max-requests", "2000", "--max-requests-jitter", "200", "--forwarded-allow-ips", "*", "--access-logfile", "-", "wsgi:app"]
|
||||||
|
17
Dockerfile.redis
Normal file
17
Dockerfile.redis
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Redis for PANJIT Document Translator
|
||||||
|
FROM redis:7-alpine
|
||||||
|
|
||||||
|
# Set container labels for identification
|
||||||
|
LABEL application="panjit-document-translator"
|
||||||
|
LABEL component="redis"
|
||||||
|
LABEL version="v2.0"
|
||||||
|
LABEL maintainer="PANJIT IT Team"
|
||||||
|
|
||||||
|
# Copy custom redis configuration if needed
|
||||||
|
# COPY redis.conf /usr/local/etc/redis/redis.conf
|
||||||
|
|
||||||
|
# Expose the default Redis port
|
||||||
|
EXPOSE 6379
|
||||||
|
|
||||||
|
# Use the default Redis entrypoint
|
||||||
|
# CMD ["redis-server", "/usr/local/etc/redis/redis.conf"]
|
10
README.md
10
README.md
@@ -48,14 +48,20 @@ PANJIT 文件翻譯系統是一個企業級的多語言文件翻譯平台,支
|
|||||||
# 1. 進入專案目錄
|
# 1. 進入專案目錄
|
||||||
cd Document_translator_V2
|
cd Document_translator_V2
|
||||||
|
|
||||||
# 2. 建置並啟動所有服務
|
# 2. 建置並啟動所有服務(強制重建以確保使用最新代碼)
|
||||||
docker-compose up -d
|
docker-compose up -d --build
|
||||||
|
|
||||||
# 3. 檢查服務狀態
|
# 3. 檢查服務狀態
|
||||||
docker-compose ps
|
docker-compose ps
|
||||||
|
|
||||||
# 4. 訪問系統
|
# 4. 訪問系統
|
||||||
curl http://localhost:12010/api/v1/health
|
curl http://localhost:12010/api/v1/health
|
||||||
|
|
||||||
|
# 5. 停止服務
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 6. 查看日誌
|
||||||
|
docker-compose logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
詳細部署說明請參考 [DEPLOYMENT.md](DEPLOYMENT.md)
|
詳細部署說明請參考 [DEPLOYMENT.md](DEPLOYMENT.md)
|
||||||
|
@@ -135,9 +135,9 @@ def create_app(config_name=None):
|
|||||||
# 創建 Celery 實例
|
# 創建 Celery 實例
|
||||||
app.celery = make_celery(app)
|
app.celery = make_celery(app)
|
||||||
|
|
||||||
# 初始化 WebSocket
|
# WebSocket 功能完全禁用
|
||||||
from app.websocket import init_websocket
|
app.logger.info("🔌 [WebSocket] WebSocket 服務已禁用")
|
||||||
app.socketio = init_websocket(app)
|
app.socketio = None
|
||||||
|
|
||||||
# 註冊 Root 路由(提供 SPA 與基本 API 資訊)
|
# 註冊 Root 路由(提供 SPA 與基本 API 資訊)
|
||||||
try:
|
try:
|
||||||
|
@@ -439,8 +439,8 @@ class NotificationService:
|
|||||||
|
|
||||||
logger.info(f"資料庫通知已創建: {notification.notification_uuid} for user {user_id}")
|
logger.info(f"資料庫通知已創建: {notification.notification_uuid} for user {user_id}")
|
||||||
|
|
||||||
# 觸發 WebSocket 推送
|
# WebSocket 推送已禁用
|
||||||
self._send_websocket_notification(notification)
|
# self._send_websocket_notification(notification)
|
||||||
|
|
||||||
return notification
|
return notification
|
||||||
|
|
||||||
@@ -611,16 +611,14 @@ class NotificationService:
|
|||||||
|
|
||||||
def _send_websocket_notification(self, notification: Notification):
|
def _send_websocket_notification(self, notification: Notification):
|
||||||
"""
|
"""
|
||||||
通過 WebSocket 發送通知
|
通過 WebSocket 發送通知 - 已禁用
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
notification: 通知對象
|
notification: 通知對象
|
||||||
"""
|
"""
|
||||||
try:
|
# WebSocket 功能已完全禁用
|
||||||
from app.websocket import send_notification_to_user
|
logger.debug(f"WebSocket 推送已禁用,跳過通知: {notification.notification_uuid}")
|
||||||
send_notification_to_user(notification.user_id, notification.to_dict())
|
pass
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"WebSocket 推送通知失敗: {e}")
|
|
||||||
|
|
||||||
def get_unread_count(self, user_id: int) -> int:
|
def get_unread_count(self, user_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
|
@@ -1,22 +1,27 @@
|
|||||||
services:
|
services:
|
||||||
# Redis 服務 (Celery 後端和緩存)
|
# Redis 服務 (Celery 後端和緩存)
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: panjit-translator:redis
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.redis
|
||||||
container_name: panjit-translator-redis
|
container_name: panjit-translator-redis
|
||||||
# Redis only for internal network use; no public port exposure
|
# Redis only for internal network use; no public port exposure
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: redis-server --appendonly yes
|
command: redis-server --appendonly yes
|
||||||
|
networks:
|
||||||
|
- panjit-translator-network
|
||||||
|
|
||||||
# 主應用服務
|
# 主應用服務
|
||||||
app:
|
app:
|
||||||
|
image: panjit-translator:main
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: panjit-translator-app
|
container_name: translator-app
|
||||||
ports:
|
# No external port; only Nginx exposes ports
|
||||||
- "12010:12010"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./uploads:/app/uploads
|
- ./uploads:/app/uploads
|
||||||
- ./cache:/app/cache
|
- ./cache:/app/cache
|
||||||
@@ -25,19 +30,34 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
environment:
|
environment:
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
- LDAP_SERVER=panjit.com.tw
|
||||||
|
- LDAP_PORT=389
|
||||||
|
- LDAP_USE_SSL=false
|
||||||
|
- LDAP_SEARCH_BASE=DC=panjit,DC=com,DC=tw
|
||||||
|
- LDAP_USER_LOGIN_ATTR=userPrincipalName
|
||||||
|
- DEV_MODE=false
|
||||||
|
- DISABLE_WEBSOCKET=true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1.5G
|
||||||
|
cpus: '1.0'
|
||||||
|
reservations:
|
||||||
|
memory: 512M
|
||||||
|
cpus: '0.5'
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:12010/api/v1/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:12010/api/v1/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
networks:
|
||||||
|
- panjit-translator-network
|
||||||
|
|
||||||
# Celery Worker 服務
|
# Celery Worker 服務
|
||||||
celery-worker:
|
celery-worker:
|
||||||
build:
|
image: panjit-translator:main
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: panjit-translator-worker
|
container_name: panjit-translator-worker
|
||||||
volumes:
|
volumes:
|
||||||
- ./uploads:/app/uploads
|
- ./uploads:/app/uploads
|
||||||
@@ -46,22 +66,33 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- app
|
- app
|
||||||
|
pull_policy: never
|
||||||
environment:
|
environment:
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
- DEV_MODE=false
|
||||||
|
- DISABLE_WEBSOCKET=true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: celery -A celery_app worker --loglevel=info --concurrency=8
|
command: celery -A celery_app worker --loglevel=info --concurrency=4 --max-memory-per-child=200000
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
cpus: '0.8'
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
cpus: '0.3'
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "celery", "-A", "celery_app", "inspect", "ping"]
|
test: ["CMD", "celery", "-A", "celery_app", "inspect", "ping"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
networks:
|
||||||
|
- panjit-translator-network
|
||||||
|
|
||||||
# Celery Beat 調度服務 (可選,如果需要定期任務)
|
# Celery Beat 調度服務 (可選,如果需要定期任務)
|
||||||
celery-beat:
|
celery-beat:
|
||||||
build:
|
image: panjit-translator:main
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: panjit-translator-beat
|
container_name: panjit-translator-beat
|
||||||
volumes:
|
volumes:
|
||||||
- ./uploads:/app/uploads
|
- ./uploads:/app/uploads
|
||||||
@@ -70,15 +101,35 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- app
|
- app
|
||||||
|
pull_policy: never
|
||||||
environment:
|
environment:
|
||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
- DEV_MODE=false
|
||||||
|
- DISABLE_WEBSOCKET=true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: celery -A celery_app beat --loglevel=info
|
command: celery -A celery_app beat --loglevel=info
|
||||||
|
networks:
|
||||||
|
- panjit-translator-network
|
||||||
|
|
||||||
|
# Nginx reverse proxy
|
||||||
|
nginx:
|
||||||
|
image: panjit-translator:nginx
|
||||||
|
build:
|
||||||
|
context: ./nginx
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: panjit-translator-nginx
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
ports:
|
||||||
|
- "12010:12010"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- panjit-translator-network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
redis_data:
|
redis_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
panjit-translator-network:
|
||||||
name: panjit-translator-network
|
driver: bridge
|
||||||
|
@@ -20,6 +20,16 @@ class WebSocketService {
|
|||||||
* 初始化並連接 WebSocket
|
* 初始化並連接 WebSocket
|
||||||
*/
|
*/
|
||||||
connect() {
|
connect() {
|
||||||
|
// 檢查 WebSocket 是否被禁用
|
||||||
|
const devMode = import.meta.env.VITE_DEV_MODE === 'true'
|
||||||
|
const isProd = import.meta.env.PROD
|
||||||
|
const wsDisabled = import.meta.env.VITE_DISABLE_WEBSOCKET === 'true'
|
||||||
|
|
||||||
|
if (!devMode || isProd || wsDisabled) {
|
||||||
|
console.log('🔌 [WebSocket] WebSocket 連接已禁用,跳過連接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -271,6 +281,15 @@ class WebSocketService {
|
|||||||
* @param {string} jobUuid - 任務 UUID
|
* @param {string} jobUuid - 任務 UUID
|
||||||
*/
|
*/
|
||||||
subscribeToJob(jobUuid) {
|
subscribeToJob(jobUuid) {
|
||||||
|
// 檢查 WebSocket 是否被禁用
|
||||||
|
const devMode = import.meta.env.VITE_DEV_MODE === 'true'
|
||||||
|
const isProd = import.meta.env.PROD
|
||||||
|
const wsDisabled = import.meta.env.VITE_DISABLE_WEBSOCKET === 'true'
|
||||||
|
|
||||||
|
if (!devMode || isProd || wsDisabled) {
|
||||||
|
return // WebSocket 被禁用,靜默返回
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.socket || !this.isConnected) {
|
if (!this.socket || !this.isConnected) {
|
||||||
// 靜默處理,避免控制台警告
|
// 靜默處理,避免控制台警告
|
||||||
return
|
return
|
||||||
@@ -334,6 +353,15 @@ class WebSocketService {
|
|||||||
* @param {Object} data - 事件資料
|
* @param {Object} data - 事件資料
|
||||||
*/
|
*/
|
||||||
emit(event, data) {
|
emit(event, data) {
|
||||||
|
// 檢查 WebSocket 是否被禁用
|
||||||
|
const devMode = import.meta.env.VITE_DEV_MODE === 'true'
|
||||||
|
const isProd = import.meta.env.PROD
|
||||||
|
const wsDisabled = import.meta.env.VITE_DISABLE_WEBSOCKET === 'true'
|
||||||
|
|
||||||
|
if (!devMode || isProd || wsDisabled) {
|
||||||
|
return // WebSocket 被禁用,靜默返回
|
||||||
|
}
|
||||||
|
|
||||||
if (this.socket && this.isConnected) {
|
if (this.socket && this.isConnected) {
|
||||||
this.socket.emit(event, data)
|
this.socket.emit(event, data)
|
||||||
}
|
}
|
||||||
@@ -345,6 +373,15 @@ class WebSocketService {
|
|||||||
* @param {Function} callback - 回調函數
|
* @param {Function} callback - 回調函數
|
||||||
*/
|
*/
|
||||||
on(event, callback) {
|
on(event, callback) {
|
||||||
|
// 檢查 WebSocket 是否被禁用
|
||||||
|
const devMode = import.meta.env.VITE_DEV_MODE === 'true'
|
||||||
|
const isProd = import.meta.env.PROD
|
||||||
|
const wsDisabled = import.meta.env.VITE_DISABLE_WEBSOCKET === 'true'
|
||||||
|
|
||||||
|
if (!devMode || isProd || wsDisabled) {
|
||||||
|
return // WebSocket 被禁用,靜默返回
|
||||||
|
}
|
||||||
|
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.on(event, callback)
|
this.socket.on(event, callback)
|
||||||
}
|
}
|
||||||
@@ -401,6 +438,17 @@ export const websocketService = new WebSocketService()
|
|||||||
|
|
||||||
// 自動連接(在需要時)
|
// 自動連接(在需要時)
|
||||||
export const initWebSocket = () => {
|
export const initWebSocket = () => {
|
||||||
|
// 檢查是否禁用 WebSocket (多種方式)
|
||||||
|
const devMode = import.meta.env.VITE_DEV_MODE === 'true'
|
||||||
|
const isProd = import.meta.env.PROD
|
||||||
|
const wsDisabled = import.meta.env.VITE_DISABLE_WEBSOCKET === 'true'
|
||||||
|
|
||||||
|
if (!devMode || isProd || wsDisabled) {
|
||||||
|
console.log('🔌 [WebSocket] WebSocket 連接已禁用', { devMode, isProd, wsDisabled })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔌 [WebSocket] 嘗試初始化 WebSocket 連接')
|
||||||
websocketService.connect()
|
websocketService.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
nginx/Dockerfile
Normal file
10
nginx/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM nginx:1.25-alpine
|
||||||
|
|
||||||
|
# Copy custom nginx configuration
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 12010
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
67
nginx/nginx.conf
Normal file
67
nginx/nginx.conf
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
multi_accept on;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 4096;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_comp_level 5;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||||
|
|
||||||
|
upstream app_backend {
|
||||||
|
server translator-app:12010 max_fails=3 fail_timeout=10s;
|
||||||
|
keepalive 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 12010;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Adjust for document uploads (can be large)
|
||||||
|
client_max_body_size 500m;
|
||||||
|
|
||||||
|
# Proxy API requests to Flask/Gunicorn
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://app_backend;
|
||||||
|
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_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 600s; # Longer timeout for translation processing
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_buffering off; # Disable buffering for real-time progress
|
||||||
|
}
|
||||||
|
|
||||||
|
# All other routes (frontend SPA and static) via backend
|
||||||
|
location / {
|
||||||
|
proxy_pass http://app_backend;
|
||||||
|
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_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
proxy_send_timeout 120s;
|
||||||
|
proxy_connect_timeout 5s;
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffers 32 32k;
|
||||||
|
proxy_busy_buffers_size 64k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -3,7 +3,7 @@ Flask==3.0.0
|
|||||||
Flask-SQLAlchemy==3.1.1
|
Flask-SQLAlchemy==3.1.1
|
||||||
Flask-Session==0.5.0
|
Flask-Session==0.5.0
|
||||||
Flask-Cors==4.0.0
|
Flask-Cors==4.0.0
|
||||||
Flask-SocketIO==5.3.6
|
# Flask-SocketIO==5.3.6 # Temporarily disabled
|
||||||
Flask-JWT-Extended==4.6.0
|
Flask-JWT-Extended==4.6.0
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
@@ -33,7 +33,7 @@ pysbd==0.3.4
|
|||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
Werkzeug==3.0.1
|
Werkzeug==3.0.1
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
eventlet==0.33.3
|
gevent==23.9.1
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
|
Reference in New Issue
Block a user