diff --git a/.gitignore b/.gitignore index a3d9b53..4521bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 4696a11..1bd1056 100644 --- a/DEPLOYMENT.md +++ b/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 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 \ No newline at end of file +- [ ] 所有容器運行正常 (`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 部署完成!** + +系統已具備完整的台灣時區支援、展延限制控制、優化的文件同步機制以及增強的用戶體驗。 \ No newline at end of file diff --git a/Dockerfile.redis b/Dockerfile.redis new file mode 100644 index 0000000..8db1579 --- /dev/null +++ b/Dockerfile.redis @@ -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"] \ No newline at end of file diff --git a/README.md b/README.md index ca20466..b95bdb6 100644 --- a/README.md +++ b/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 -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** - 讓企業文件管理更智慧、更高效! \ No newline at end of file +**暫時規範管理系統 V4** - 讓企業文件管理更智慧、更高效! \ No newline at end of file diff --git a/USER_MANUAL.md b/USER_MANUAL.md index c0c1692..9ed3711 100644 --- a/USER_MANUAL.md +++ b/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!** -希望這個操作手冊能幫助您更有效地使用系統功能。 \ No newline at end of file +**感謝您使用暫時規範管理系統 V4!** +希望這個操作手冊能幫助您更有效地使用系統功能。如有任何問題,請聯繫系統管理員。 \ No newline at end of file diff --git a/app.py b/app.py index e220ca0..d46d255 100644 --- a/app.py +++ b/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 diff --git a/docker-compose.yml b/docker-compose.yml index 42af0aa..647766f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/models.py b/models.py index 57f1c70..eb67221 100644 --- a/models.py +++ b/models.py @@ -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') diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..8465ef9 --- /dev/null +++ b/nginx/Dockerfile @@ -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;"] \ No newline at end of file diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf index 30782f4..ce1f05e 100644 --- a/nginx/conf.d/default.conf +++ b/nginx/conf.d/default.conf @@ -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; } diff --git a/requirements.txt b/requirements.txt index 099bf19..3608d27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file +# 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 \ No newline at end of file diff --git a/routes/auth.py b/routes/auth.py index dfc81f1..e8f5518 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -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 diff --git a/routes/temp_spec.py b/routes/temp_spec.py index cfe8990..394ad51 100644 --- a/routes/temp_spec.py +++ b/routes/temp_spec.py @@ -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/', 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) diff --git a/static/generated/PE1140901.docx b/static/generated/PE1140901.docx index 84cab68..510aa7c 100644 Binary files a/static/generated/PE1140901.docx and b/static/generated/PE1140901.docx differ diff --git a/static/generated/PE1140902.docx b/static/generated/PE1140902.docx deleted file mode 100644 index 46f4015..0000000 Binary files a/static/generated/PE1140902.docx and /dev/null differ diff --git a/static/generated/PE1140903.docx b/static/generated/PE1140903.docx deleted file mode 100644 index 16ba89c..0000000 Binary files a/static/generated/PE1140903.docx and /dev/null differ diff --git a/templates/extend_spec.html b/templates/extend_spec.html index 8e04476..5728e47 100644 --- a/templates/extend_spec.html +++ b/templates/extend_spec.html @@ -12,12 +12,18 @@

主題: {{ spec.title }}

-

原結束日期: {{ spec.end_date.strftime('%Y-%m-%d') }}

- +

原結束日期: {{ spec.end_date|taiwan_date }}

+

展延次數: + {{ spec.extension_count }} / 2 + + 剩餘可展延次數: {{ 2 - spec.extension_count }} 次 + +

+
- +
預設為原結束日期後一個月。
diff --git a/templates/spec_history.html b/templates/spec_history.html index b37ccd6..d53266b 100644 --- a/templates/spec_history.html +++ b/templates/spec_history.html @@ -21,7 +21,7 @@ {{ entry.action }}{{ entry.user.username if entry.user else '[已刪除的使用者]' }} 執行 - {{ entry.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} + {{ entry.timestamp|taiwan_time }}

{{ entry.details }}

diff --git a/templates/spec_list.html b/templates/spec_list.html index 10e9d92..dfe0c80 100644 --- a/templates/spec_list.html +++ b/templates/spec_list.html @@ -47,7 +47,7 @@ 建立日期 結束日期 剩餘天數 - 狀態 + 狀態 操作 @@ -57,8 +57,8 @@ {{ spec.spec_code }} {{ spec.title }} {{ spec.applicant }} - {{ spec.created_at.strftime('%Y-%m-%d') }} - {{ spec.end_date.strftime('%Y-%m-%d') }} + {{ spec.created_at|taiwan_date }} + {{ spec.end_date|taiwan_date }} {% if spec.status in ['active', 'expired'] %} @@ -80,7 +80,7 @@ {% endif %} - + {% if spec.status == 'active' %} 已生效 {% elif spec.status == 'pending_approval' %} @@ -90,6 +90,18 @@ {% else %} 已過期 {% endif %} + + {% if spec.extension_count > 0 %} +
+
+ + 已展延 {{ spec.extension_count }} 次 + + {% if spec.extension_count >= 2 %} +
達到上限 + {% endif %} +
+ {% endif %} @@ -102,7 +114,11 @@ {% endif %} {% if current_user.role in ['editor', 'admin'] and spec.status == 'active' %} - + {% if spec.extension_count < 2 %} + + {% else %} + + {% endif %} {% endif %} diff --git a/templates/user_management.html b/templates/user_management.html index 78a032c..f129d8b 100644 --- a/templates/user_management.html +++ b/templates/user_management.html @@ -84,7 +84,7 @@ {% if user.last_login %} - {{ user.last_login.strftime('%Y-%m-%d %H:%M') }} + {{ user.last_login|taiwan_time('%Y-%m-%d %H:%M') }} {% else %} 從未登入 {% endif %} diff --git a/utils.py b/utils/__init__.py similarity index 97% rename from utils.py rename to utils/__init__.py index fd6c234..6980099 100644 --- a/utils.py +++ b/utils/__init__.py @@ -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 \ No newline at end of file diff --git a/utils/timezone.py b/utils/timezone.py new file mode 100644 index 0000000..b9b6548 --- /dev/null +++ b/utils/timezone.py @@ -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) \ No newline at end of file