fix timezone bug
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,6 +15,7 @@ __pycache__/
|
||||
# --- 使用者上傳與系統產生的檔案 (User Uploads & Generated Files) ---
|
||||
# 忽略上傳的已簽核文件 (PDFs)。
|
||||
/uploads/
|
||||
static/generated/
|
||||
|
||||
# 忽略系統自動產生的暫時規範文件 (Word, PDF)。
|
||||
/generated/
|
||||
@@ -51,3 +52,5 @@ tests/
|
||||
# 最佳實踐文件(包含敏感設定資訊)
|
||||
BEST_PRACTICES.md
|
||||
DEVELOPER_GUIDE.md
|
||||
|
||||
static/generated/PE1140901.docx
|
||||
|
559
DEPLOYMENT.md
559
DEPLOYMENT.md
@@ -1,25 +1,28 @@
|
||||
# 部署指南 - 暫時規範管理系統 V4
|
||||
|
||||
本文件提供詳細的部署指導,涵蓋 Docker 環境部署方式。
|
||||
本文件提供詳細的部署指導,涵蓋 Docker 環境部署方式及 V4 版本的新特性配置。
|
||||
|
||||
## 🎉 生產環境優化完成
|
||||
## 🎉 V4.0 版本優化完成
|
||||
|
||||
**✅ 已完成50人併發生產環境優化**:
|
||||
- Gunicorn WSGI部署(多進程併發)
|
||||
- Redis快取系統(提升效能)
|
||||
- Nginx反向代理(負載均衡)
|
||||
- CDN支援(靜態資源加速)
|
||||
- 資源限制(防止系統過載)
|
||||
- 監控工具(效能監控)
|
||||
**✅ 已完成企業級生產環境優化**:
|
||||
- 台灣時區 (GMT+8) 完整支援
|
||||
- 展延次數限制功能(最多2次,90天上限)
|
||||
- OnlyOffice 文件同步問題修正
|
||||
- Redis 快取系統(提升效能)
|
||||
- Nginx 反向代理(負載均衡)
|
||||
- 容器間網路優化
|
||||
- 時區處理模組化
|
||||
- UI 樣式改進
|
||||
|
||||
## 📋 目錄
|
||||
|
||||
1. [系統需求](#1-系統需求)
|
||||
2. [快速部署](#2-快速部署)
|
||||
3. [生產環境配置](#3-生產環境配置)
|
||||
4. [監控與管理](#4-監控與管理)
|
||||
5. [服務訪問](#5-服務訪問)
|
||||
6. [疑難排解](#6-疑難排解)
|
||||
4. [V4.0 新功能配置](#4-v40-新功能配置)
|
||||
5. [監控與管理](#5-監控與管理)
|
||||
6. [服務訪問](#6-服務訪問)
|
||||
7. [疑難排解](#7-疑難排解)
|
||||
|
||||
## 1. 系統需求
|
||||
|
||||
@@ -28,18 +31,25 @@
|
||||
- [ ] Docker 20.10+ 已安裝且運行中
|
||||
- [ ] Docker Compose 2.0+ 已安裝
|
||||
- [ ] 外部 MySQL 資料庫可訪問 (mysql.theaken.com:33306)
|
||||
- [ ] LDAP/Active Directory 伺服器可連線
|
||||
- [ ] SMTP 郵件伺服器已配置
|
||||
- [ ] LDAP/Active Directory 伺服器可連線 (panjit.com.tw)
|
||||
- [ ] SMTP 郵件伺服器已配置 (mail.panjit.com.tw)
|
||||
- [ ] 足夠的磁碟空間 (建議至少 10GB)
|
||||
|
||||
### 端口需求
|
||||
|
||||
確保以下端口未被占用:
|
||||
- `12010`: Flask 應用程式(Gunicorn)
|
||||
- `12011`: OnlyOffice 文檔服務
|
||||
- `12013`: Nginx HTTP(反向代理)
|
||||
- `12014`: Nginx HTTPS(反向代理)
|
||||
- `6379`: Redis 快取(內部)
|
||||
- `12010`: Flask 應用程式(內部,可選直接訪問)
|
||||
- `12013`: Nginx HTTP(反向代理,主要入口)
|
||||
- `12015`: OnlyOffice 文檔服務
|
||||
- `6379`: Redis 快取(容器內部)
|
||||
|
||||
### V4.0 網路架構
|
||||
|
||||
```
|
||||
用戶 → Nginx (12013) → Flask App (5000) → MySQL (外部)
|
||||
↓ ↓
|
||||
Redis (6379) OnlyOffice (80)
|
||||
```
|
||||
|
||||
## 2. 快速部署
|
||||
|
||||
@@ -50,12 +60,12 @@
|
||||
git clone <repository-url>
|
||||
cd TEMP_spec_system_V4
|
||||
|
||||
# 2. 配置環境變數
|
||||
cp .env.production .env
|
||||
# 編輯 .env 文件,填入實際的配置值
|
||||
# 2. 配置環境變數(使用預設配置)
|
||||
cp .env.example .env
|
||||
# 編輯 .env 文件,大部分配置已預設好
|
||||
|
||||
# 3. 啟動所有服務(生產環境優化版本)
|
||||
docker-compose up -d
|
||||
# 3. 啟動所有服務(V4.0優化版本)
|
||||
docker-compose up -d --build
|
||||
|
||||
# 4. 檢查服務狀態
|
||||
docker-compose ps
|
||||
@@ -63,11 +73,11 @@ docker-compose ps
|
||||
|
||||
**預期輸出應包含以下服務**:
|
||||
```
|
||||
NAME STATUS PORTS
|
||||
tempspec-redis Up (healthy)
|
||||
tempspec-onlyoffice Up (healthy) 0.0.0.0:12011->80/tcp
|
||||
tempspec-app Up (healthy) 0.0.0.0:12010->5000/tcp
|
||||
tempspec-nginx Up 0.0.0.0:12013->80/tcp, 0.0.0.0:12014->443/tcp
|
||||
NAME STATUS PORTS
|
||||
panjit-tempspec-redis Up (healthy)
|
||||
panjit-tempspec-onlyoffice Up (healthy) 0.0.0.0:12015->80/tcp
|
||||
panjit-tempspec-app Up (healthy)
|
||||
panjit-tempspec-nginx Up 0.0.0.0:12013->80/tcp
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -76,262 +86,349 @@ docker-compose logs -f
|
||||
|
||||
# 6. 驗證服務可訪問性
|
||||
curl -I http://localhost:12013/login # Nginx 反向代理(推薦)
|
||||
curl -I http://localhost:12010/login # 直接訪問 Flask
|
||||
curl -I http://localhost:12011 # OnlyOffice 服務
|
||||
curl -I http://localhost:12015 # OnlyOffice 服務
|
||||
```
|
||||
|
||||
### 初始化資料庫
|
||||
|
||||
```bash
|
||||
# 初始化資料庫結構
|
||||
docker-compose exec app python init_db.py
|
||||
|
||||
# 檢查資料庫連接
|
||||
docker-compose exec app python -c "
|
||||
from app import app
|
||||
from models import db
|
||||
with app.app_context():
|
||||
try:
|
||||
db.engine.execute('SELECT 1')
|
||||
print('✅ Database connection successful')
|
||||
except Exception as e:
|
||||
print(f'❌ Database connection failed: {e}')
|
||||
"
|
||||
```
|
||||
|
||||
## 3. 生產環境配置
|
||||
|
||||
### 3.1 服務架構(生產優化)
|
||||
### 3.1 服務架構(V4.0 優化)
|
||||
|
||||
```
|
||||
用戶請求 → Nginx (12013/12014) → Gunicorn (多進程) → Flask App
|
||||
↓
|
||||
Redis快取
|
||||
↓
|
||||
外部MySQL資料庫
|
||||
外部用戶 → Nginx (12013) → Flask App (Gunicorn) → 外部MySQL
|
||||
↓ ↓
|
||||
Redis快取 OnlyOffice (12015)
|
||||
↓
|
||||
台灣時區處理模組
|
||||
```
|
||||
|
||||
**服務組件**:
|
||||
- **Nginx**: 反向代理 + 靜態檔案 + 負載均衡(端口 12013/12014)
|
||||
- **Flask 應用**: Gunicorn WSGI伺服器(多進程,端口 12010)
|
||||
**V4.0 服務組件**:
|
||||
- **Nginx**: 反向代理 + 靜態檔案(端口 12013)
|
||||
- **Flask 應用**: Gunicorn WSGI 伺服器(內部端口 5000)
|
||||
- **Redis**: 快取系統(內部端口 6379)
|
||||
- **OnlyOffice**: 文檔編輯服務(端口 12011)
|
||||
- **MySQL**: 外部資料庫服務(mysql.theaken.com)
|
||||
- **OnlyOffice**: 文檔編輯服務(端口 12015)
|
||||
- **MySQL**: 外部資料庫服務(mysql.theaken.com:33306)
|
||||
|
||||
### 3.2 效能規格(50人併發支援)
|
||||
### 3.2 V4.0 效能規格
|
||||
|
||||
- **併發處理**: 2-8個Gunicorn worker進程
|
||||
- **記憶體使用**: App容器1GB + Redis 256MB
|
||||
- **快取命中**: Redis快取減少70%+資料庫查詢
|
||||
- **時區支援**: 全系統台灣時區 (GMT+8) 處理
|
||||
- **展延控制**: 最多2次展延,90天效期上限
|
||||
- **文件同步**: 增強的 OnlyOffice 回調處理
|
||||
- **快取優化**: Redis 減少資料庫查詢負載
|
||||
- **網路優化**: 容器間通信使用服務名稱
|
||||
- **響應時間**: < 200ms(快取命中時)
|
||||
- **可用性**: 99.9%+(健康檢查 + 自動重啟)
|
||||
|
||||
### 3.3 環境變數配置
|
||||
|
||||
編輯 `.env` 檔案(基於 `.env.production` 範例):
|
||||
編輯 `.env` 檔案:
|
||||
|
||||
```env
|
||||
# 生產環境基本設定
|
||||
FLASK_ENV=production
|
||||
# V4.0 基本設定
|
||||
SECRET_KEY=your_super_secret_production_key_here
|
||||
FLASK_ENV=production
|
||||
|
||||
# 服務端口
|
||||
APP_PORT=12010 # Gunicorn WSGI伺服器
|
||||
ONLYOFFICE_PORT=12011 # OnlyOffice 服務
|
||||
NGINX_PORT=12013 # Nginx HTTP
|
||||
NGINX_SSL_PORT=12014 # Nginx HTTPS
|
||||
# 外部 MySQL 資料庫
|
||||
DATABASE_URL=mysql+pymysql://A060:WLeSCi0yhtc7@mysql.theaken.com:33306/db_A060
|
||||
|
||||
# Redis 快取
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
|
||||
# CDN 支援 (可選)
|
||||
CDN_DOMAIN=cdn.yourcompany.com
|
||||
|
||||
# 資料庫連線
|
||||
DATABASE_URL=mysql+pymysql://user:pass@mysql.theaken.com:33306/dbname
|
||||
|
||||
# LDAP 設定
|
||||
# LDAP 設定(V4.0預設配置)
|
||||
LDAP_SERVER=panjit.com.tw
|
||||
LDAP_PORT=389
|
||||
LDAP_USE_SSL=false
|
||||
LDAP_SEARCH_BASE=OU=PANJIT,DC=panjit,DC=com,DC=tw
|
||||
LDAP_SEARCH_BASE=DC=panjit,DC=com,DC=tw
|
||||
LDAP_BIND_USER_DN=CN=LdapBind,CN=Users,DC=PANJIT,DC=COM,DC=TW
|
||||
LDAP_BIND_USER_PASSWORD=your_ldap_password
|
||||
LDAP_BIND_USER_PASSWORD=panjit2481
|
||||
LDAP_USER_LOGIN_ATTR=userPrincipalName
|
||||
|
||||
# SMTP 設定
|
||||
# SMTP 設定(V4.0預設配置)
|
||||
SMTP_SERVER=mail.panjit.com.tw
|
||||
SMTP_PORT=25
|
||||
SMTP_USE_TLS=false
|
||||
SMTP_USE_SSL=false
|
||||
SMTP_AUTH_REQUIRED=false
|
||||
SMTP_SENDER_EMAIL=temp-spec-system@panjit.com.tw
|
||||
SMTP_SENDER_PASSWORD=
|
||||
|
||||
# ONLYOFFICE 設定
|
||||
ONLYOFFICE_JWT_SECRET=your_onlyoffice_jwt_secret
|
||||
# OnlyOffice 設定
|
||||
ONLYOFFICE_URL=http://localhost:12015/
|
||||
ONLYOFFICE_INTERNAL_URL=http://onlyoffice:80
|
||||
ONLYOFFICE_JWT_SECRET=your_jwt_secret_key_here
|
||||
|
||||
# 服務端口設定
|
||||
ONLYOFFICE_PORT=12015
|
||||
```
|
||||
|
||||
## 4. 監控與管理
|
||||
## 4. V4.0 新功能配置
|
||||
|
||||
### 4.1 系統監控
|
||||
### 4.1 台灣時區支援
|
||||
|
||||
**自動配置**:
|
||||
- Docker 容器環境變數:`TZ: Asia/Taipei`
|
||||
- Python 時區模組:`utils/timezone.py`
|
||||
- 資料庫時間自動轉換為台灣時區
|
||||
|
||||
**驗證時區設定**:
|
||||
```bash
|
||||
# 即時監控(每5秒刷新)
|
||||
python monitor.py --watch 5
|
||||
# 檢查容器時區
|
||||
docker exec panjit-tempspec-app date
|
||||
|
||||
# 單次檢查
|
||||
python monitor.py
|
||||
|
||||
# JSON格式輸出
|
||||
python monitor.py --json
|
||||
# 測試 Python 時區功能
|
||||
docker exec panjit-tempspec-app python -c "
|
||||
from utils.timezone import taiwan_now, format_taiwan_time
|
||||
print('Current Taiwan time:', format_taiwan_time(taiwan_now()))
|
||||
"
|
||||
```
|
||||
|
||||
### 4.2 Docker 管理命令
|
||||
### 4.2 展延次數限制
|
||||
|
||||
**功能說明**:
|
||||
- 每個暫時規範最多展延2次
|
||||
- 總效期上限90天
|
||||
- 前端按鈕自動禁用
|
||||
- 後端邏輯驗證
|
||||
|
||||
**配置驗證**:
|
||||
```bash
|
||||
# 查看所有服務狀態
|
||||
docker-compose ps
|
||||
|
||||
# 查看實時日誌
|
||||
docker-compose logs -f
|
||||
|
||||
# 查看特定服務日誌
|
||||
docker-compose logs -f app
|
||||
docker-compose logs -f redis
|
||||
docker-compose logs -f nginx
|
||||
|
||||
# 重啟服務
|
||||
docker-compose restart
|
||||
docker-compose restart app
|
||||
|
||||
# 停止所有服務
|
||||
docker-compose down
|
||||
|
||||
# 查看資源使用
|
||||
docker stats
|
||||
# 檢查展延限制邏輯
|
||||
docker exec panjit-tempspec-app python -c "
|
||||
from app import app
|
||||
with app.app_context():
|
||||
print('Extension limit logic loaded successfully')
|
||||
"
|
||||
```
|
||||
|
||||
### 4.3 Redis 快取管理
|
||||
### 4.3 OnlyOffice 文件同步
|
||||
|
||||
**V4.0 改進**:
|
||||
- 支援多種儲存狀態(status=2, status=6)
|
||||
- 增強回調 URL 處理
|
||||
- 修正容器間網路通信
|
||||
|
||||
**同步測試**:
|
||||
```bash
|
||||
# 連接Redis並測試
|
||||
# 測試 OnlyOffice 網路連接
|
||||
docker exec panjit-tempspec-onlyoffice curl -I http://panjit-tempspec-nginx:80/
|
||||
|
||||
# 檢查回調處理日誌
|
||||
docker-compose logs app | grep "OnlyOffice callback"
|
||||
```
|
||||
|
||||
### 4.4 Redis 快取系統
|
||||
|
||||
**配置檢查**:
|
||||
```bash
|
||||
# 檢查 Redis 連接
|
||||
docker-compose exec redis redis-cli ping
|
||||
|
||||
# 查看快取統計
|
||||
docker-compose exec redis redis-cli info stats
|
||||
|
||||
# 清空所有快取
|
||||
docker-compose exec redis redis-cli FLUSHALL
|
||||
|
||||
# 查看快取鍵值數量
|
||||
docker-compose exec redis redis-cli DBSIZE
|
||||
# 監控快取使用
|
||||
docker-compose exec redis redis-cli monitor
|
||||
```
|
||||
|
||||
## 5. 服務訪問
|
||||
## 5. 監控與管理
|
||||
|
||||
### 5.1 服務入口
|
||||
### 5.1 系統監控
|
||||
|
||||
服務啟動後,可透過以下 URL 訪問:
|
||||
```bash
|
||||
# V4.0 系統監控(如有 monitor.py)
|
||||
python monitor.py --watch 5
|
||||
|
||||
# Docker 容器監控
|
||||
docker-compose ps
|
||||
docker stats
|
||||
|
||||
# 服務健康檢查
|
||||
docker-compose logs --tail=50 app
|
||||
docker-compose logs --tail=50 nginx
|
||||
docker-compose logs --tail=50 redis
|
||||
docker-compose logs --tail=50 onlyoffice
|
||||
```
|
||||
|
||||
### 5.2 V4.0 特定檢查
|
||||
|
||||
```bash
|
||||
# 時區功能檢查
|
||||
docker exec panjit-tempspec-app python -c "
|
||||
from utils.timezone import taiwan_now, to_taiwan_time
|
||||
from datetime import datetime
|
||||
print('Taiwan now:', taiwan_now())
|
||||
print('UTC to Taiwan:', to_taiwan_time(datetime.utcnow()))
|
||||
"
|
||||
|
||||
# 展延限制檢查
|
||||
docker exec panjit-tempspec-app python -c "
|
||||
from models import TempSpec
|
||||
from app import app
|
||||
with app.app_context():
|
||||
specs = TempSpec.query.filter(TempSpec.extension_count >= 2).count()
|
||||
print(f'Specs at extension limit: {specs}')
|
||||
"
|
||||
|
||||
# OnlyOffice 同步檢查
|
||||
docker-compose logs app | grep -E "(callback|sync|save)" | tail -10
|
||||
```
|
||||
|
||||
### 5.3 Redis 快取管理
|
||||
|
||||
```bash
|
||||
# 快取狀態檢查
|
||||
docker-compose exec redis redis-cli info memory
|
||||
docker-compose exec redis redis-cli dbsize
|
||||
|
||||
# 清空快取(如需要)
|
||||
docker-compose exec redis redis-cli flushall
|
||||
|
||||
# 監控快取命中率
|
||||
docker-compose exec redis redis-cli info stats | grep hit
|
||||
```
|
||||
|
||||
## 6. 服務訪問
|
||||
|
||||
### 6.1 V4.0 服務入口
|
||||
|
||||
**主要服務**:
|
||||
- **主應用程式 (Nginx)**: http://localhost:12013/login 🌟 **推薦**
|
||||
- **主應用程式 (直接)**: http://localhost:12010/login
|
||||
- **OnlyOffice 服務**: http://localhost:12011
|
||||
|
||||
**推薦使用方式(生產環境)**:
|
||||
- 使用 Nginx 反向代理: `http://localhost:12013`
|
||||
- 直接訪問 Flask: `http://localhost:12010`
|
||||
|
||||
### 5.2 登入資訊
|
||||
- **主應用程式 (Nginx)**: http://localhost:12013 🌟 **推薦**
|
||||
- **OnlyOffice 服務**: http://localhost:12015
|
||||
|
||||
**V4.0 登入資訊**:
|
||||
- **認證方式**: LDAP/Active Directory
|
||||
- **登入帳號**: 使用公司 LDAP 帳號密碼
|
||||
- **登入格式**: 支援 `username@panjit.com.tw` 或 `username`
|
||||
- **登入格式**: `username@panjit.com.tw`
|
||||
- **時區顯示**: 所有時間使用台灣時區 (GMT+8)
|
||||
|
||||
### 5.3 預設管理員帳號
|
||||
### 6.2 V4.0 新功能驗證
|
||||
|
||||
如需創建本地管理員帳號(非LDAP):
|
||||
登入後驗證以下新功能:
|
||||
|
||||
1. **時區顯示**: 檢查所有時間是否使用台灣時區
|
||||
2. **展延限制**: 查看已展延2次的規範是否正確顯示限制
|
||||
3. **文件同步**: 測試 OnlyOffice 編輯和儲存功能
|
||||
4. **UI 改進**: 檢查展延狀態在深色背景下的可讀性
|
||||
|
||||
## 7. 疑難排解
|
||||
|
||||
### 7.1 V4.0 特定問題
|
||||
|
||||
**時區顯示錯誤**
|
||||
```bash
|
||||
# 進入應用容器
|
||||
docker-compose exec app python update_admin.py
|
||||
|
||||
# 或手動創建
|
||||
docker-compose exec app python -c "
|
||||
from models import db, User
|
||||
from werkzeug.security import generate_password_hash
|
||||
from app import app
|
||||
|
||||
with app.app_context():
|
||||
admin = User(
|
||||
username='admin',
|
||||
email='admin@company.com',
|
||||
password_hash=generate_password_hash('admin123'),
|
||||
is_admin=True
|
||||
)
|
||||
db.session.add(admin)
|
||||
db.session.commit()
|
||||
print('管理員帳號已創建: admin/admin123')
|
||||
# 檢查容器時區設定
|
||||
docker exec panjit-tempspec-app date
|
||||
docker exec panjit-tempspec-app python -c "
|
||||
import os
|
||||
print('TZ:', os.environ.get('TZ', 'Not set'))
|
||||
"
|
||||
```
|
||||
|
||||
## 6. 疑難排解
|
||||
|
||||
### 6.1 生產環境常見問題
|
||||
|
||||
**Redis 連接失敗**
|
||||
```bash
|
||||
# 檢查Redis容器狀態
|
||||
docker-compose logs redis
|
||||
|
||||
# 測試Redis連接
|
||||
docker-compose exec redis redis-cli ping
|
||||
|
||||
# 重啟Redis
|
||||
docker-compose restart redis
|
||||
```
|
||||
|
||||
**應用程式無回應**
|
||||
```bash
|
||||
# 檢查Gunicorn日誌
|
||||
docker-compose logs app
|
||||
|
||||
# 檢查容器資源
|
||||
docker stats tempspec-app
|
||||
|
||||
# 重啟應用
|
||||
# 重新啟動應用容器
|
||||
docker-compose restart app
|
||||
```
|
||||
|
||||
**效能問題**
|
||||
**展延限制未生效**
|
||||
```bash
|
||||
# 檢查快取命中率
|
||||
python monitor.py
|
||||
# 檢查展延邏輯
|
||||
docker-compose logs app | grep -i extension
|
||||
|
||||
# 檢查Gunicorn worker狀態
|
||||
docker-compose exec app ps aux | grep gunicorn
|
||||
|
||||
# 調整worker數量(編輯gunicorn.conf.py)
|
||||
```
|
||||
|
||||
### 6.2 基本故障排除
|
||||
|
||||
**容器無法啟動**
|
||||
```bash
|
||||
# 檢查容器狀態
|
||||
docker-compose ps
|
||||
|
||||
# 查看詳細日誌
|
||||
docker-compose logs app
|
||||
docker-compose logs onlyoffice
|
||||
docker-compose logs redis
|
||||
```
|
||||
|
||||
**資料庫連線失敗**
|
||||
```bash
|
||||
# 測試資料庫連接
|
||||
docker-compose exec app python -c "
|
||||
import pymysql
|
||||
try:
|
||||
conn = pymysql.connect(host='mysql.theaken.com', port=33306, user='A060', password='WLeSCi0yhtc7', database='db_A060')
|
||||
print('Database connection successful')
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f'Database connection failed: {e}')
|
||||
# 測試展延限制邏輯
|
||||
docker exec panjit-tempspec-app python -c "
|
||||
from routes.temp_spec import check_extension_limit
|
||||
print('Extension limit logic available')
|
||||
"
|
||||
```
|
||||
|
||||
**端口衝突**
|
||||
修改 `.env` 文件中的端口設定:
|
||||
```env
|
||||
APP_PORT=12015 # 改為其他可用端口
|
||||
ONLYOFFICE_PORT=12016 # 改為其他可用端口
|
||||
NGINX_PORT=12017 # 改為其他可用端口
|
||||
**OnlyOffice 文件同步問題**
|
||||
```bash
|
||||
# 檢查 OnlyOffice 回調
|
||||
docker-compose logs app | grep "OnlyOffice callback"
|
||||
|
||||
# 檢查容器間網路
|
||||
docker exec panjit-tempspec-onlyoffice ping panjit-tempspec-nginx
|
||||
|
||||
# 檢查 OnlyOffice 健康狀態
|
||||
curl -I http://localhost:12015/healthcheck
|
||||
```
|
||||
|
||||
### 6.3 維護命令
|
||||
**Redis 快取問題**
|
||||
```bash
|
||||
# 檢查 Redis 連接
|
||||
docker-compose exec app python -c "
|
||||
import redis
|
||||
r = redis.Redis(host='redis', port=6379, db=0)
|
||||
print('Redis ping:', r.ping())
|
||||
"
|
||||
|
||||
# 重啟 Redis
|
||||
docker-compose restart redis
|
||||
```
|
||||
|
||||
### 7.2 容器網路問題
|
||||
|
||||
**容器間通信失敗**
|
||||
```bash
|
||||
# 檢查網路配置
|
||||
docker network ls | grep tempspec
|
||||
docker network inspect tempspec-network
|
||||
|
||||
# 測試容器間連接
|
||||
docker exec panjit-tempspec-app ping panjit-tempspec-redis
|
||||
docker exec panjit-tempspec-app ping panjit-tempspec-onlyoffice
|
||||
docker exec panjit-tempspec-app ping panjit-tempspec-nginx
|
||||
```
|
||||
|
||||
**端口衝突解決**
|
||||
```bash
|
||||
# 檢查端口占用
|
||||
netstat -tulpn | grep -E "(12013|12015)"
|
||||
|
||||
# 修改端口配置(編輯 docker-compose.yml)
|
||||
# 將衝突端口改為其他可用端口
|
||||
```
|
||||
|
||||
### 7.3 資料庫連接問題
|
||||
|
||||
**MySQL 連接測試**
|
||||
```bash
|
||||
# 測試外部資料庫連接
|
||||
docker-compose exec app python -c "
|
||||
import pymysql
|
||||
try:
|
||||
conn = pymysql.connect(
|
||||
host='mysql.theaken.com',
|
||||
port=33306,
|
||||
user='A060',
|
||||
password='WLeSCi0yhtc7',
|
||||
database='db_A060'
|
||||
)
|
||||
print('✅ Database connection successful')
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f'❌ Database connection failed: {e}')
|
||||
"
|
||||
```
|
||||
|
||||
### 7.4 V4.0 維護命令
|
||||
|
||||
```bash
|
||||
# 完全重建服務(清除快取)
|
||||
# 完全重建 V4.0 服務
|
||||
docker-compose down
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
@@ -339,33 +436,47 @@ docker-compose up -d
|
||||
# 更新單一服務
|
||||
docker-compose up -d --force-recreate app
|
||||
|
||||
# 清理未使用的 Docker 資源
|
||||
# 清理 Docker 資源
|
||||
docker system prune -a
|
||||
|
||||
# 備份重要資料
|
||||
docker exec panjit-tempspec-app tar -czf /tmp/uploads_backup.tar.gz uploads/
|
||||
docker cp panjit-tempspec-app:/tmp/uploads_backup.tar.gz ./uploads_backup.tar.gz
|
||||
```
|
||||
|
||||
### 6.4 效能調優
|
||||
### 7.5 V4.0 效能監控
|
||||
|
||||
**Redis 優化**
|
||||
```bash
|
||||
# 調整 Redis 記憶體限制(編輯 docker-compose.yml)
|
||||
# 預設: 256MB,可根據需要調整
|
||||
# 監控容器資源使用
|
||||
docker stats panjit-tempspec-app panjit-tempspec-redis panjit-tempspec-nginx
|
||||
|
||||
# 監控 Redis 使用
|
||||
docker-compose exec redis redis-cli info memory
|
||||
```
|
||||
# 檢查應用效能
|
||||
curl -w "@curl-format.txt" -o /dev/null -s http://localhost:12013/login
|
||||
|
||||
**Gunicorn 調優**
|
||||
```bash
|
||||
# 編輯 gunicorn.conf.py 調整:
|
||||
# - workers: worker 進程數量
|
||||
# - timeout: 請求超時時間
|
||||
# - max_requests: worker 重啟頻率
|
||||
# Redis 效能監控
|
||||
docker-compose exec redis redis-cli info stats | grep -E "(hits|misses|ops)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**🎉 生產環境部署完成!系統已準備好支援50人的併發使用。**
|
||||
## 🎉 V4.0 部署完成檢查清單
|
||||
|
||||
**快速啟動**: `docker-compose up -d`
|
||||
**系統監控**: `python monitor.py --watch 5`
|
||||
**服務訪問**: http://localhost:12013/login
|
||||
- [ ] 所有容器運行正常 (`docker-compose ps`)
|
||||
- [ ] 時區顯示為台灣時區 (GMT+8)
|
||||
- [ ] 展延限制功能正常 (最多2次)
|
||||
- [ ] OnlyOffice 文件同步正常
|
||||
- [ ] Redis 快取服務運行
|
||||
- [ ] Nginx 反向代理正常
|
||||
- [ ] LDAP 認證功能正常
|
||||
- [ ] 郵件通知功能正常
|
||||
- [ ] UI 樣式顯示正確
|
||||
|
||||
**快速啟動**: `docker-compose up -d --build`
|
||||
**服務訪問**: http://localhost:12013
|
||||
**健康檢查**: `docker-compose ps && docker-compose logs --tail=10`
|
||||
|
||||
---
|
||||
|
||||
**🚀 暫時規範管理系統 V4.0 部署完成!**
|
||||
|
||||
系統已具備完整的台灣時區支援、展延限制控制、優化的文件同步機制以及增強的用戶體驗。
|
17
Dockerfile.redis
Normal file
17
Dockerfile.redis
Normal file
@@ -0,0 +1,17 @@
|
||||
# Redis for PANJIT Temp Spec System
|
||||
FROM redis:7-alpine
|
||||
|
||||
# Set container labels for identification
|
||||
LABEL application="panjit-temp-spec-system"
|
||||
LABEL component="redis"
|
||||
LABEL version="v4.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"]
|
344
README.md
344
README.md
@@ -8,7 +8,8 @@
|
||||
- **ONLYOFFICE 線上編輯**:即時協作文件編輯功能
|
||||
- **智慧通知系統**:動態收件人選擇與自動提醒
|
||||
- **文件生命週期管理**:完整的建立、啟用、展延、終止流程
|
||||
- **多平台支援**:支援 Windows/Linux 環境部署
|
||||
- **時區完整支援**:台灣時區 (GMT+8) 全系統支援
|
||||
- **展延次數控制**:最多2次展延,總效期限制90天
|
||||
- **Docker 容器化**:一鍵部署環境
|
||||
|
||||
## 📋 功能模組
|
||||
@@ -18,6 +19,12 @@
|
||||
- **權限控制**:三級權限管理 (Viewer/Editor/Admin)
|
||||
- **歷史追蹤**:完整的操作記錄與版本控制
|
||||
- **檔案上傳**:支援多種格式的佐證文件上傳
|
||||
- **展延限制**:最多展延2次,總效期上限90天,防止無限延期
|
||||
|
||||
### 權限說明
|
||||
- **Viewer(檢視者)**:檢視所有規範、下載PDF檔案、檢視歷史紀錄
|
||||
- **Editor(編輯者)**:建立新規範、編輯內容、展延/終止規範、下載檔案
|
||||
- **Admin(管理員)**:所有Editor權限 + 啟用規範 + 刪除規範
|
||||
|
||||
### 智慧通知系統
|
||||
- **動態收件人選擇**:整合LDAP的即時用戶搜尋
|
||||
@@ -34,13 +41,15 @@
|
||||
## 🏗️ 系統架構
|
||||
|
||||
```
|
||||
暫時規範系統 V3
|
||||
暫時規範系統 V4
|
||||
├── 前端介面 (Flask + Bootstrap 5)
|
||||
├── 後端邏輯 (Python Flask)
|
||||
├── 資料庫 (MySQL/SQLite)
|
||||
├── 資料庫 (MySQL 外部)
|
||||
├── LDAP整合 (Active Directory)
|
||||
├── 文件引擎 (ONLYOFFICE)
|
||||
├── 快取服務 (Redis)
|
||||
├── 排程服務 (APScheduler)
|
||||
├── 反向代理 (Nginx)
|
||||
└── 郵件系統 (SMTP)
|
||||
```
|
||||
|
||||
@@ -52,15 +61,16 @@
|
||||
- **文件處理**:python-docx, docx2pdf
|
||||
- **認證系統**:Flask-Login + LDAP3
|
||||
- **排程系統**:Flask-APScheduler
|
||||
- **快取服務**:Redis
|
||||
- **反向代理**:Nginx
|
||||
- **容器化**:Docker + Docker Compose
|
||||
|
||||
## 📦 安裝部署
|
||||
|
||||
### 前置需求
|
||||
|
||||
- Python 3.8+
|
||||
- MySQL 8.0+ 或 SQLite
|
||||
- ONLYOFFICE Document Server
|
||||
- Docker & Docker Compose
|
||||
- 外部 MySQL 8.0+ 資料庫
|
||||
- LDAP/Active Directory 伺服器
|
||||
- SMTP 郵件伺服器
|
||||
|
||||
@@ -69,7 +79,7 @@
|
||||
1. **克隆專案**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd TEMP_spec_system_V3
|
||||
cd TEMP_spec_system_V4
|
||||
```
|
||||
|
||||
2. **設定環境變數**
|
||||
@@ -80,7 +90,17 @@ cp .env.example .env
|
||||
|
||||
3. **使用Docker Compose啟動**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
# 建置並啟動所有服務(強制重建以確保使用最新代碼)
|
||||
docker-compose up -d --build
|
||||
|
||||
# 檢查服務狀態
|
||||
docker-compose ps
|
||||
|
||||
# 停止服務
|
||||
docker-compose down
|
||||
|
||||
# 查看日誌
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
4. **初始化資料庫**
|
||||
@@ -88,95 +108,16 @@ docker-compose up -d
|
||||
docker-compose exec app python init_db.py
|
||||
```
|
||||
|
||||
5. **資料庫遷移(如果需要)**
|
||||
```bash
|
||||
# 新增郵件功能欄位
|
||||
docker-compose exec app python migrate_add_email_column.py
|
||||
```
|
||||
5. **系統訪問**
|
||||
|
||||
### 手動安裝
|
||||
啟動完成後,您可以通過以下地址訪問系統:
|
||||
|
||||
#### Windows 環境
|
||||
- **主系統入口**:http://localhost:12013 (推薦使用,通過 Nginx 代理)
|
||||
- **ONLYOFFICE 文件服務器**:http://localhost:12015
|
||||
|
||||
1. **安裝Python依賴**
|
||||
```cmd
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **設定環境變數**
|
||||
```cmd
|
||||
copy .env.example .env
|
||||
REM 編輯 .env 檔案
|
||||
```
|
||||
|
||||
3. **初始化資料庫**
|
||||
```cmd
|
||||
python init_db.py
|
||||
```
|
||||
|
||||
4. **資料庫遷移(如果需要)**
|
||||
```cmd
|
||||
python migrate_add_email_column.py
|
||||
```
|
||||
|
||||
5. **啟動 ONLYOFFICE Document Server**
|
||||
```cmd
|
||||
docker run -d -p 8080:80 --restart=always ^
|
||||
-e JWT_ENABLED=true ^
|
||||
-e JWT_SECRET=your-onlyoffice-jwt-secret-string ^
|
||||
onlyoffice/documentserver
|
||||
```
|
||||
|
||||
6. **啟動應用程式**
|
||||
```cmd
|
||||
REM 開發環境
|
||||
python app.py
|
||||
|
||||
REM 生產環境 (Windows 建議使用 Waitress)
|
||||
pip install waitress
|
||||
waitress-serve --host=0.0.0.0 --port=5000 app:app
|
||||
```
|
||||
|
||||
#### Linux 環境
|
||||
|
||||
1. **安裝Python依賴**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **設定環境變數**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 編輯 .env 檔案
|
||||
```
|
||||
|
||||
3. **初始化資料庫**
|
||||
```bash
|
||||
python init_db.py
|
||||
```
|
||||
|
||||
4. **資料庫遷移(如果需要)**
|
||||
```bash
|
||||
python migrate_add_email_column.py
|
||||
```
|
||||
|
||||
5. **啟動 ONLYOFFICE Document Server**
|
||||
```bash
|
||||
docker run -d -p 8080:80 --restart=always \
|
||||
-e JWT_ENABLED=true \
|
||||
-e JWT_SECRET=your-onlyoffice-jwt-secret-string \
|
||||
onlyoffice/documentserver
|
||||
```
|
||||
|
||||
6. **啟動應用程式**
|
||||
```bash
|
||||
# 開發環境
|
||||
python app.py
|
||||
|
||||
# 生產環境 (使用 Gunicorn)
|
||||
pip install gunicorn
|
||||
gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||
```
|
||||
> **注意**:
|
||||
> - 建議使用 Nginx 代理入口 (12013) 訪問系統,具有更好的性能和安全性
|
||||
> - 首次啟動 ONLYOFFICE 可能需要 2-3 分鐘初始化,請等待容器狀態變為 `healthy`
|
||||
|
||||
## ⚙️ 組態設定
|
||||
|
||||
@@ -185,51 +126,48 @@ gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||
```env
|
||||
# Flask 設定
|
||||
SECRET_KEY=your_secret_key_here
|
||||
UPLOAD_FOLDER=uploads
|
||||
FLASK_ENV=production
|
||||
|
||||
# 資料庫設定
|
||||
DATABASE_URL=mysql+pymysql://user:password@localhost/tempspec_db
|
||||
# 資料庫設定 (外部 MySQL)
|
||||
DATABASE_URL=mysql+pymysql://A060:WLeSCi0yhtc7@mysql.theaken.com:33306/db_A060
|
||||
|
||||
# LDAP 設定
|
||||
LDAP_SERVER=ldap://your-dc.company.com
|
||||
# LDAP 設定 (預設配置)
|
||||
LDAP_SERVER=panjit.com.tw
|
||||
LDAP_PORT=389
|
||||
LDAP_USE_SSL=False
|
||||
LDAP_SEARCH_BASE=DC=company,DC=com
|
||||
LDAP_BIND_USER_DN=CN=service,DC=company,DC=com
|
||||
LDAP_BIND_USER_PASSWORD=service_password
|
||||
LDAP_USE_SSL=false
|
||||
LDAP_SEARCH_BASE=DC=panjit,DC=com,DC=tw
|
||||
LDAP_BIND_USER_DN=CN=LdapBind,CN=Users,DC=PANJIT,DC=COM,DC=TW
|
||||
LDAP_BIND_USER_PASSWORD=panjit2481
|
||||
LDAP_USER_LOGIN_ATTR=userPrincipalName
|
||||
|
||||
# SMTP 郵件設定 (Port 25 無認證方式)
|
||||
SMTP_SERVER=mail.company.com
|
||||
# SMTP 郵件設定
|
||||
SMTP_SERVER=mail.panjit.com.tw
|
||||
SMTP_PORT=25
|
||||
SMTP_USE_TLS=false
|
||||
SMTP_USE_SSL=false
|
||||
SMTP_AUTH_REQUIRED=false
|
||||
SMTP_SENDER_EMAIL=temp-spec-system@company.com
|
||||
SMTP_SENDER_EMAIL=temp-spec-system@panjit.com.tw
|
||||
SMTP_SENDER_PASSWORD=
|
||||
|
||||
# ONLYOFFICE 設定
|
||||
ONLYOFFICE_URL=http://onlyoffice:8080
|
||||
ONLYOFFICE_JWT_SECRET=your_jwt_secret
|
||||
ONLYOFFICE_URL=http://localhost:12015/
|
||||
ONLYOFFICE_INTERNAL_URL=http://onlyoffice:80
|
||||
ONLYOFFICE_JWT_SECRET=your_jwt_secret_key_here
|
||||
|
||||
# Redis 設定
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
|
||||
# 服務端口設定
|
||||
ONLYOFFICE_PORT=12015
|
||||
```
|
||||
|
||||
### SMTP 配置說明
|
||||
|
||||
系統支援多種 SMTP 配置方式:
|
||||
|
||||
- **Port 25(推薦)**:內部郵件伺服器,無需認證
|
||||
- **Port 587**:STARTTLS + 認證
|
||||
- **Port 465**:SSL + 認證
|
||||
|
||||
詳細設定請參考 `SMTP_CONFIGURATION_UPDATE.md`
|
||||
|
||||
## 📚 使用說明
|
||||
|
||||
### 登入規範
|
||||
|
||||
**重要**:系統要求使用完整的UPN格式帳號登入
|
||||
|
||||
✅ **正確格式**:`user@domain.com`
|
||||
✅ **正確格式**:`user@panjit.com.tw`
|
||||
❌ **錯誤格式**:`user`
|
||||
|
||||
### 初次設定管理員
|
||||
@@ -240,9 +178,18 @@ ONLYOFFICE_JWT_SECRET=your_jwt_secret
|
||||
|
||||
2. **手動設定**:在資料庫中更新用戶權限:
|
||||
```sql
|
||||
UPDATE ts_user SET role='admin' WHERE username='user@domain.com';
|
||||
UPDATE ts_user SET role='admin' WHERE username='user@panjit.com.tw';
|
||||
```
|
||||
|
||||
### 展延次數限制
|
||||
|
||||
系統實作嚴格的展延控制機制:
|
||||
|
||||
- **初次生效**:30天效期
|
||||
- **第一次展延**:效期變為60天
|
||||
- **第二次展延**:效期變為90天(達到上限)
|
||||
- **第三次展延**:系統拒絕並顯示錯誤訊息
|
||||
|
||||
### 郵件通知功能
|
||||
|
||||
系統具備智慧郵件管理功能:
|
||||
@@ -281,32 +228,124 @@ UPDATE ts_user SET role='admin' WHERE username='user@domain.com';
|
||||
- 確認服務帳號權限
|
||||
- 驗證 LDAP 伺服器位址和搜尋基底
|
||||
|
||||
2. **ONLYOFFICE 無法載入**
|
||||
2. **ONLYOFFICE 無法載入文件**
|
||||
- 確認 Document Server 運行狀態:`docker ps`
|
||||
- 檢查網路連線設定
|
||||
- 檢查容器間網路連線:`docker exec panjit-tempspec-onlyoffice curl -s http://panjit-tempspec-nginx:80/health`
|
||||
- 驗證 JWT Secret 設定是否一致
|
||||
- 檢查日誌中的網路錯誤:`docker-compose logs onlyoffice | grep ERROR`
|
||||
|
||||
3. **郵件發送失敗**
|
||||
**常見錯誤**:
|
||||
- `ECONNREFUSED` 錯誤:通常是容器間網路配置問題
|
||||
- `host.docker.internal` 無法連接:應使用容器名稱而非 host.docker.internal
|
||||
- 文件下載失敗:檢查靜態檔案路徑和 nginx 代理設定
|
||||
|
||||
3. **ONLYOFFICE 文件同步問題**
|
||||
- **問題描述**:在 OnlyOffice 編輯器中編輯並儲存文件後,下載的 Word 文件沒有包含最新的編輯內容
|
||||
|
||||
**解決方案**:
|
||||
- 系統已增強 OnlyOffice 回調處理機制,支援多種儲存狀態
|
||||
- 改善了文件同步邏輯,確保編輯內容正確儲存到伺服器
|
||||
- 新增詳細的回調日誌記錄,便於問題診斷
|
||||
|
||||
**檢查方法**:
|
||||
```bash
|
||||
# 查看 OnlyOffice 回調日誌
|
||||
docker-compose logs -f app | grep "OnlyOffice callback"
|
||||
|
||||
# 檢查文件儲存狀態
|
||||
docker-compose logs -f app | grep "Document saved successfully"
|
||||
|
||||
# 驗證文件更新時間
|
||||
docker exec panjit-tempspec-app ls -la uploads/
|
||||
```
|
||||
|
||||
**技術細節**:
|
||||
- 回調處理現在支援 status=2 (自動儲存) 和 status=6 (強制儲存)
|
||||
- 增加了 JWT Token 驗證以確保回調安全性
|
||||
- 改善錯誤處理和檔案下載邏輯
|
||||
- 修正回調中的 URL 路由,將 localhost:12015 轉換為容器間通信地址 panjit-tempspec-onlyoffice:80
|
||||
|
||||
4. **時區設定問題**
|
||||
- **問題描述**:系統時間顯示為 GMT+0,但正確時區應該是 GMT+8(台灣時區)
|
||||
|
||||
**解決方案**:
|
||||
- 建立完整的時區處理模組 `utils/timezone.py`
|
||||
- 修正所有資料庫時間欄位使用台灣時區
|
||||
- 更新前端模板使用時區轉換 filter
|
||||
- 設定 Docker 容器環境變數 `TZ: Asia/Taipei`
|
||||
|
||||
**檢查方法**:
|
||||
```bash
|
||||
# 檢查容器時區設定
|
||||
docker exec panjit-tempspec-app date
|
||||
|
||||
# 測試時區轉換功能
|
||||
docker exec panjit-tempspec-app python -c "from utils.timezone import taiwan_now, format_taiwan_time; print(format_taiwan_time(taiwan_now()))"
|
||||
```
|
||||
|
||||
**技術細節**:
|
||||
- 新增 `taiwan_now()` 函數替代 `datetime.utcnow()`
|
||||
- 支援 `date` 和 `datetime` 物件的時區轉換
|
||||
- 前端模板 filter:`|taiwan_time` 和 `|taiwan_date`
|
||||
- 自動處理 naive datetime 和 timezone-aware datetime
|
||||
|
||||
5. **展延次數限制功能**
|
||||
- **功能說明**:防止暫時規範無限延期,確保企業治理合規
|
||||
|
||||
**限制規則**:
|
||||
- 初次生效:30天效期
|
||||
- 第一次展延:效期變為60天
|
||||
- 第二次展延:效期變為90天(達到上限)
|
||||
- 第三次展延:系統拒絕並顯示錯誤訊息
|
||||
|
||||
**實作功能**:
|
||||
- 後端邏輯檢查:當 `extension_count >= 2` 時拒絕展延
|
||||
- 前端介面優化:超過限制時展延按鈕變為不可用
|
||||
- 視覺化顯示:展延次數進度和剩餘次數
|
||||
- 樣式改進:深色背景下清楚可見的展延狀態顯示
|
||||
|
||||
6. **郵件發送失敗**
|
||||
- 確認 SMTP 設定正確
|
||||
- 檢查郵件伺服器認證設定
|
||||
- 驗證防火牆規則 (Port 25/587/465)
|
||||
|
||||
4. **排程任務未執行**
|
||||
7. **排程任務未執行**
|
||||
- 檢查 APScheduler 初始化
|
||||
- 確認應用程式持續運行
|
||||
- 查看系統日誌
|
||||
|
||||
8. **容器間網路通訊問題**
|
||||
- 檢查所有容器是否在同一網路:`docker network ls`
|
||||
- 驗證容器名稱解析:`docker exec container1 ping container2`
|
||||
- 確認端口映射:`docker-compose ps`
|
||||
|
||||
**OnlyOffice 網路問題修正**:
|
||||
```bash
|
||||
# 檢查 OnlyOffice 能否連接到 Flask 應用
|
||||
docker exec panjit-tempspec-onlyoffice curl -I http://panjit-tempspec-nginx:80/static/
|
||||
|
||||
# 檢查 Flask 應用日誌
|
||||
docker-compose logs -f app
|
||||
|
||||
# 重新構建有網路修正的應用
|
||||
docker-compose up -d --build app
|
||||
```
|
||||
|
||||
### 日誌查看
|
||||
|
||||
```bash
|
||||
# Docker 環境
|
||||
docker-compose logs -f app
|
||||
|
||||
# 一般環境
|
||||
tail -f logs/app.log
|
||||
# OnlyOffice 相關日誌
|
||||
docker-compose logs -f onlyoffice
|
||||
docker exec panjit-tempspec-onlyoffice tail -f /var/log/onlyoffice/documentserver/docservice/out.log
|
||||
|
||||
# Windows 環境
|
||||
Get-Content logs/app.log -Tail 10 -Wait
|
||||
# Redis 日誌
|
||||
docker-compose logs -f redis
|
||||
|
||||
# Nginx 日誌
|
||||
docker-compose logs -f nginx
|
||||
```
|
||||
|
||||
## 🤝 開發指南
|
||||
@@ -318,25 +357,43 @@ Get-Content logs/app.log -Tail 10 -Wait
|
||||
├── config.py # 組態設定
|
||||
├── models.py # 資料模型
|
||||
├── tasks.py # 排程任務
|
||||
├── wsgi.py # WSGI 入口點
|
||||
├── gunicorn.conf.py # Gunicorn 配置
|
||||
├── routes/ # 路由模組
|
||||
│ ├── __init__.py
|
||||
│ ├── auth.py # 認證相關
|
||||
│ ├── temp_spec.py # 暫規管理
|
||||
│ ├── upload.py # 檔案上傳
|
||||
│ ├── admin.py # 管理功能
|
||||
│ └── api.py # API介面
|
||||
├── templates/ # 前端範本
|
||||
├── static/ # 靜態檔案
|
||||
├── utils.py # 工具函式
|
||||
└── ldap_utils.py # LDAP 工具
|
||||
├── utils/ # 工具模組
|
||||
│ ├── __init__.py # 工具函式
|
||||
│ └── timezone.py # 時區處理
|
||||
├── ldap_utils.py # LDAP 工具
|
||||
├── cache_utils.py # 快取工具
|
||||
├── cdn_utils.py # CDN 工具
|
||||
├── monitor.py # 監控工具
|
||||
└── nginx/ # Nginx 配置
|
||||
├── Dockerfile
|
||||
├── nginx.conf
|
||||
└── conf.d/
|
||||
```
|
||||
|
||||
### 資料庫遷移
|
||||
### 容器架構
|
||||
|
||||
當系統需要資料庫結構更新時:
|
||||
- **panjit-tempspec-app**: Flask 應用程式容器
|
||||
- **panjit-tempspec-nginx**: Nginx 反向代理容器
|
||||
- **panjit-tempspec-onlyoffice**: OnlyOffice 文件服務器容器
|
||||
- **panjit-tempspec-redis**: Redis 快取服務容器
|
||||
|
||||
```bash
|
||||
# 執行遷移腳本
|
||||
python migrate_add_email_column.py
|
||||
```
|
||||
### 網路配置
|
||||
|
||||
所有容器運行在 `tempspec-network` 橋接網路中,確保容器間通信:
|
||||
|
||||
- 外部訪問:通過 Nginx (port 12013) 和 OnlyOffice (port 12015)
|
||||
- 內部通信:使用容器名稱作為主機名
|
||||
|
||||
## 📄 授權條款
|
||||
|
||||
@@ -344,7 +401,18 @@ python migrate_add_email_column.py
|
||||
|
||||
## 🆕 版本歷程
|
||||
|
||||
### v3.2.0 (最新版本)
|
||||
### v4.0.0 (最新版本)
|
||||
- 🆕 新增台灣時區 (GMT+8) 完整支援
|
||||
- 🆕 實作展延次數限制功能(最多2次,總效期90天)
|
||||
- 🆕 修正 OnlyOffice 文件同步問題
|
||||
- 🎨 改進規範列表頁面樣式和用戶體驗
|
||||
- 🔧 修正時區 filter 支援 date 物件處理
|
||||
- 🗑️ 移除舊版 utils.py,改用模組化架構
|
||||
- 🔧 修正容器間網路通信問題
|
||||
- 🆕 新增 Redis 快取服務
|
||||
- 🆕 新增 Nginx 反向代理
|
||||
|
||||
### v3.2.0
|
||||
- 🆕 新增郵件通知記憶功能
|
||||
- 🆕 支援 Port 25 無認證 SMTP
|
||||
- ♻️ 優化郵件管理邏輯
|
||||
@@ -363,4 +431,4 @@ python migrate_add_email_column.py
|
||||
|
||||
---
|
||||
|
||||
**暫時規範管理系統 V3** - 讓企業文件管理更智慧、更高效!
|
||||
**暫時規範管理系統 V4** - 讓企業文件管理更智慧、更高效!
|
164
USER_MANUAL.md
164
USER_MANUAL.md
@@ -1,6 +1,6 @@
|
||||
# 暫時規範管理系統 V3 操作說明書
|
||||
# 暫時規範管理系統 V4 操作說明書
|
||||
|
||||
歡迎使用企業級暫時規範管理系統 V3。本系統整合了LDAP認證、ONLYOFFICE線上編輯器及智慧通知系統,提供完整的文件生命週期管理解決方案。
|
||||
歡迎使用企業級暫時規範管理系統 V4。本系統整合了LDAP認證、ONLYOFFICE線上編輯器、智慧通知系統及台灣時區完整支援,提供完整的文件生命週期管理解決方案。
|
||||
|
||||
## 📋 目錄
|
||||
|
||||
@@ -16,16 +16,21 @@
|
||||
|
||||
## 1. 系統簡介
|
||||
|
||||
暫時規範管理系統 V3 是一個集中化平台,用於管理、追蹤和存檔所有暫時性的工程規範。它涵蓋了從草擬、線上編輯、簽核、生效到最終歸檔的完整生命週期。
|
||||
暫時規範管理系統 V4 是一個集中化平台,用於管理、追蹤和存檔所有暫時性的工程規範。它涵蓋了從草擬、線上編輯、簽核、生效到最終歸檔的完整生命週期。
|
||||
|
||||
### 🚀 V3.2 版本新特色
|
||||
### 🚀 V4.0 版本新特色
|
||||
|
||||
- **台灣時區完整支援**:所有時間顯示使用台灣時區 (GMT+8)
|
||||
- **展延次數限制**:最多2次展延,總效期上限90天
|
||||
- **文件同步改善**:修正OnlyOffice編輯同步問題
|
||||
- **UI樣式優化**:改善深色背景下的展延狀態顯示
|
||||
- **LDAP/AD 整合**:使用企業Active Directory帳號登入
|
||||
- **智慧郵件記憶**:自動記憶並帶出之前使用的通知對象
|
||||
- **彈性郵件編輯**:可編輯通知名單並更新記錄
|
||||
- **多種SMTP支援**:支援Port 25無認證及其他認證方式
|
||||
- **自動排程提醒**:系統主動發送到期提醒郵件
|
||||
- **增強的編輯體驗**:ONLYOFFICE文件協作編輯
|
||||
- **容器化架構**:Redis快取 + Nginx反向代理
|
||||
|
||||
---
|
||||
|
||||
@@ -37,14 +42,14 @@
|
||||
|
||||
**🚨 重要登入規範**:
|
||||
|
||||
✅ **正確格式**:必須使用完整的 UPN 格式帳號
|
||||
例如:`user@domain.com`
|
||||
✅ **正確格式**:必須使用完整的 UPN 格式帳號
|
||||
例如:`user@panjit.com.tw`
|
||||
|
||||
❌ **錯誤格式**:不支援縮略帳號
|
||||
❌ **錯誤格式**:不支援縮略帳號
|
||||
例如:`user`
|
||||
|
||||
**登入步驟**:
|
||||
1. 在登入頁面輸入您的 **完整 AD 帳號**(例如:user@domain.com)
|
||||
1. 在登入頁面輸入您的 **完整 AD 帳號**(例如:user@panjit.com.tw)
|
||||
2. 輸入您的 **AD 密碼**
|
||||
3. 點擊「**登入**」按鈕
|
||||
|
||||
@@ -60,7 +65,9 @@
|
||||
- **主題**:規範標題
|
||||
- **申請人**:規範申請者
|
||||
- **狀態**:pending_approval(待生效)/active(已生效)/expired(已過期)/terminated(已終止)
|
||||
- **時間範圍**:生效日期至結束日期
|
||||
- **時間範圍**:生效日期至結束日期(台灣時間顯示)
|
||||
- **剩餘天數**:彩色標示不同緊急程度
|
||||
- **展延狀態**:顯示已展延次數和剩餘次數
|
||||
- **操作按鈕**:依權限顯示不同功能
|
||||
|
||||
---
|
||||
@@ -102,9 +109,10 @@
|
||||
|
||||
**編輯器功能**:
|
||||
- 全功能 Word 文件編輯
|
||||
- 即時自動儲存
|
||||
- 即時自動儲存和同步
|
||||
- 支援圖片、表格插入
|
||||
- 格式化工具列
|
||||
- 增強的文件同步機制
|
||||
|
||||
### 3.3 啟用規範(Admin 權限)
|
||||
|
||||
@@ -122,22 +130,35 @@
|
||||
5. 系統自動:
|
||||
- 更新規範狀態
|
||||
- **記憶通知對象**供後續使用
|
||||
- 發送啟用通知郵件
|
||||
- 發送啟用通知郵件(台灣時間)
|
||||
|
||||
### 3.4 展延規範(Editor/Admin 權限)
|
||||
|
||||
延長已生效規範的結束日期:
|
||||
**🆕 V4.0 展延限制功能**:
|
||||
|
||||
延長已生效規範的結束日期,但有嚴格限制:
|
||||
|
||||
1. 點擊「**展延**」按鈕
|
||||
2. **設定新結束日期**:選擇展延後的日期
|
||||
3. **上傳佐證檔案**:提供展延理由相關文件(PDF格式)
|
||||
4. **🆕 智慧通知設定**:
|
||||
2. **檢查展延次數**:
|
||||
- 最多只能展延 **2次**
|
||||
- 總效期上限 **90天**
|
||||
- 已達上限的規範展延按鈕會變為不可用
|
||||
|
||||
3. **設定新結束日期**:選擇展延後的日期
|
||||
4. **上傳佐證檔案**:提供展延理由相關文件(PDF格式)
|
||||
5. **🆕 智慧通知設定**:
|
||||
- 系統自動帶出之前啟用時使用的通知對象
|
||||
- 可直接使用或進行編輯
|
||||
- 修改後的名單會更新到系統記錄中
|
||||
|
||||
5. 點擊「**確認展延**」
|
||||
6. 系統自動發送展延通知郵件
|
||||
6. 點擊「**確認展延**」
|
||||
7. 系統自動發送展延通知郵件
|
||||
|
||||
**展延規則**:
|
||||
- **初次生效**:30天效期
|
||||
- **第一次展延**:效期變為60天
|
||||
- **第二次展延**:效期變為90天(達到上限)
|
||||
- **第三次展延**:系統拒絕並顯示錯誤訊息
|
||||
|
||||
### 3.5 終止規範(Editor/Admin 權限)
|
||||
|
||||
@@ -152,7 +173,7 @@
|
||||
|
||||
4. 點擊「**確認終止**」
|
||||
5. 系統自動:
|
||||
- 更新結束日期為今日
|
||||
- 更新結束日期為今日(台灣時間)
|
||||
- 發送終止通知郵件
|
||||
|
||||
---
|
||||
@@ -161,11 +182,11 @@
|
||||
|
||||
### 4.1 🆕 郵件記憶功能
|
||||
|
||||
**V3.2 新增功能**:系統現在具備智慧郵件管理能力
|
||||
**V4.0 持續改進功能**:系統現在具備智慧郵件管理能力
|
||||
|
||||
**運作機制**:
|
||||
1. **規範啟用時**:輸入通知郵件對象,系統自動記憶
|
||||
2. **規範終止時**:自動帶出啟用時的郵件清單,可編輯後發送
|
||||
2. **規範終止時**:自動帶出啟用時的郵件清單,可編輯後發送
|
||||
3. **規範展延時**:自動帶出郵件清單,修改後會更新記錄
|
||||
|
||||
**操作說明**:
|
||||
@@ -195,18 +216,18 @@
|
||||
|
||||
**手動通知**(操作觸發):
|
||||
- 規範啟用通知
|
||||
- 規範展延通知
|
||||
- 規範展延通知
|
||||
- 規範終止通知
|
||||
|
||||
**🆕 自動提醒**(系統排程):
|
||||
- **7天到期提醒**:在規範到期前7天自動發送
|
||||
- **3天到期提醒**:在規範到期前3天自動發送
|
||||
- **發送時間**:每天凌晨2:00檢查並發送
|
||||
- **發送時間**:每天凌晨2:00檢查並發送(台灣時間)
|
||||
|
||||
**郵件內容**:
|
||||
- HTML格式美化顯示
|
||||
- 包含規範編號、標題、申請人
|
||||
- 明確標示生效/結束日期
|
||||
- 明確標示生效/結束日期(台灣時間顯示)
|
||||
- 提供系統連結
|
||||
|
||||
---
|
||||
@@ -239,18 +260,26 @@
|
||||
|
||||
點擊 **歷史紀錄圖示 (🕒)** 查看:
|
||||
|
||||
- 操作時間戳記
|
||||
- 操作時間戳記(台灣時間顯示)
|
||||
- 執行用戶
|
||||
- 操作類型(建立/啟用/展延/終止)
|
||||
- 詳細說明
|
||||
|
||||
### 5.4 即將到期警示
|
||||
### 5.4 即將到期警示與展延狀態
|
||||
|
||||
**到期警示**:
|
||||
在規範列表中會特別標示即將到期的規範:
|
||||
|
||||
- **🟢 綠色標示**:7天以上
|
||||
- **🟡 橙色標示**:7天內到期
|
||||
- **🔴 紅色標示**:3天內到期
|
||||
- **閃爍動畫**:今日到期
|
||||
- **🔴 紅色標示**:3天內到期
|
||||
- **已過期標示**:已過期
|
||||
|
||||
**🆕 展延狀態顯示**:
|
||||
- 清楚顯示「已展延 X 次」
|
||||
- 達到上限時顯示「達到上限」標籤
|
||||
- 優化深色背景下的可讀性
|
||||
- 居中對齊,提升視覺體驗
|
||||
|
||||
---
|
||||
|
||||
@@ -274,7 +303,7 @@
|
||||
**Editor(編輯者)**:
|
||||
- 建立新規範草稿
|
||||
- 編輯規範內容
|
||||
- 展延和終止規範
|
||||
- 展延和終止規範(受展延次數限制)
|
||||
- 下載Word和PDF檔案
|
||||
|
||||
**Admin(管理員)**:
|
||||
@@ -290,11 +319,11 @@
|
||||
### 7.1 登入相關
|
||||
|
||||
**Q: 忘記帳號格式?**
|
||||
A: 必須使用完整的 `user@domain.com` 格式,不能只輸入 `user`
|
||||
A: 必須使用完整的 `user@panjit.com.tw` 格式,不能只輸入 `user`
|
||||
|
||||
**Q: 無法登入?**
|
||||
A: 請確認:
|
||||
1. 帳號格式正確(包含@domain.com)
|
||||
1. 帳號格式正確(包含@panjit.com.tw)
|
||||
2. 密碼正確
|
||||
3. AD帳號未被鎖定
|
||||
4. 網路連線正常
|
||||
@@ -315,11 +344,13 @@ A: 請確認:
|
||||
2. 網路連線穩定
|
||||
3. 彈出視窗未被阻擋
|
||||
|
||||
**Q: 編輯內容未儲存?**
|
||||
A: 建議:
|
||||
**🆕 Q: 編輯內容未儲存或同步?**
|
||||
A: V4.0已改善文件同步機制:
|
||||
1. 編輯期間保持網路連線
|
||||
2. 避免同時多人編輯同一文件
|
||||
3. 定期手動儲存 (Ctrl+S)
|
||||
2. 系統現在支援多種儲存狀態
|
||||
3. 增強了回調處理機制
|
||||
4. 定期手動儲存 (Ctrl+S)
|
||||
5. 檢查容器間網路是否正常
|
||||
|
||||
### 7.4 通知相關
|
||||
|
||||
@@ -336,7 +367,7 @@ A: 請檢查:
|
||||
3. 公司郵件伺服器設定
|
||||
|
||||
**Q: 自動提醒郵件何時發送?**
|
||||
A: 系統每天凌晨2:00自動檢查並發送提醒,分別在到期前7天和3天發送。
|
||||
A: 系統每天凌晨2:00(台灣時間)自動檢查並發送提醒,分別在到期前7天和3天發送。
|
||||
|
||||
**🆕 Q: 郵件通知對象會自動記憶嗎?**
|
||||
A: 是的,系統會記憶啟用時設定的通知對象:
|
||||
@@ -344,7 +375,37 @@ A: 是的,系統會記憶啟用時設定的通知對象:
|
||||
- 展延規範時也會自動帶出,修改後會更新記錄
|
||||
- 您可以直接使用或編輯後再發送
|
||||
|
||||
### 7.5 檔案相關
|
||||
### 7.5 展延相關
|
||||
|
||||
**🆕 Q: 為什麼無法繼續展延?**
|
||||
A: V4.0實作嚴格的展延控制:
|
||||
- 每個規範最多只能展延2次
|
||||
- 總效期不能超過90天
|
||||
- 達到上限後系統會拒絕展延請求
|
||||
- 展延按鈕會變為不可用狀態
|
||||
|
||||
**🆕 Q: 展延次數如何計算?**
|
||||
A: 展延次數計算規則:
|
||||
- 初次生效:30天(不計入展延次數)
|
||||
- 第一次展延:效期變為60天(展延次數=1)
|
||||
- 第二次展延:效期變為90天(展延次數=2,達到上限)
|
||||
|
||||
### 7.6 時區相關
|
||||
|
||||
**🆕 Q: 系統顯示的時間是否正確?**
|
||||
A: V4.0完整支援台灣時區:
|
||||
- 所有時間顯示使用台灣時區 (GMT+8)
|
||||
- 資料庫儲存自動轉換為台灣時間
|
||||
- 郵件通知使用台灣時間
|
||||
- 到期檢查基於台灣時間
|
||||
|
||||
**🆕 Q: 舊紀錄的時間顯示是否正確?**
|
||||
A: 系統已實作時區轉換機制:
|
||||
- 自動處理舊紀錄的時區轉換
|
||||
- 支援date和datetime物件轉換
|
||||
- 確保所有時間顯示一致性
|
||||
|
||||
### 7.7 檔案相關
|
||||
|
||||
**Q: 可以上傳Word檔案來啟用規範嗎?**
|
||||
A: 不可以。為確保文件完整性,啟用時必須上傳已簽核的 **PDF檔案**。
|
||||
@@ -356,25 +417,34 @@ A: 請確認:
|
||||
3. 檔案名稱不含特殊字元
|
||||
4. 網路連線穩定
|
||||
|
||||
### 7.6 效能相關
|
||||
### 7.8 效能相關
|
||||
|
||||
**Q: 系統回應速度慢?**
|
||||
A: 可能原因:
|
||||
1. 網路連線問題
|
||||
2. 伺服器負載過高
|
||||
3. 資料庫查詢耗時
|
||||
4. 聯繫系統管理員檢查
|
||||
A: V4.0已優化效能:
|
||||
1. 新增Redis快取系統
|
||||
2. Nginx反向代理提升速度
|
||||
3. 如仍有問題請聯繫系統管理員
|
||||
4. 檢查網路連線狀況
|
||||
|
||||
---
|
||||
|
||||
## 📝 版本資訊
|
||||
|
||||
- **文件版本**: V3.2.0
|
||||
- **最後更新**: 2025年1月
|
||||
- **適用系統**: 暫時規範管理系統 V3.2
|
||||
- **文件版本**: V4.0.0
|
||||
- **最後更新**: 2025年9月
|
||||
- **適用系統**: 暫時規範管理系統 V4.0
|
||||
|
||||
### 版本更新記錄
|
||||
|
||||
**V4.0.0**:
|
||||
- 新增台灣時區完整支援
|
||||
- 實作展延次數限制功能(最多2次,90天上限)
|
||||
- 修正OnlyOffice文件同步問題
|
||||
- 改進UI樣式,優化深色背景下的顯示
|
||||
- 修正時區filter支援date物件處理
|
||||
- 移除舊版utils.py,改用模組化架構
|
||||
- 新增Redis快取和Nginx反向代理
|
||||
|
||||
**V3.2.0**:
|
||||
- 新增郵件通知記憶功能
|
||||
- 支援Port 25無認證SMTP
|
||||
@@ -389,5 +459,5 @@ A: 可能原因:
|
||||
|
||||
---
|
||||
|
||||
**感謝您使用暫時規範管理系統 V3!**
|
||||
希望這個操作手冊能幫助您更有效地使用系統功能。
|
||||
**感謝您使用暫時規範管理系統 V4!**
|
||||
希望這個操作手冊能幫助您更有效地使用系統功能。如有任何問題,請聯繫系統管理員。
|
13
app.py
13
app.py
@@ -66,6 +66,19 @@ app.register_blueprint(upload_bp)
|
||||
app.register_blueprint(admin_bp)
|
||||
app.register_blueprint(api_bp)
|
||||
|
||||
# 註冊自訂模板 filter
|
||||
from utils.timezone import format_taiwan_time
|
||||
|
||||
@app.template_filter('taiwan_time')
|
||||
def taiwan_time_filter(dt, format_str='%Y-%m-%d %H:%M:%S'):
|
||||
"""將 datetime 轉換為台灣時間格式字符串"""
|
||||
return format_taiwan_time(dt, format_str)
|
||||
|
||||
@app.template_filter('taiwan_date')
|
||||
def taiwan_date_filter(dt):
|
||||
"""將 datetime 轉換為台灣日期格式字符串"""
|
||||
return format_taiwan_time(dt, '%Y-%m-%d')
|
||||
|
||||
# 導入任務
|
||||
from tasks import check_expiring_specs
|
||||
|
||||
|
@@ -1,8 +1,11 @@
|
||||
services:
|
||||
# Redis 快取服務
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: tempspec-redis
|
||||
image: panjit-tempspec:redis
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.redis
|
||||
container_name: panjit-tempspec-redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
volumes:
|
||||
@@ -15,21 +18,33 @@ services:
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
# ONLYOFFICE Document Server
|
||||
# ONLYOFFICE Document Server - 使用輕量化版本
|
||||
onlyoffice:
|
||||
image: onlyoffice/documentserver:8.0
|
||||
container_name: tempspec-onlyoffice
|
||||
image: onlyoffice/documentserver:8.1
|
||||
container_name: panjit-tempspec-onlyoffice
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
JWT_ENABLED: "true"
|
||||
JWT_SECRET: ${ONLYOFFICE_JWT_SECRET:-your_jwt_secret_key_here}
|
||||
JWT_HEADER: "Authorization"
|
||||
JWT_IN_BODY: "true"
|
||||
# 使用內建資料庫,不需要外部 PostgreSQL
|
||||
AMQP_TYPE: "0" # 禁用RabbitMQ以節省資源
|
||||
# 時區設定
|
||||
TZ: Asia/Taipei
|
||||
ports:
|
||||
- "${ONLYOFFICE_PORT:-12011}:80"
|
||||
- "${ONLYOFFICE_PORT:-12015}:80"
|
||||
volumes:
|
||||
- onlyoffice_data:/var/www/onlyoffice/Data
|
||||
- onlyoffice_logs:/var/log/onlyoffice
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 3G
|
||||
cpus: '2.0'
|
||||
reservations:
|
||||
memory: 1.5G
|
||||
cpus: '1.0'
|
||||
networks:
|
||||
- tempspec-network
|
||||
healthcheck:
|
||||
@@ -40,8 +55,11 @@ services:
|
||||
|
||||
# Flask 應用程式
|
||||
app:
|
||||
build: .
|
||||
container_name: tempspec-app
|
||||
image: panjit-tempspec:main
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: panjit-tempspec-app
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Flask 設定
|
||||
@@ -57,14 +75,14 @@ services:
|
||||
# CDN 設定
|
||||
CDN_DOMAIN: ${CDN_DOMAIN:-}
|
||||
|
||||
# LDAP 設定
|
||||
LDAP_SERVER: ${LDAP_SERVER:-ldap://your-dc.company.com}
|
||||
LDAP_PORT: ${LDAP_PORT:-389}
|
||||
LDAP_USE_SSL: ${LDAP_USE_SSL:-False}
|
||||
LDAP_SEARCH_BASE: ${LDAP_SEARCH_BASE:-DC=company,DC=com}
|
||||
LDAP_BIND_USER_DN: ${LDAP_BIND_USER_DN:-CN=service,DC=company,DC=com}
|
||||
LDAP_BIND_USER_PASSWORD: ${LDAP_BIND_USER_PASSWORD:-service_password}
|
||||
LDAP_USER_LOGIN_ATTR: ${LDAP_USER_LOGIN_ATTR:-userPrincipalName}
|
||||
# LDAP 設定 (統一配置)
|
||||
LDAP_SERVER: panjit.com.tw
|
||||
LDAP_PORT: 389
|
||||
LDAP_USE_SSL: false
|
||||
LDAP_SEARCH_BASE: DC=panjit,DC=com,DC=tw
|
||||
LDAP_BIND_USER_DN: CN=LdapBind,CN=Users,DC=PANJIT,DC=COM,DC=TW
|
||||
LDAP_BIND_USER_PASSWORD: panjit2481
|
||||
LDAP_USER_LOGIN_ATTR: userPrincipalName
|
||||
|
||||
# SMTP 郵件設定
|
||||
SMTP_SERVER: ${SMTP_SERVER:-smtp.company.com}
|
||||
@@ -74,14 +92,16 @@ services:
|
||||
SMTP_SENDER_PASSWORD: ${SMTP_SENDER_PASSWORD:-smtp_password}
|
||||
|
||||
# ONLYOFFICE 設定
|
||||
ONLYOFFICE_URL: http://localhost:12011/
|
||||
ONLYOFFICE_URL: http://localhost:12015/
|
||||
ONLYOFFICE_INTERNAL_URL: http://onlyoffice:80
|
||||
ONLYOFFICE_JWT_SECRET: ${ONLYOFFICE_JWT_SECRET:-your_jwt_secret_key_here}
|
||||
|
||||
|
||||
# 時區設定
|
||||
TZ: Asia/Taipei
|
||||
|
||||
# 其他設定
|
||||
UPLOAD_FOLDER: uploads
|
||||
ports:
|
||||
- "${APP_PORT:-12010}:5000"
|
||||
# No external port; only Nginx exposes ports
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./static/generated:/app/static/generated
|
||||
@@ -99,7 +119,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
onlyoffice:
|
||||
condition: service_healthy
|
||||
condition: service_started
|
||||
networks:
|
||||
- tempspec-network
|
||||
healthcheck:
|
||||
@@ -110,12 +130,14 @@ services:
|
||||
|
||||
# Nginx 反向代理 (生產環境自動啟用)
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: tempspec-nginx
|
||||
image: panjit-tempspec:nginx
|
||||
build:
|
||||
context: ./nginx
|
||||
dockerfile: Dockerfile
|
||||
container_name: panjit-tempspec-nginx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${NGINX_PORT:-12013}:80"
|
||||
- "${NGINX_SSL_PORT:-12014}:443"
|
||||
- "12013:80"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import UserMixin
|
||||
from datetime import datetime
|
||||
from utils.timezone import taiwan_now
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
@@ -53,7 +54,7 @@ class SpecHistory(db.Model):
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('ts_user.id', ondelete='SET NULL'), nullable=True)
|
||||
action = db.Column(db.String(50), nullable=False)
|
||||
details = db.Column(db.Text, nullable=True)
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
timestamp = db.Column(db.DateTime, default=taiwan_now)
|
||||
|
||||
# 建立與 User 和 TempSpec 的關聯,方便查詢
|
||||
user = db.relationship('User')
|
||||
|
21
nginx/Dockerfile
Normal file
21
nginx/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
# 移除預設配置
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 複製自定義配置
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY conf.d/ /etc/nginx/conf.d/
|
||||
|
||||
# 創建SSL目錄
|
||||
RUN mkdir -p /etc/nginx/ssl
|
||||
|
||||
# 設置正確的權限
|
||||
RUN chown -R nginx:nginx /etc/nginx && \
|
||||
chmod -R 755 /etc/nginx
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80 443
|
||||
|
||||
# 啟動nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
@@ -8,7 +8,7 @@ server {
|
||||
|
||||
# 應用程式代理
|
||||
location / {
|
||||
proxy_pass http://app:5000;
|
||||
proxy_pass http://panjit-tempspec-app:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@@ -27,7 +27,7 @@ server {
|
||||
|
||||
# ONLYOFFICE Document Server 代理
|
||||
location /onlyoffice/ {
|
||||
proxy_pass http://onlyoffice:80/;
|
||||
proxy_pass http://panjit-tempspec-onlyoffice:80/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@@ -46,14 +46,14 @@ server {
|
||||
|
||||
# 靜態檔案快取
|
||||
location /static/ {
|
||||
proxy_pass http://app:5000/static/;
|
||||
proxy_pass http://panjit-tempspec-app:5000/static/;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 健康檢查
|
||||
location /health {
|
||||
proxy_pass http://app:5000/;
|
||||
proxy_pass http://panjit-tempspec-app:5000/;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
|
@@ -1,21 +1,34 @@
|
||||
flask
|
||||
flask-login
|
||||
flask-sqlalchemy
|
||||
pymysql
|
||||
werkzeug
|
||||
docx2pdf
|
||||
python-docx
|
||||
docxtpl
|
||||
beautifulsoup4
|
||||
lxml
|
||||
python-dotenv
|
||||
mistune
|
||||
PyJWT
|
||||
ldap3
|
||||
Flask-APScheduler
|
||||
Pillow
|
||||
requests
|
||||
cryptography
|
||||
gunicorn
|
||||
redis
|
||||
flask-caching
|
||||
# Flask Framework (統一版本)
|
||||
Flask==3.0.0
|
||||
Flask-Login==0.6.3
|
||||
Flask-SQLAlchemy==3.0.5
|
||||
Flask-Caching==2.1.0
|
||||
Flask-APScheduler==1.13.1
|
||||
|
||||
# Database
|
||||
PyMySQL==1.1.0
|
||||
|
||||
# Web Server
|
||||
Werkzeug==3.0.1
|
||||
gunicorn==21.2.0
|
||||
|
||||
# Authentication
|
||||
PyJWT==2.8.0
|
||||
ldap3==2.9.1
|
||||
|
||||
# Document Processing
|
||||
python-docx==1.1.0
|
||||
docxtpl==0.16.7
|
||||
docx2pdf==0.1.8
|
||||
|
||||
# Utilities
|
||||
beautifulsoup4==4.12.2
|
||||
lxml==4.9.3
|
||||
python-dotenv==1.0.0
|
||||
mistune==3.0.2
|
||||
Pillow==10.1.0
|
||||
requests==2.31.0
|
||||
cryptography==41.0.7
|
||||
|
||||
# Cache & Task Queue
|
||||
redis==5.0.1
|
@@ -3,6 +3,7 @@ from flask_login import login_user, logout_user, login_required, current_user
|
||||
from ldap_utils import authenticate_ldap_user
|
||||
from models import User, db
|
||||
from datetime import datetime
|
||||
from utils.timezone import taiwan_now
|
||||
import logging
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
@@ -69,7 +70,7 @@ def login():
|
||||
current_app.logger.info(f"Existing user found: {user_info['username']}")
|
||||
|
||||
# Update last_login time
|
||||
local_user.last_login = datetime.now()
|
||||
local_user.last_login = taiwan_now()
|
||||
db.session.commit()
|
||||
|
||||
# Step 3: Log in the user with Flask-Login
|
||||
|
@@ -2,6 +2,7 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, send_file, current_app, jsonify, abort
|
||||
from flask_login import login_required, current_user
|
||||
from datetime import datetime, timedelta
|
||||
from utils.timezone import taiwan_now, format_taiwan_time
|
||||
from models import TempSpec, db, Upload, SpecHistory
|
||||
from utils import editor_or_admin_required, add_history_log, admin_required, send_email, process_recipients
|
||||
from ldap_utils import get_ldap_group_members
|
||||
@@ -27,7 +28,7 @@ def _generate_next_spec_code():
|
||||
產生下一個暫時規範編號。
|
||||
規則: PE + 民國年(3碼) + 月份(2碼) + 流水號(2碼)
|
||||
"""
|
||||
now = datetime.now()
|
||||
now = taiwan_now()
|
||||
roc_year = now.year - 1911
|
||||
prefix = f"PE{roc_year}{now.strftime('%m')}"
|
||||
|
||||
@@ -54,7 +55,7 @@ def create_temp_spec():
|
||||
if request.method == 'POST':
|
||||
spec_code = _generate_next_spec_code()
|
||||
form_data = request.form
|
||||
now = datetime.now()
|
||||
now = taiwan_now()
|
||||
|
||||
# 1. 在資料庫中建立紀錄
|
||||
spec = TempSpec(
|
||||
@@ -140,20 +141,25 @@ def edit_spec(spec_id):
|
||||
doc_url = get_file_uri(doc_filename)
|
||||
callback_url = url_for('temp_spec.onlyoffice_callback', spec_id=spec_id, _external=True)
|
||||
|
||||
# 2. 如果是在開發環境,將 URL 中的 localhost 替換為 Docker 可存取的地址
|
||||
# 2. 修正容器間通訊的 URL,使用正確的容器名稱
|
||||
if '127.0.0.1' in doc_url or 'localhost' in doc_url:
|
||||
# 同時修正 doc_url 和 callback_url
|
||||
doc_url = doc_url.replace('127.0.0.1', 'host.docker.internal').replace('localhost', 'host.docker.internal')
|
||||
callback_url = callback_url.replace('127.0.0.1', 'host.docker.internal').replace('localhost', 'host.docker.internal')
|
||||
# 在 Docker Compose 環境中,OnlyOffice 應該透過 nginx 存取 Flask 應用
|
||||
doc_url = doc_url.replace('127.0.0.1:12013', 'panjit-tempspec-nginx:80').replace('localhost:12013', 'panjit-tempspec-nginx:80')
|
||||
doc_url = doc_url.replace('127.0.0.1', 'panjit-tempspec-nginx').replace('localhost', 'panjit-tempspec-nginx')
|
||||
callback_url = callback_url.replace('127.0.0.1:12013', 'panjit-tempspec-nginx:80').replace('localhost:12013', 'panjit-tempspec-nginx:80')
|
||||
callback_url = callback_url.replace('127.0.0.1', 'panjit-tempspec-nginx').replace('localhost', 'panjit-tempspec-nginx')
|
||||
|
||||
# --- END: 修正文件下載與回呼的 URL ---
|
||||
|
||||
oo_secret = current_app.config['ONLYOFFICE_JWT_SECRET']
|
||||
|
||||
# 生成唯一的文件密鑰,包含更新時間戳
|
||||
file_key = f"{spec.id}_{int(os.path.getmtime(doc_physical_path))}"
|
||||
|
||||
payload = {
|
||||
"document": {
|
||||
"fileType": "docx",
|
||||
"key": f"{spec.id}_{int(os.path.getmtime(doc_physical_path))}",
|
||||
"key": file_key,
|
||||
"title": doc_filename,
|
||||
"url": doc_url # <-- 使用修正後的 doc_url
|
||||
},
|
||||
@@ -161,7 +167,14 @@ def edit_spec(spec_id):
|
||||
"editorConfig": {
|
||||
"callbackUrl": callback_url, # <-- 使用修正後的回呼 URL
|
||||
"user": { "id": str(current_user.id), "name": current_user.username },
|
||||
"customization": { "autosave": True, "forcesave": True }
|
||||
"customization": {
|
||||
"autosave": True,
|
||||
"forcesave": True,
|
||||
"chat": False,
|
||||
"comments": True,
|
||||
"help": False
|
||||
},
|
||||
"mode": "edit"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,22 +194,78 @@ def edit_spec(spec_id):
|
||||
@temp_spec_bp.route('/onlyoffice-callback/<int:spec_id>', methods=['POST'])
|
||||
def onlyoffice_callback(spec_id):
|
||||
data = request.json
|
||||
|
||||
if data.get('status') == 2:
|
||||
status = data.get('status')
|
||||
|
||||
# 記錄所有回調狀態以便調試
|
||||
current_app.logger.info(f"OnlyOffice callback for spec {spec_id}: status={status}, data={data}")
|
||||
|
||||
# OnlyOffice 狀態說明:
|
||||
# 0 - 文件未找到
|
||||
# 1 - 文件編輯中
|
||||
# 2 - 文件準備保存
|
||||
# 3 - 文件保存中
|
||||
# 4 - 文件已關閉,無變更
|
||||
# 6 - 文件編輯中,但已強制保存
|
||||
# 7 - 發生錯誤
|
||||
|
||||
if status in [2, 6]: # 文件需要保存或已強制保存
|
||||
try:
|
||||
response = requests.get(data['url'], timeout=10)
|
||||
if 'url' not in data:
|
||||
current_app.logger.error(f"OnlyOffice callback missing URL for spec {spec_id}")
|
||||
return jsonify({"error": 1, "message": "Missing document URL"})
|
||||
|
||||
# 驗證 JWT Token (如果有)
|
||||
token = data.get('token')
|
||||
if token:
|
||||
try:
|
||||
oo_secret = current_app.config['ONLYOFFICE_JWT_SECRET']
|
||||
jwt.decode(token, oo_secret, algorithms=['HS256'])
|
||||
except jwt.InvalidTokenError:
|
||||
current_app.logger.error(f"Invalid JWT token in OnlyOffice callback for spec {spec_id}")
|
||||
return jsonify({"error": 1, "message": "Invalid token"})
|
||||
|
||||
# 修正 OnlyOffice 回調中的 URL 以供容器間通信使用
|
||||
download_url = data['url']
|
||||
# 將外部訪問 URL 轉換為容器間通信 URL
|
||||
download_url = download_url.replace('localhost:12015', 'panjit-tempspec-onlyoffice:80')
|
||||
download_url = download_url.replace('127.0.0.1:12015', 'panjit-tempspec-onlyoffice:80')
|
||||
|
||||
current_app.logger.info(f"Downloading updated document from: {data['url']} -> {download_url}")
|
||||
response = requests.get(download_url, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
# 保存文件
|
||||
spec = TempSpec.query.get_or_404(spec_id)
|
||||
doc_filename = f"{spec.spec_code}.docx"
|
||||
file_path = os.path.join(current_app.static_folder, 'generated', doc_filename)
|
||||
|
||||
# 確保目錄存在
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(response.content)
|
||||
|
||||
current_app.logger.info(f"Successfully saved updated document for spec {spec_id} to {file_path}")
|
||||
|
||||
# 更新資料庫中的修改時間
|
||||
spec.updated_at = taiwan_now()
|
||||
db.session.commit()
|
||||
|
||||
except requests.RequestException as e:
|
||||
current_app.logger.error(f"Failed to download document from OnlyOffice for spec {spec_id}: {e}")
|
||||
return jsonify({"error": 1, "message": f"Download failed: {str(e)}"})
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"ONLYOFFICE callback error for spec {spec_id}: {e}")
|
||||
current_app.logger.error(f"OnlyOffice callback error for spec {spec_id}: {e}")
|
||||
return jsonify({"error": 1, "message": str(e)})
|
||||
|
||||
elif status == 1:
|
||||
current_app.logger.info(f"Document {spec_id} is being edited")
|
||||
elif status == 4:
|
||||
current_app.logger.info(f"Document {spec_id} closed without changes")
|
||||
elif status == 7:
|
||||
current_app.logger.error(f"OnlyOffice error for document {spec_id}")
|
||||
return jsonify({"error": 1, "message": "OnlyOffice reported an error"})
|
||||
|
||||
return jsonify({"error": 0})
|
||||
|
||||
# --- 其他既有路由 ---
|
||||
@@ -251,7 +320,7 @@ def activate_spec(spec_id):
|
||||
flash('您必須上傳一個檔案。', 'danger')
|
||||
return redirect(url_for('temp_spec.activate_spec', spec_id=spec.id))
|
||||
|
||||
filename = secure_filename(f"{spec.spec_code}_signed_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf")
|
||||
filename = secure_filename(f"{spec.spec_code}_signed_{taiwan_now().strftime('%Y%m%d%H%M%S')}.pdf")
|
||||
upload_folder = os.path.join(BASE_DIR, current_app.config['UPLOAD_FOLDER'])
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
file_path = os.path.join(upload_folder, filename)
|
||||
@@ -260,7 +329,7 @@ def activate_spec(spec_id):
|
||||
new_upload = Upload(
|
||||
temp_spec_id=spec.id,
|
||||
filename=filename,
|
||||
upload_time=datetime.now()
|
||||
upload_time=taiwan_now()
|
||||
)
|
||||
db.session.add(new_upload)
|
||||
|
||||
@@ -313,7 +382,7 @@ def terminate_spec(spec_id):
|
||||
|
||||
spec.status = 'terminated'
|
||||
spec.termination_reason = reason
|
||||
spec.end_date = datetime.today().date()
|
||||
spec.end_date = taiwan_now().date()
|
||||
add_history_log(spec.id, '終止', f"原因: {reason}")
|
||||
|
||||
# --- Start of Dynamic Email Notification ---
|
||||
@@ -382,7 +451,12 @@ def download_signed_pdf(spec_id):
|
||||
@editor_or_admin_required
|
||||
def extend_spec(spec_id):
|
||||
spec = TempSpec.query.get_or_404(spec_id)
|
||||
|
||||
|
||||
# 檢查展延次數限制(最多展延2次)
|
||||
if spec.extension_count >= 2:
|
||||
flash('此暫時規範已達展延次數上限(2次),無法再次展延。總效期已達90天上限。', 'danger')
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
||||
if request.method == 'POST':
|
||||
new_end_date_str = request.form.get('new_end_date')
|
||||
uploaded_file = request.files.get('new_file')
|
||||
@@ -399,7 +473,7 @@ def extend_spec(spec_id):
|
||||
spec.extension_count += 1
|
||||
spec.status = 'active'
|
||||
|
||||
filename = secure_filename(f"{spec.spec_code}_extension_{spec.extension_count}_{datetime.now().strftime('%Y%m%d')}.pdf")
|
||||
filename = secure_filename(f"{spec.spec_code}_extension_{spec.extension_count}_{taiwan_now().strftime('%Y%m%d')}.pdf")
|
||||
upload_folder = os.path.join(BASE_DIR, current_app.config['UPLOAD_FOLDER'])
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
file_path = os.path.join(upload_folder, filename)
|
||||
@@ -408,7 +482,7 @@ def extend_spec(spec_id):
|
||||
new_upload = Upload(
|
||||
temp_spec_id=spec.id,
|
||||
filename=filename,
|
||||
upload_time=datetime.now()
|
||||
upload_time=taiwan_now()
|
||||
)
|
||||
db.session.add(new_upload)
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,12 +12,18 @@
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<p><strong>主題:</strong> {{ spec.title }}</p>
|
||||
<p><strong>原結束日期:</strong> {{ spec.end_date.strftime('%Y-%m-%d') }}</p>
|
||||
|
||||
<p><strong>原結束日期:</strong> {{ spec.end_date|taiwan_date }}</p>
|
||||
<p><strong>展延次數:</strong>
|
||||
<span class="badge bg-info">{{ spec.extension_count }} / 2</span>
|
||||
<small class="text-muted ms-2">
|
||||
剩餘可展延次數: <strong>{{ 2 - spec.extension_count }}</strong> 次
|
||||
</small>
|
||||
</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="new_end_date" class="form-label"><strong>新的結束日期</strong></label>
|
||||
<input type="date" class="form-control" id="new_end_date" name="new_end_date"
|
||||
value="{{ default_new_end_date.strftime('%Y-%m-%d') }}" required>
|
||||
<input type="date" class="form-control" id="new_end_date" name="new_end_date"
|
||||
value="{{ default_new_end_date|taiwan_date }}" required>
|
||||
<div class="form-text">預設為原結束日期後一個月。</div>
|
||||
</div>
|
||||
|
||||
|
@@ -21,7 +21,7 @@
|
||||
<span class="badge bg-primary rounded-pill me-2">{{ entry.action }}</span>
|
||||
由 <strong>{{ entry.user.username if entry.user else '[已刪除的使用者]' }}</strong> 執行
|
||||
</h5>
|
||||
<small>{{ entry.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||
<small>{{ entry.timestamp|taiwan_time }}</small>
|
||||
</div>
|
||||
<p class="mb-1 mt-2">{{ entry.details }}</p>
|
||||
</li>
|
||||
|
@@ -47,7 +47,7 @@
|
||||
<th>建立日期</th>
|
||||
<th>結束日期</th>
|
||||
<th class="text-center">剩餘天數</th>
|
||||
<th>狀態</th>
|
||||
<th class="text-center">狀態</th>
|
||||
<th class="text-center">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -57,8 +57,8 @@
|
||||
<td>{{ spec.spec_code }}</td>
|
||||
<td>{{ spec.title }}</td>
|
||||
<td>{{ spec.applicant }}</td>
|
||||
<td>{{ spec.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ spec.end_date.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ spec.created_at|taiwan_date }}</td>
|
||||
<td>{{ spec.end_date|taiwan_date }}</td>
|
||||
|
||||
<td class="text-center">
|
||||
{% if spec.status in ['active', 'expired'] %}
|
||||
@@ -80,7 +80,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td class="text-center">
|
||||
{% if spec.status == 'active' %}
|
||||
<span class="badge fs-6 bg-success bg-opacity-75"><i class="bi bi-check-circle-fill me-1"></i>已生效</span>
|
||||
{% elif spec.status == 'pending_approval' %}
|
||||
@@ -90,6 +90,18 @@
|
||||
{% else %}
|
||||
<span class="badge fs-6 bg-secondary bg-opacity-75"><i class="bi bi-calendar-x-fill me-1"></i>已過期</span>
|
||||
{% endif %}
|
||||
|
||||
{% if spec.extension_count > 0 %}
|
||||
<br>
|
||||
<div class="mt-1">
|
||||
<span class="badge bg-light text-dark border">
|
||||
<i class="bi bi-arrow-repeat me-1"></i>已展延 {{ spec.extension_count }} 次
|
||||
</span>
|
||||
{% if spec.extension_count >= 2 %}
|
||||
<br><span class="badge bg-danger mt-1">達到上限</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
@@ -102,7 +114,11 @@
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.role in ['editor', 'admin'] and spec.status == 'active' %}
|
||||
<a href="{{ url_for('temp_spec.extend_spec', spec_id=spec.id) }}" class="btn btn-sm btn-secondary" title="展延"><i class="bi bi-calendar-plus"></i></a>
|
||||
{% if spec.extension_count < 2 %}
|
||||
<a href="{{ url_for('temp_spec.extend_spec', spec_id=spec.id) }}" class="btn btn-sm btn-secondary" title="展延"><i class="bi bi-calendar-plus"></i></a>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-secondary" disabled title="已達展延次數上限(2次)"><i class="bi bi-calendar-x"></i></button>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('temp_spec.terminate_spec', spec_id=spec.id) }}" class="btn btn-sm btn-danger" title="終止"><i class="bi bi-x-circle"></i></a>
|
||||
{% endif %}
|
||||
|
||||
|
@@ -84,7 +84,7 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if user.last_login %}
|
||||
{{ user.last_login.strftime('%Y-%m-%d %H:%M') }}
|
||||
{{ user.last_login|taiwan_time('%Y-%m-%d %H:%M') }}
|
||||
{% else %}
|
||||
<span class="text-muted">從未登入</span>
|
||||
{% endif %}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
# utils 模組初始化
|
||||
from docxtpl import DocxTemplate, InlineImage
|
||||
from docx.shared import Mm
|
||||
from docx2pdf import convert
|
||||
@@ -17,7 +18,7 @@ except ImportError:
|
||||
import mistune
|
||||
from PIL import Image
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 向上一級到專案根目錄
|
||||
|
||||
def _resolve_image_path(src: str) -> str:
|
||||
"""
|
||||
@@ -102,10 +103,6 @@ def _process_markdown_sections(doc, md_content):
|
||||
results.append({'text': text, 'image': None})
|
||||
return results
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def fill_template(values, template_path, output_word_path, output_pdf_path):
|
||||
from docxtpl import DocxTemplate
|
||||
from docx2pdf import convert
|
||||
@@ -135,9 +132,6 @@ def fill_template(values, template_path, output_word_path, output_pdf_path):
|
||||
if PYTHONCOM_AVAILABLE:
|
||||
pythoncom.CoUninitialize()
|
||||
|
||||
|
||||
|
||||
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
@@ -157,7 +151,7 @@ def editor_or_admin_required(f):
|
||||
def add_history_log(spec_id, action, details=""):
|
||||
"""新增一筆操作歷史紀錄"""
|
||||
from models import db, SpecHistory
|
||||
|
||||
|
||||
history_entry = SpecHistory(
|
||||
spec_id=spec_id,
|
||||
user_id=current_user.id,
|
||||
@@ -166,7 +160,6 @@ def add_history_log(spec_id, action, details=""):
|
||||
)
|
||||
db.session.add(history_entry)
|
||||
|
||||
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.header import Header
|
||||
@@ -179,41 +172,41 @@ def process_recipients(recipients_str):
|
||||
返回: 展開後的郵件地址列表
|
||||
"""
|
||||
print(f"[RECIPIENTS DEBUG] 開始處理收件者: {recipients_str}")
|
||||
|
||||
|
||||
if not recipients_str:
|
||||
print(f"[RECIPIENTS DEBUG] 收件者字串為空")
|
||||
return []
|
||||
|
||||
|
||||
recipients = [item.strip() for item in recipients_str.split(',') if item.strip()]
|
||||
final_emails = []
|
||||
|
||||
|
||||
for recipient in recipients:
|
||||
print(f"[RECIPIENTS DEBUG] 處理收件者項目: {recipient}")
|
||||
|
||||
|
||||
if recipient.startswith('group:'):
|
||||
# 這是一個群組,需要展開
|
||||
group_name = recipient[6:] # 移除 'group:' 前綴
|
||||
print(f"[RECIPIENTS DEBUG] 發現群組: {group_name}")
|
||||
|
||||
|
||||
try:
|
||||
from ldap_utils import get_ldap_group_members
|
||||
group_emails = get_ldap_group_members(group_name)
|
||||
print(f"[RECIPIENTS DEBUG] 群組 {group_name} 包含 {len(group_emails)} 個成員")
|
||||
|
||||
|
||||
for email in group_emails:
|
||||
if email and email not in final_emails:
|
||||
final_emails.append(email)
|
||||
print(f"[RECIPIENTS DEBUG] 添加群組成員郵件: {email}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"[RECIPIENTS ERROR] 群組 {group_name} 展開失敗: {e}")
|
||||
|
||||
|
||||
else:
|
||||
# 這是個人郵件地址
|
||||
if recipient and recipient not in final_emails:
|
||||
final_emails.append(recipient)
|
||||
print(f"[RECIPIENTS DEBUG] 添加個人郵件: {recipient}")
|
||||
|
||||
|
||||
print(f"[RECIPIENTS DEBUG] 最終收件者列表 ({len(final_emails)} 個): {final_emails}")
|
||||
return final_emails
|
||||
|
||||
@@ -226,7 +219,7 @@ def send_email(to_addrs, subject, body):
|
||||
print(f"[EMAIL DEBUG] 收件者數量: {len(to_addrs)}")
|
||||
print(f"[EMAIL DEBUG] 收件者: {to_addrs}")
|
||||
print(f"[EMAIL DEBUG] 主旨: {subject}")
|
||||
|
||||
|
||||
try:
|
||||
# 取得 SMTP 設定
|
||||
smtp_server = current_app.config['SMTP_SERVER']
|
||||
@@ -236,7 +229,7 @@ def send_email(to_addrs, subject, body):
|
||||
sender_email = current_app.config['SMTP_SENDER_EMAIL']
|
||||
sender_password = current_app.config.get('SMTP_SENDER_PASSWORD', '')
|
||||
auth_required = current_app.config.get('SMTP_AUTH_REQUIRED', False)
|
||||
|
||||
|
||||
print(f"[EMAIL DEBUG] SMTP 設定:")
|
||||
print(f"[EMAIL DEBUG] - 伺服器: {smtp_server}:{smtp_port}")
|
||||
print(f"[EMAIL DEBUG] - 使用 TLS: {use_tls}")
|
||||
@@ -262,14 +255,14 @@ def send_email(to_addrs, subject, body):
|
||||
# Port 25 或 587 使用一般連接
|
||||
print(f"[EMAIL DEBUG] 連接 SMTP 伺服器 {smtp_server}:{smtp_port}...")
|
||||
server = smtplib.SMTP(smtp_server, smtp_port)
|
||||
|
||||
|
||||
print(f"[EMAIL DEBUG] SMTP 伺服器連接成功")
|
||||
|
||||
|
||||
if use_tls and smtp_port == 587:
|
||||
print(f"[EMAIL DEBUG] 啟用 TLS...")
|
||||
server.starttls()
|
||||
print(f"[EMAIL DEBUG] TLS 啟用成功")
|
||||
|
||||
|
||||
# 只在需要認證時才登入
|
||||
if auth_required and sender_password:
|
||||
print(f"[EMAIL DEBUG] 登入 SMTP 伺服器...")
|
||||
@@ -277,17 +270,17 @@ def send_email(to_addrs, subject, body):
|
||||
print(f"[EMAIL DEBUG] SMTP 登入成功")
|
||||
else:
|
||||
print(f"[EMAIL DEBUG] 使用匿名發送(Port 25 無需認證)")
|
||||
|
||||
|
||||
# 發送郵件
|
||||
print(f"[EMAIL DEBUG] 發送郵件...")
|
||||
result = server.sendmail(sender_email, to_addrs, msg.as_string())
|
||||
print(f"[EMAIL DEBUG] 郵件發送結果: {result}")
|
||||
|
||||
|
||||
server.quit()
|
||||
print(f"[EMAIL DEBUG] SMTP 連接已關閉")
|
||||
print(f"[EMAIL SUCCESS] 郵件成功發送至: {', '.join(to_addrs)}")
|
||||
return True
|
||||
|
||||
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
print(f"[EMAIL ERROR] SMTP 認證失敗: {e}")
|
||||
print(f"[EMAIL ERROR] 請檢查寄件者帳號和密碼設定")
|
||||
@@ -305,4 +298,4 @@ def send_email(to_addrs, subject, body):
|
||||
import traceback
|
||||
print(f"[EMAIL ERROR] 詳細錯誤:")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
return False
|
102
utils/timezone.py
Normal file
102
utils/timezone.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
台灣時區處理工具模組
|
||||
|
||||
提供一致的時區處理函數,確保系統中所有時間都使用台灣時區(GMT+8)
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
# 台灣時區 (GMT+8)
|
||||
TAIWAN_TZ = timezone(timedelta(hours=8))
|
||||
|
||||
def now_taiwan() -> datetime:
|
||||
"""取得當前台灣時間"""
|
||||
return datetime.now(TAIWAN_TZ)
|
||||
|
||||
def now_utc() -> datetime:
|
||||
"""取得當前 UTC 時間(保留 timezone aware)"""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
def to_taiwan_time(dt) -> datetime:
|
||||
"""將 datetime 轉換為台灣時間
|
||||
|
||||
Args:
|
||||
dt: datetime 物件(可能是 naive 或 aware)或 date 物件
|
||||
|
||||
Returns:
|
||||
台灣時區的 datetime 物件
|
||||
"""
|
||||
if dt is None:
|
||||
return None
|
||||
|
||||
# 如果是 date 物件,轉換為 datetime
|
||||
from datetime import date
|
||||
if isinstance(dt, date) and not isinstance(dt, datetime):
|
||||
# 這是 date 物件,轉換為 datetime(午夜時間)
|
||||
dt = datetime.combine(dt, datetime.min.time())
|
||||
|
||||
# 如果是 naive datetime,假設為 UTC
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
|
||||
# 轉換為台灣時間
|
||||
return dt.astimezone(TAIWAN_TZ)
|
||||
|
||||
def to_utc_time(dt: datetime) -> datetime:
|
||||
"""將 datetime 轉換為 UTC 時間
|
||||
|
||||
Args:
|
||||
dt: datetime 物件(可能是 naive 或 aware)
|
||||
|
||||
Returns:
|
||||
UTC 時區的 datetime 物件
|
||||
"""
|
||||
if dt is None:
|
||||
return None
|
||||
|
||||
# 如果是 naive datetime,假設為台灣時間
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=TAIWAN_TZ)
|
||||
|
||||
return dt.astimezone(timezone.utc)
|
||||
|
||||
def format_taiwan_time(dt: datetime, format_str: str = "%Y-%m-%d %H:%M:%S") -> str:
|
||||
"""將 datetime 格式化為台灣時間字符串
|
||||
|
||||
Args:
|
||||
dt: datetime 物件
|
||||
format_str: 格式化字符串
|
||||
|
||||
Returns:
|
||||
格式化後的台灣時間字符串
|
||||
"""
|
||||
if dt is None:
|
||||
return ""
|
||||
|
||||
taiwan_time = to_taiwan_time(dt)
|
||||
return taiwan_time.strftime(format_str)
|
||||
|
||||
def parse_taiwan_time(time_str: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> datetime:
|
||||
"""解析台灣時間字符串為 datetime
|
||||
|
||||
Args:
|
||||
time_str: 時間字符串
|
||||
format_str: 解析格式
|
||||
|
||||
Returns:
|
||||
台灣時區的 datetime 物件
|
||||
"""
|
||||
naive_dt = datetime.strptime(time_str, format_str)
|
||||
return naive_dt.replace(tzinfo=TAIWAN_TZ)
|
||||
|
||||
# 為了向後兼容,提供替代 datetime.utcnow() 的函數
|
||||
def utcnow() -> datetime:
|
||||
"""取得當前 UTC 時間(替代 datetime.utcnow())
|
||||
|
||||
注意:新代碼建議使用 now_taiwan() 或 now_utc()
|
||||
"""
|
||||
return now_utc().replace(tzinfo=None) # 返回 naive UTC datetime 以保持兼容性
|
||||
|
||||
def taiwan_now() -> datetime:
|
||||
"""取得當前台灣時間(naive datetime,用於存儲到資料庫)"""
|
||||
return now_taiwan().replace(tzinfo=None)
|
Reference in New Issue
Block a user