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