fix timezone bug

This commit is contained in:
beabigegg
2025-09-21 11:37:39 +08:00
parent a408ce402d
commit 2a0b29402f
22 changed files with 1050 additions and 519 deletions

3
.gitignore vendored
View File

@@ -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

View File

@@ -1,25 +1,28 @@
# 部署指南 - 暫時規範管理系統 V4
本文件提供詳細的部署指導,涵蓋 Docker 環境部署方式。
本文件提供詳細的部署指導,涵蓋 Docker 環境部署方式及 V4 版本的新特性配置
## 🎉 生產環境優化完成
## 🎉 V4.0 版本優化完成
**✅ 已完成50人併發生產環境優化**
- Gunicorn WSGI部署多進程併發
**✅ 已完成企業級生產環境優化**
- 台灣時區 (GMT+8) 完整支援
- 展延次數限制功能最多2次90天上限
- OnlyOffice 文件同步問題修正
- Redis 快取系統(提升效能)
- Nginx 反向代理(負載均衡)
- CDN支援靜態資源加速
- 資源限制(防止系統過載)
- 監控工具(效能監控)
- 容器間網路優化
- 時區處理模組化
- UI 樣式改進
## 📋 目錄
1. [系統需求](#1-系統需求)
2. [快速部署](#2-快速部署)
3. [生產環境配置](#3-生產環境配置)
4. [監控與管理](#4-監控與管理)
5. [服務訪問](#5-服務訪問)
6. [疑難排解](#6-疑難排解)
4. [V4.0 新功能配置](#4-v40-新功能配置)
5. [監控與管理](#5-監控與管理)
6. [服務訪問](#6-服務訪問)
7. [疑難排解](#7-疑難排解)
## 1. 系統需求
@@ -28,18 +31,25 @@
- [ ] Docker 20.10+ 已安裝且運行中
- [ ] Docker Compose 2.0+ 已安裝
- [ ] 外部 MySQL 資料庫可訪問 (mysql.theaken.com:33306)
- [ ] LDAP/Active Directory 伺服器可連線
- [ ] SMTP 郵件伺服器已配置
- [ ] LDAP/Active Directory 伺服器可連線 (panjit.com.tw)
- [ ] SMTP 郵件伺服器已配置 (mail.panjit.com.tw)
- [ ] 足夠的磁碟空間 (建議至少 10GB)
### 端口需求
確保以下端口未被占用:
- `12010`: Flask 應用程式(Gunicorn
- `12011`: OnlyOffice 文檔服務
- `12013`: Nginx HTTP反向代理
- `12014`: Nginx HTTPS反向代理
- `6379`: Redis 快取(內部)
- `12010`: Flask 應用程式(內部,可選直接訪問
- `12013`: Nginx HTTP反向代理主要入口
- `12015`: OnlyOffice 文檔服務
- `6379`: Redis 快取(容器內部
### V4.0 網路架構
```
用戶 → Nginx (12013) → Flask App (5000) → MySQL (外部)
↓ ↓
Redis (6379) OnlyOffice (80)
```
## 2. 快速部署
@@ -50,12 +60,12 @@
git clone <repository-url>
cd TEMP_spec_system_V4
# 2. 配置環境變數
cp .env.production .env
# 編輯 .env 文件,填入實際的配置值
# 2. 配置環境變數(使用預設配置)
cp .env.example .env
# 編輯 .env 文件,大部分配置已預設好
# 3. 啟動所有服務(生產環境優化版本)
docker-compose up -d
# 3. 啟動所有服務(V4.0優化版本)
docker-compose up -d --build
# 4. 檢查服務狀態
docker-compose ps
@@ -64,10 +74,10 @@ 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
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
外部用戶 → Nginx (12013) → Flask App (Gunicorn) → 外部MySQL
↓ ↓
Redis快取 OnlyOffice (12015)
Redis快取
外部MySQL資料庫
台灣時區處理模組
```
**服務組件**
- **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 服務入口
服務啟動後可透過以下 URL 訪問
**主要服務**
- **主應用程式 (Nginx)**: http://localhost:12013/login 🌟 **推薦**
- **主應用程式 (直接)**: http://localhost:12010/login
- **OnlyOffice 服務**: http://localhost:12011
**推薦使用方式(生產環境)**
- 使用 Nginx 反向代理: `http://localhost:12013`
- 直接訪問 Flask: `http://localhost:12010`
### 5.2 登入資訊
- **認證方式**: LDAP/Active Directory
- **登入帳號**: 使用公司 LDAP 帳號密碼
- **登入格式**: 支援 `username@panjit.com.tw` `username`
### 5.3 預設管理員帳號
如需創建本地管理員帳號非LDAP
### 5.1 系統監控
```bash
# 進入應用容器
docker-compose exec app python update_admin.py
# V4.0 系統監控(如有 monitor.py
python monitor.py --watch 5
# 或手動創建
docker-compose exec app python -c "
from models import db, User
from werkzeug.security import generate_password_hash
# 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():
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')
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 🌟 **推薦**
- **OnlyOffice 服務**: http://localhost:12015
**V4.0 登入資訊**
- **認證方式**: LDAP/Active Directory
- **登入格式**: `username@panjit.com.tw`
- **時區顯示**: 所有時間使用台灣時區 (GMT+8)
### 6.2 V4.0 新功能驗證
登入後驗證以下新功能
1. **時區顯示**: 檢查所有時間是否使用台灣時區
2. **展延限制**: 查看已展延2次的規範是否正確顯示限制
3. **文件同步**: 測試 OnlyOffice 編輯和儲存功能
4. **UI 改進**: 檢查展延狀態在深色背景下的可讀性
## 7. 疑難排解
### 7.1 V4.0 特定問題
**時區顯示錯誤**
```bash
# 檢查容器時區設定
docker exec panjit-tempspec-app date
docker exec panjit-tempspec-app python -c "
import os
print('TZ:', os.environ.get('TZ', 'Not set'))
"
# 重新啟動應用容器
docker-compose restart app
```
**展延限制未生效**
```bash
# 檢查展延邏輯
docker-compose logs app | grep -i extension
# 測試展延限制邏輯
docker exec panjit-tempspec-app python -c "
from routes.temp_spec import check_extension_limit
print('Extension limit logic available')
"
```
## 6. 疑難排解
### 6.1 生產環境常見問題
**Redis 連接失敗**
**OnlyOffice 文件同步問題**
```bash
# 檢查Redis容器狀態
docker-compose logs redis
# 檢查 OnlyOffice 回調
docker-compose logs app | grep "OnlyOffice callback"
# 測試Redis連接
docker-compose exec redis redis-cli ping
# 檢查容器間網路
docker exec panjit-tempspec-onlyoffice ping panjit-tempspec-nginx
# 檢查 OnlyOffice 健康狀態
curl -I http://localhost:12015/healthcheck
```
**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
# 檢查Gunicorn日誌
docker-compose logs app
# 檢查網路配置
docker network ls | grep tempspec
docker network inspect tempspec-network
# 檢查容器資源
docker stats tempspec-app
# 重啟應用
docker-compose restart app
# 測試容器間連接
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
# 檢查快取命中率
python monitor.py
# 檢查端口占用
netstat -tulpn | grep -E "(12013|12015)"
# 檢查Gunicorn worker狀態
docker-compose exec app ps aux | grep gunicorn
# 調整worker數量編輯gunicorn.conf.py
# 修改端口配置(編輯 docker-compose.yml
# 將衝突端口改為其他可用端口
```
### 6.2 基本故障排除
### 7.3 資料庫連接問題
**容器無法啟動**
**MySQL 連接測試**
```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 = 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}')
print(f'Database connection failed: {e}')
"
```
**端口衝突**
修改 `.env` 文件中的端口設定
```env
APP_PORT=12015 # 改為其他可用端口
ONLYOFFICE_PORT=12016 # 改為其他可用端口
NGINX_PORT=12017 # 改為其他可用端口
```
### 6.3 維護命令
### 7.4 V4.0 維護命令
```bash
# 完全重建服務(清除快取)
# 完全重建 V4.0 服務
docker-compose down
docker-compose build --no-cache
docker-compose up -d
@@ -339,33 +436,47 @@ docker-compose up -d
# 更新單一服務
docker-compose up -d --force-recreate app
# 清理未使用的 Docker 資源
# 清理 Docker 資源
docker system prune -a
# 備份重要資料
docker exec panjit-tempspec-app tar -czf /tmp/uploads_backup.tar.gz uploads/
docker cp panjit-tempspec-app:/tmp/uploads_backup.tar.gz ./uploads_backup.tar.gz
```
### 6.4 效能調優
### 7.5 V4.0 效能監控
**Redis 優化**
```bash
# 調整 Redis 記憶體限制(編輯 docker-compose.yml
# 預設: 256MB可根據需要調整
# 監控容器資源使用
docker stats panjit-tempspec-app panjit-tempspec-redis panjit-tempspec-nginx
# 監控 Redis 使用
docker-compose exec redis redis-cli info memory
```
# 檢查應用效能
curl -w "@curl-format.txt" -o /dev/null -s http://localhost:12013/login
**Gunicorn 調優**
```bash
# 編輯 gunicorn.conf.py 調整:
# - workers: worker 進程數量
# - timeout: 請求超時時間
# - max_requests: worker 重啟頻率
# Redis 效能監控
docker-compose exec redis redis-cli info stats | grep -E "(hits|misses|ops)"
```
---
**🎉 生產環境部署完成系統已準備好支援50人的併發使用。**
## 🎉 V4.0 部署完成檢查清單
**快速啟動**: `docker-compose up -d`
**系統監控**: `python monitor.py --watch 5`
**服務訪問**: http://localhost:12013/login
- [ ] 所有容器運行正常 (`docker-compose ps`)
- [ ] 時區顯示為台灣時區 (GMT+8)
- [ ] 展延限制功能正常 (最多2次)
- [ ] OnlyOffice 文件同步正常
- [ ] Redis 快取服務運行
- [ ] Nginx 反向代理正常
- [ ] LDAP 認證功能正常
- [ ] 郵件通知功能正常
- [ ] UI 樣式顯示正確
**快速啟動**: `docker-compose up -d --build`
**服務訪問**: http://localhost:12013
**健康檢查**: `docker-compose ps && docker-compose logs --tail=10`
---
**🚀 暫時規範管理系統 V4.0 部署完成**
系統已具備完整的台灣時區支援展延限制控制優化的文件同步機制以及增強的用戶體驗

17
Dockerfile.redis Normal file
View 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
View File

@@ -8,7 +8,8 @@
- **ONLYOFFICE 線上編輯**:即時協作文件編輯功能
- **智慧通知系統**:動態收件人選擇與自動提醒
- **文件生命週期管理**:完整的建立、啟用、展延、終止流程
- **多平台支援**支援 Windows/Linux 環境部署
- **時區完整支援**台灣時區 (GMT+8) 全系統支援
- **展延次數控制**最多2次展延總效期限制90天
- **Docker 容器化**:一鍵部署環境
## 📋 功能模組
@@ -18,6 +19,12 @@
- **權限控制**:三級權限管理 (Viewer/Editor/Admin)
- **歷史追蹤**:完整的操作記錄與版本控制
- **檔案上傳**:支援多種格式的佐證文件上傳
- **展延限制**最多展延2次總效期上限90天防止無限延期
### 權限說明
- **Viewer檢視者**檢視所有規範、下載PDF檔案、檢視歷史紀錄
- **Editor編輯者**:建立新規範、編輯內容、展延/終止規範、下載檔案
- **Admin管理員**所有Editor權限 + 啟用規範 + 刪除規範
### 智慧通知系統
- **動態收件人選擇**整合LDAP的即時用戶搜尋
@@ -34,13 +41,15 @@
## 🏗️ 系統架構
```
暫時規範系統 V3
暫時規範系統 V4
├── 前端介面 (Flask + Bootstrap 5)
├── 後端邏輯 (Python Flask)
├── 資料庫 (MySQL/SQLite)
├── 資料庫 (MySQL 外部)
├── LDAP整合 (Active Directory)
├── 文件引擎 (ONLYOFFICE)
├── 快取服務 (Redis)
├── 排程服務 (APScheduler)
├── 反向代理 (Nginx)
└── 郵件系統 (SMTP)
```
@@ -52,15 +61,16 @@
- **文件處理**python-docx, docx2pdf
- **認證系統**Flask-Login + LDAP3
- **排程系統**Flask-APScheduler
- **快取服務**Redis
- **反向代理**Nginx
- **容器化**Docker + Docker Compose
## 📦 安裝部署
### 前置需求
- Python 3.8+
- MySQL 8.0+ 或 SQLite
- ONLYOFFICE Document Server
- Docker & Docker Compose
- 外部 MySQL 8.0+ 資料庫
- LDAP/Active Directory 伺服器
- SMTP 郵件伺服器
@@ -69,7 +79,7 @@
1. **克隆專案**
```bash
git clone <repository-url>
cd TEMP_spec_system_V3
cd TEMP_spec_system_V4
```
2. **設定環境變數**
@@ -80,7 +90,17 @@ cp .env.example .env
3. **使用Docker Compose啟動**
```bash
docker-compose up -d
# 建置並啟動所有服務(強制重建以確保使用最新代碼)
docker-compose up -d --build
# 檢查服務狀態
docker-compose ps
# 停止服務
docker-compose down
# 查看日誌
docker-compose logs -f
```
4. **初始化資料庫**
@@ -88,95 +108,16 @@ docker-compose up -d
docker-compose exec app python init_db.py
```
5. **資料庫遷移(如果需要)**
```bash
# 新增郵件功能欄位
docker-compose exec app python migrate_add_email_column.py
```
5. **系統訪問**
### 手動安裝
啟動完成後,您可以通過以下地址訪問系統:
#### Windows 環境
- **主系統入口**http://localhost:12013 (推薦使用,通過 Nginx 代理)
- **ONLYOFFICE 文件服務器**http://localhost:12015
1. **安裝Python依賴**
```cmd
pip install -r requirements.txt
```
2. **設定環境變數**
```cmd
copy .env.example .env
REM 編輯 .env 檔案
```
3. **初始化資料庫**
```cmd
python init_db.py
```
4. **資料庫遷移(如果需要)**
```cmd
python migrate_add_email_column.py
```
5. **啟動 ONLYOFFICE Document Server**
```cmd
docker run -d -p 8080:80 --restart=always ^
-e JWT_ENABLED=true ^
-e JWT_SECRET=your-onlyoffice-jwt-secret-string ^
onlyoffice/documentserver
```
6. **啟動應用程式**
```cmd
REM 開發環境
python app.py
REM 生產環境 (Windows 建議使用 Waitress)
pip install waitress
waitress-serve --host=0.0.0.0 --port=5000 app:app
```
#### Linux 環境
1. **安裝Python依賴**
```bash
pip install -r requirements.txt
```
2. **設定環境變數**
```bash
cp .env.example .env
# 編輯 .env 檔案
```
3. **初始化資料庫**
```bash
python init_db.py
```
4. **資料庫遷移(如果需要)**
```bash
python migrate_add_email_column.py
```
5. **啟動 ONLYOFFICE Document Server**
```bash
docker run -d -p 8080:80 --restart=always \
-e JWT_ENABLED=true \
-e JWT_SECRET=your-onlyoffice-jwt-secret-string \
onlyoffice/documentserver
```
6. **啟動應用程式**
```bash
# 開發環境
python app.py
# 生產環境 (使用 Gunicorn)
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:5000 app:app
```
> **注意**
> - 建議使用 Nginx 代理入口 (12013) 訪問系統,具有更好的性能和安全性
> - 首次啟動 ONLYOFFICE 可能需要 2-3 分鐘初始化,請等待容器狀態變為 `healthy`
## ⚙️ 組態設定
@@ -185,51 +126,48 @@ gunicorn -w 4 -b 0.0.0.0:5000 app:app
```env
# Flask 設定
SECRET_KEY=your_secret_key_here
UPLOAD_FOLDER=uploads
FLASK_ENV=production
# 資料庫設定
DATABASE_URL=mysql+pymysql://user:password@localhost/tempspec_db
# 資料庫設定 (外部 MySQL)
DATABASE_URL=mysql+pymysql://A060:WLeSCi0yhtc7@mysql.theaken.com:33306/db_A060
# LDAP 設定
LDAP_SERVER=ldap://your-dc.company.com
# LDAP 設定 (預設配置)
LDAP_SERVER=panjit.com.tw
LDAP_PORT=389
LDAP_USE_SSL=False
LDAP_SEARCH_BASE=DC=company,DC=com
LDAP_BIND_USER_DN=CN=service,DC=company,DC=com
LDAP_BIND_USER_PASSWORD=service_password
LDAP_USE_SSL=false
LDAP_SEARCH_BASE=DC=panjit,DC=com,DC=tw
LDAP_BIND_USER_DN=CN=LdapBind,CN=Users,DC=PANJIT,DC=COM,DC=TW
LDAP_BIND_USER_PASSWORD=panjit2481
LDAP_USER_LOGIN_ATTR=userPrincipalName
# SMTP 郵件設定 (Port 25 無認證方式)
SMTP_SERVER=mail.company.com
# SMTP 郵件設定
SMTP_SERVER=mail.panjit.com.tw
SMTP_PORT=25
SMTP_USE_TLS=false
SMTP_USE_SSL=false
SMTP_AUTH_REQUIRED=false
SMTP_SENDER_EMAIL=temp-spec-system@company.com
SMTP_SENDER_EMAIL=temp-spec-system@panjit.com.tw
SMTP_SENDER_PASSWORD=
# ONLYOFFICE 設定
ONLYOFFICE_URL=http://onlyoffice:8080
ONLYOFFICE_JWT_SECRET=your_jwt_secret
ONLYOFFICE_URL=http://localhost:12015/
ONLYOFFICE_INTERNAL_URL=http://onlyoffice:80
ONLYOFFICE_JWT_SECRET=your_jwt_secret_key_here
# Redis 設定
REDIS_URL=redis://redis:6379/0
# 服務端口設定
ONLYOFFICE_PORT=12015
```
### SMTP 配置說明
系統支援多種 SMTP 配置方式:
- **Port 25推薦**:內部郵件伺服器,無需認證
- **Port 587**STARTTLS + 認證
- **Port 465**SSL + 認證
詳細設定請參考 `SMTP_CONFIGURATION_UPDATE.md`
## 📚 使用說明
### 登入規範
**重要**系統要求使用完整的UPN格式帳號登入
**正確格式**`user@domain.com`
**正確格式**`user@panjit.com.tw`
**錯誤格式**`user`
### 初次設定管理員
@@ -240,9 +178,18 @@ ONLYOFFICE_JWT_SECRET=your_jwt_secret
2. **手動設定**:在資料庫中更新用戶權限:
```sql
UPDATE ts_user SET role='admin' WHERE username='user@domain.com';
UPDATE ts_user SET role='admin' WHERE username='user@panjit.com.tw';
```
### 展延次數限制
系統實作嚴格的展延控制機制:
- **初次生效**30天效期
- **第一次展延**效期變為60天
- **第二次展延**效期變為90天達到上限
- **第三次展延**:系統拒絕並顯示錯誤訊息
### 郵件通知功能
系統具備智慧郵件管理功能:
@@ -281,32 +228,124 @@ UPDATE ts_user SET role='admin' WHERE username='user@domain.com';
- 確認服務帳號權限
- 驗證 LDAP 伺服器位址和搜尋基底
2. **ONLYOFFICE 無法載入**
2. **ONLYOFFICE 無法載入文件**
- 確認 Document Server 運行狀態:`docker ps`
- 檢查網路連線設定
- 檢查容器間網路連線`docker exec panjit-tempspec-onlyoffice curl -s http://panjit-tempspec-nginx:80/health`
- 驗證 JWT Secret 設定是否一致
- 檢查日誌中的網路錯誤:`docker-compose logs onlyoffice | grep ERROR`
3. **郵件發送失敗**
**常見錯誤**
- `ECONNREFUSED` 錯誤:通常是容器間網路配置問題
- `host.docker.internal` 無法連接:應使用容器名稱而非 host.docker.internal
- 文件下載失敗:檢查靜態檔案路徑和 nginx 代理設定
3. **ONLYOFFICE 文件同步問題**
- **問題描述**:在 OnlyOffice 編輯器中編輯並儲存文件後,下載的 Word 文件沒有包含最新的編輯內容
**解決方案**
- 系統已增強 OnlyOffice 回調處理機制,支援多種儲存狀態
- 改善了文件同步邏輯,確保編輯內容正確儲存到伺服器
- 新增詳細的回調日誌記錄,便於問題診斷
**檢查方法**
```bash
# 查看 OnlyOffice 回調日誌
docker-compose logs -f app | grep "OnlyOffice callback"
# 檢查文件儲存狀態
docker-compose logs -f app | grep "Document saved successfully"
# 驗證文件更新時間
docker exec panjit-tempspec-app ls -la uploads/
```
**技術細節**
- 回調處理現在支援 status=2 (自動儲存) 和 status=6 (強制儲存)
- 增加了 JWT Token 驗證以確保回調安全性
- 改善錯誤處理和檔案下載邏輯
- 修正回調中的 URL 路由,將 localhost:12015 轉換為容器間通信地址 panjit-tempspec-onlyoffice:80
4. **時區設定問題**
- **問題描述**:系統時間顯示為 GMT+0但正確時區應該是 GMT+8台灣時區
**解決方案**
- 建立完整的時區處理模組 `utils/timezone.py`
- 修正所有資料庫時間欄位使用台灣時區
- 更新前端模板使用時區轉換 filter
- 設定 Docker 容器環境變數 `TZ: Asia/Taipei`
**檢查方法**
```bash
# 檢查容器時區設定
docker exec panjit-tempspec-app date
# 測試時區轉換功能
docker exec panjit-tempspec-app python -c "from utils.timezone import taiwan_now, format_taiwan_time; print(format_taiwan_time(taiwan_now()))"
```
**技術細節**
- 新增 `taiwan_now()` 函數替代 `datetime.utcnow()`
- 支援 `date` 和 `datetime` 物件的時區轉換
- 前端模板 filter`|taiwan_time` 和 `|taiwan_date`
- 自動處理 naive datetime 和 timezone-aware datetime
5. **展延次數限制功能**
- **功能說明**:防止暫時規範無限延期,確保企業治理合規
**限制規則**
- 初次生效30天效期
- 第一次展延效期變為60天
- 第二次展延效期變為90天達到上限
- 第三次展延:系統拒絕並顯示錯誤訊息
**實作功能**
- 後端邏輯檢查:當 `extension_count >= 2` 時拒絕展延
- 前端介面優化:超過限制時展延按鈕變為不可用
- 視覺化顯示:展延次數進度和剩餘次數
- 樣式改進:深色背景下清楚可見的展延狀態顯示
6. **郵件發送失敗**
- 確認 SMTP 設定正確
- 檢查郵件伺服器認證設定
- 驗證防火牆規則 (Port 25/587/465)
4. **排程任務未執行**
7. **排程任務未執行**
- 檢查 APScheduler 初始化
- 確認應用程式持續運行
- 查看系統日誌
8. **容器間網路通訊問題**
- 檢查所有容器是否在同一網路:`docker network ls`
- 驗證容器名稱解析:`docker exec container1 ping container2`
- 確認端口映射:`docker-compose ps`
**OnlyOffice 網路問題修正**
```bash
# 檢查 OnlyOffice 能否連接到 Flask 應用
docker exec panjit-tempspec-onlyoffice curl -I http://panjit-tempspec-nginx:80/static/
# 檢查 Flask 應用日誌
docker-compose logs -f app
# 重新構建有網路修正的應用
docker-compose up -d --build app
```
### 日誌查看
```bash
# Docker 環境
docker-compose logs -f app
# 一般環境
tail -f logs/app.log
# OnlyOffice 相關日誌
docker-compose logs -f onlyoffice
docker exec panjit-tempspec-onlyoffice tail -f /var/log/onlyoffice/documentserver/docservice/out.log
# Windows 環境
Get-Content logs/app.log -Tail 10 -Wait
# Redis 日誌
docker-compose logs -f redis
# Nginx 日誌
docker-compose logs -f nginx
```
## 🤝 開發指南
@@ -318,25 +357,43 @@ Get-Content logs/app.log -Tail 10 -Wait
├── config.py # 組態設定
├── models.py # 資料模型
├── tasks.py # 排程任務
├── wsgi.py # WSGI 入口點
├── gunicorn.conf.py # Gunicorn 配置
├── routes/ # 路由模組
│ ├── __init__.py
│ ├── auth.py # 認證相關
│ ├── temp_spec.py # 暫規管理
│ ├── upload.py # 檔案上傳
│ ├── admin.py # 管理功能
│ └── api.py # API介面
├── templates/ # 前端範本
├── static/ # 靜態檔案
├── utils.py # 工具函式
└── ldap_utils.py # LDAP 工具
├── utils/ # 工具模組
│ ├── __init__.py # 工具函式
│ └── timezone.py # 時區處理
├── ldap_utils.py # LDAP 工具
├── cache_utils.py # 快取工具
├── cdn_utils.py # CDN 工具
├── monitor.py # 監控工具
└── nginx/ # Nginx 配置
├── Dockerfile
├── nginx.conf
└── conf.d/
```
### 資料庫遷移
### 容器架構
當系統需要資料庫結構更新時:
- **panjit-tempspec-app**: Flask 應用程式容器
- **panjit-tempspec-nginx**: Nginx 反向代理容器
- **panjit-tempspec-onlyoffice**: OnlyOffice 文件服務器容器
- **panjit-tempspec-redis**: Redis 快取服務容器
```bash
# 執行遷移腳本
python migrate_add_email_column.py
```
### 網路配置
所有容器運行在 `tempspec-network` 橋接網路中,確保容器間通信:
- 外部訪問:通過 Nginx (port 12013) 和 OnlyOffice (port 12015)
- 內部通信:使用容器名稱作為主機名
## 📄 授權條款
@@ -344,7 +401,18 @@ python migrate_add_email_column.py
## 🆕 版本歷程
### v3.2.0 (最新版本)
### v4.0.0 (最新版本)
- 🆕 新增台灣時區 (GMT+8) 完整支援
- 🆕 實作展延次數限制功能最多2次總效期90天
- 🆕 修正 OnlyOffice 文件同步問題
- 🎨 改進規範列表頁面樣式和用戶體驗
- 🔧 修正時區 filter 支援 date 物件處理
- 🗑️ 移除舊版 utils.py改用模組化架構
- 🔧 修正容器間網路通信問題
- 🆕 新增 Redis 快取服務
- 🆕 新增 Nginx 反向代理
### v3.2.0
- 🆕 新增郵件通知記憶功能
- 🆕 支援 Port 25 無認證 SMTP
- ♻️ 優化郵件管理邏輯
@@ -363,4 +431,4 @@ python migrate_add_email_column.py
---
**暫時規範管理系統 V3** - 讓企業文件管理更智慧、更高效!
**暫時規範管理系統 V4** - 讓企業文件管理更智慧、更高效!

View File

@@ -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反向代理
---
@@ -38,13 +43,13 @@
**🚨 重要登入規範**
**正確格式**:必須使用完整的 UPN 格式帳號
例如:`user@domain.com`
例如:`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,7 +182,7 @@
### 4.1 🆕 郵件記憶功能
**V3.2 新增功能**:系統現在具備智慧郵件管理能力
**V4.0 持續改進功能**:系統現在具備智慧郵件管理能力
**運作機制**
1. **規範啟用時**:輸入通知郵件對象,系統自動記憶
@@ -201,12 +222,12 @@
**🆕 自動提醒**(系統排程):
- **7天到期提醒**在規範到期前7天自動發送
- **3天到期提醒**在規範到期前3天自動發送
- **發送時間**每天凌晨2:00檢查並發送
- **發送時間**每天凌晨2:00檢查並發送(台灣時間)
**郵件內容**
- HTML格式美化顯示
- 包含規範編號、標題、申請人
- 明確標示生效/結束日期
- 明確標示生效/結束日期(台灣時間顯示)
- 提供系統連結
---
@@ -239,18 +260,26 @@
點擊 **歷史紀錄圖示 (🕒)** 查看:
- 操作時間戳記
- 操作時間戳記(台灣時間顯示)
- 執行用戶
- 操作類型(建立/啟用/展延/終止)
- 詳細說明
### 5.4 即將到期警示
### 5.4 即將到期警示與展延狀態
**到期警示**
在規範列表中會特別標示即將到期的規範:
- **🟢 綠色標示**7天以上
- **🟡 橙色標示**7天內到期
- **🔴 紅色標示**3天內到期
- **閃爍動畫**:今日到
- **已過期標示**:已過
**🆕 展延狀態顯示**
- 清楚顯示「已展延 X 次」
- 達到上限時顯示「達到上限」標籤
- 優化深色背景下的可讀性
- 居中對齊,提升視覺體驗
---
@@ -274,7 +303,7 @@
**Editor編輯者**
- 建立新規範草稿
- 編輯規範內容
- 展延和終止規範
- 展延和終止規範(受展延次數限制)
- 下載Word和PDF檔案
**Admin管理員**
@@ -290,11 +319,11 @@
### 7.1 登入相關
**Q: 忘記帳號格式?**
A: 必須使用完整的 `user@domain.com` 格式,不能只輸入 `user`
A: 必須使用完整的 `user@panjit.com.tw` 格式,不能只輸入 `user`
**Q: 無法登入?**
A: 請確認:
1. 帳號格式正確(包含@domain.com
1. 帳號格式正確(包含@panjit.com.tw
2. 密碼正確
3. AD帳號未被鎖定
4. 網路連線正常
@@ -315,11 +344,13 @@ A: 請確認:
2. 網路連線穩定
3. 彈出視窗未被阻擋
**Q: 編輯內容未儲存?**
A: 建議
**🆕 Q: 編輯內容未儲存或同步**
A: V4.0已改善文件同步機制
1. 編輯期間保持網路連線
2. 避免同時多人編輯同一文件
3. 定期手動儲存 (Ctrl+S)
2. 系統現在支援多種儲存狀態
3. 增強了回調處理機制
4. 定期手動儲存 (Ctrl+S)
5. 檢查容器間網路是否正常
### 7.4 通知相關
@@ -336,7 +367,7 @@ A: 請檢查:
3. 公司郵件伺服器設定
**Q: 自動提醒郵件何時發送?**
A: 系統每天凌晨2:00自動檢查並發送提醒分別在到期前7天和3天發送。
A: 系統每天凌晨2:00(台灣時間)自動檢查並發送提醒分別在到期前7天和3天發送。
**🆕 Q: 郵件通知對象會自動記憶嗎?**
A: 是的,系統會記憶啟用時設定的通知對象:
@@ -344,7 +375,37 @@ A: 是的,系統會記憶啟用時設定的通知對象:
- 展延規範時也會自動帶出,修改後會更新記錄
- 您可以直接使用或編輯後再發送
### 7.5 檔案相關
### 7.5 展延相關
**🆕 Q: 為什麼無法繼續展延?**
A: V4.0實作嚴格的展延控制:
- 每個規範最多只能展延2次
- 總效期不能超過90天
- 達到上限後系統會拒絕展延請求
- 展延按鈕會變為不可用狀態
**🆕 Q: 展延次數如何計算?**
A: 展延次數計算規則:
- 初次生效30天不計入展延次數
- 第一次展延效期變為60天展延次數=1
- 第二次展延效期變為90天展延次數=2達到上限
### 7.6 時區相關
**🆕 Q: 系統顯示的時間是否正確?**
A: V4.0完整支援台灣時區:
- 所有時間顯示使用台灣時區 (GMT+8)
- 資料庫儲存自動轉換為台灣時間
- 郵件通知使用台灣時間
- 到期檢查基於台灣時間
**🆕 Q: 舊紀錄的時間顯示是否正確?**
A: 系統已實作時區轉換機制:
- 自動處理舊紀錄的時區轉換
- 支援date和datetime物件轉換
- 確保所有時間顯示一致性
### 7.7 檔案相關
**Q: 可以上傳Word檔案來啟用規範嗎**
A: 不可以。為確保文件完整性,啟用時必須上傳已簽核的 **PDF檔案**
@@ -356,25 +417,34 @@ A: 請確認:
3. 檔案名稱不含特殊字元
4. 網路連線穩定
### 7.6 效能相關
### 7.8 效能相關
**Q: 系統回應速度慢?**
A: 可能原因
1. 網路連線問題
2. 伺服器負載過高
3. 資料庫查詢耗時
4. 聯繫系統管理員檢查
A: V4.0已優化效能
1. 新增Redis快取系統
2. Nginx反向代理提升速度
3. 如仍有問題請聯繫系統管理員
4. 檢查網路連線狀況
---
## 📝 版本資訊
- **文件版本**: V3.2.0
- **最後更新**: 2025年1
- **適用系統**: 暫時規範管理系統 V3.2
- **文件版本**: V4.0.0
- **最後更新**: 2025年9
- **適用系統**: 暫時規範管理系統 V4.0
### 版本更新記錄
**V4.0.0**
- 新增台灣時區完整支援
- 實作展延次數限制功能最多2次90天上限
- 修正OnlyOffice文件同步問題
- 改進UI樣式優化深色背景下的顯示
- 修正時區filter支援date物件處理
- 移除舊版utils.py改用模組化架構
- 新增Redis快取和Nginx反向代理
**V3.2.0**
- 新增郵件通知記憶功能
- 支援Port 25無認證SMTP
@@ -389,5 +459,5 @@ A: 可能原因:
---
**感謝您使用暫時規範管理系統 V3**
希望這個操作手冊能幫助您更有效地使用系統功能。
**感謝您使用暫時規範管理系統 V4**
希望這個操作手冊能幫助您更有效地使用系統功能。如有任何問題,請聯繫系統管理員。

13
app.py
View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,7 @@
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime
from utils.timezone import taiwan_now
db = SQLAlchemy()
@@ -53,7 +54,7 @@ class SpecHistory(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('ts_user.id', ondelete='SET NULL'), nullable=True)
action = db.Column(db.String(50), nullable=False)
details = db.Column(db.Text, nullable=True)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
timestamp = db.Column(db.DateTime, default=taiwan_now)
# 建立與 User 和 TempSpec 的關聯,方便查詢
user = db.relationship('User')

21
nginx/Dockerfile Normal file
View 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;"]

View File

@@ -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;
}

View File

@@ -1,21 +1,34 @@
flask
flask-login
flask-sqlalchemy
pymysql
werkzeug
docx2pdf
python-docx
docxtpl
beautifulsoup4
lxml
python-dotenv
mistune
PyJWT
ldap3
Flask-APScheduler
Pillow
requests
cryptography
gunicorn
redis
flask-caching
# Flask Framework (統一版本)
Flask==3.0.0
Flask-Login==0.6.3
Flask-SQLAlchemy==3.0.5
Flask-Caching==2.1.0
Flask-APScheduler==1.13.1
# Database
PyMySQL==1.1.0
# Web Server
Werkzeug==3.0.1
gunicorn==21.2.0
# Authentication
PyJWT==2.8.0
ldap3==2.9.1
# Document Processing
python-docx==1.1.0
docxtpl==0.16.7
docx2pdf==0.1.8
# Utilities
beautifulsoup4==4.12.2
lxml==4.9.3
python-dotenv==1.0.0
mistune==3.0.2
Pillow==10.1.0
requests==2.31.0
cryptography==41.0.7
# Cache & Task Queue
redis==5.0.1

View File

@@ -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

View File

@@ -2,6 +2,7 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, send_file, current_app, jsonify, abort
from flask_login import login_required, current_user
from datetime import datetime, timedelta
from utils.timezone import taiwan_now, format_taiwan_time
from models import TempSpec, db, Upload, SpecHistory
from utils import editor_or_admin_required, add_history_log, admin_required, send_email, process_recipients
from ldap_utils import get_ldap_group_members
@@ -27,7 +28,7 @@ def _generate_next_spec_code():
產生下一個暫時規範編號。
規則: PE + 民國年(3碼) + 月份(2碼) + 流水號(2碼)
"""
now = datetime.now()
now = taiwan_now()
roc_year = now.year - 1911
prefix = f"PE{roc_year}{now.strftime('%m')}"
@@ -54,7 +55,7 @@ def create_temp_spec():
if request.method == 'POST':
spec_code = _generate_next_spec_code()
form_data = request.form
now = datetime.now()
now = taiwan_now()
# 1. 在資料庫中建立紀錄
spec = TempSpec(
@@ -140,20 +141,25 @@ def edit_spec(spec_id):
doc_url = get_file_uri(doc_filename)
callback_url = url_for('temp_spec.onlyoffice_callback', spec_id=spec_id, _external=True)
# 2. 如果是在開發環境,將 URL 中的 localhost 替換為 Docker 可存取的地址
# 2. 修正容器間通訊的 URL使用正確的容器名稱
if '127.0.0.1' in doc_url or 'localhost' in doc_url:
# 同時修正 doc_url 和 callback_url
doc_url = doc_url.replace('127.0.0.1', 'host.docker.internal').replace('localhost', 'host.docker.internal')
callback_url = callback_url.replace('127.0.0.1', 'host.docker.internal').replace('localhost', 'host.docker.internal')
# 在 Docker Compose 環境中OnlyOffice 應該透過 nginx 存取 Flask 應用
doc_url = doc_url.replace('127.0.0.1:12013', 'panjit-tempspec-nginx:80').replace('localhost:12013', 'panjit-tempspec-nginx:80')
doc_url = doc_url.replace('127.0.0.1', 'panjit-tempspec-nginx').replace('localhost', 'panjit-tempspec-nginx')
callback_url = callback_url.replace('127.0.0.1:12013', 'panjit-tempspec-nginx:80').replace('localhost:12013', 'panjit-tempspec-nginx:80')
callback_url = callback_url.replace('127.0.0.1', 'panjit-tempspec-nginx').replace('localhost', 'panjit-tempspec-nginx')
# --- END: 修正文件下載與回呼的 URL ---
oo_secret = current_app.config['ONLYOFFICE_JWT_SECRET']
# 生成唯一的文件密鑰,包含更新時間戳
file_key = f"{spec.id}_{int(os.path.getmtime(doc_physical_path))}"
payload = {
"document": {
"fileType": "docx",
"key": f"{spec.id}_{int(os.path.getmtime(doc_physical_path))}",
"key": file_key,
"title": doc_filename,
"url": doc_url # <-- 使用修正後的 doc_url
},
@@ -161,7 +167,14 @@ def edit_spec(spec_id):
"editorConfig": {
"callbackUrl": callback_url, # <-- 使用修正後的回呼 URL
"user": { "id": str(current_user.id), "name": current_user.username },
"customization": { "autosave": True, "forcesave": True }
"customization": {
"autosave": True,
"forcesave": True,
"chat": False,
"comments": True,
"help": False
},
"mode": "edit"
}
}
@@ -181,22 +194,78 @@ def edit_spec(spec_id):
@temp_spec_bp.route('/onlyoffice-callback/<int:spec_id>', methods=['POST'])
def onlyoffice_callback(spec_id):
data = request.json
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:
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 ---
@@ -383,6 +452,11 @@ def download_signed_pdf(spec_id):
def extend_spec(spec_id):
spec = TempSpec.query.get_or_404(spec_id)
# 檢查展延次數限制最多展延2次
if spec.extension_count >= 2:
flash('此暫時規範已達展延次數上限2次無法再次展延。總效期已達90天上限。', 'danger')
return redirect(url_for('temp_spec.spec_list'))
if request.method == 'POST':
new_end_date_str = request.form.get('new_end_date')
uploaded_file = request.files.get('new_file')
@@ -399,7 +473,7 @@ def extend_spec(spec_id):
spec.extension_count += 1
spec.status = 'active'
filename = secure_filename(f"{spec.spec_code}_extension_{spec.extension_count}_{datetime.now().strftime('%Y%m%d')}.pdf")
filename = secure_filename(f"{spec.spec_code}_extension_{spec.extension_count}_{taiwan_now().strftime('%Y%m%d')}.pdf")
upload_folder = os.path.join(BASE_DIR, current_app.config['UPLOAD_FOLDER'])
os.makedirs(upload_folder, exist_ok=True)
file_path = os.path.join(upload_folder, filename)
@@ -408,7 +482,7 @@ def extend_spec(spec_id):
new_upload = Upload(
temp_spec_id=spec.id,
filename=filename,
upload_time=datetime.now()
upload_time=taiwan_now()
)
db.session.add(new_upload)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -12,12 +12,18 @@
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<p><strong>主題:</strong> {{ spec.title }}</p>
<p><strong>原結束日期:</strong> {{ spec.end_date.strftime('%Y-%m-%d') }}</p>
<p><strong>原結束日期:</strong> {{ spec.end_date|taiwan_date }}</p>
<p><strong>展延次數:</strong>
<span class="badge bg-info">{{ spec.extension_count }} / 2</span>
<small class="text-muted ms-2">
剩餘可展延次數: <strong>{{ 2 - spec.extension_count }}</strong>
</small>
</p>
<div class="mb-3">
<label for="new_end_date" class="form-label"><strong>新的結束日期</strong></label>
<input type="date" class="form-control" id="new_end_date" name="new_end_date"
value="{{ default_new_end_date.strftime('%Y-%m-%d') }}" required>
value="{{ default_new_end_date|taiwan_date }}" required>
<div class="form-text">預設為原結束日期後一個月。</div>
</div>

View File

@@ -21,7 +21,7 @@
<span class="badge bg-primary rounded-pill me-2">{{ entry.action }}</span>
<strong>{{ entry.user.username if entry.user else '[已刪除的使用者]' }}</strong> 執行
</h5>
<small>{{ entry.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</small>
<small>{{ entry.timestamp|taiwan_time }}</small>
</div>
<p class="mb-1 mt-2">{{ entry.details }}</p>
</li>

View File

@@ -47,7 +47,7 @@
<th>建立日期</th>
<th>結束日期</th>
<th class="text-center">剩餘天數</th>
<th>狀態</th>
<th class="text-center">狀態</th>
<th class="text-center">操作</th>
</tr>
</thead>
@@ -57,8 +57,8 @@
<td>{{ spec.spec_code }}</td>
<td>{{ spec.title }}</td>
<td>{{ spec.applicant }}</td>
<td>{{ spec.created_at.strftime('%Y-%m-%d') }}</td>
<td>{{ spec.end_date.strftime('%Y-%m-%d') }}</td>
<td>{{ spec.created_at|taiwan_date }}</td>
<td>{{ spec.end_date|taiwan_date }}</td>
<td class="text-center">
{% if spec.status in ['active', 'expired'] %}
@@ -80,7 +80,7 @@
{% endif %}
</td>
<td>
<td class="text-center">
{% if spec.status == 'active' %}
<span class="badge fs-6 bg-success bg-opacity-75"><i class="bi bi-check-circle-fill me-1"></i>已生效</span>
{% elif spec.status == 'pending_approval' %}
@@ -90,6 +90,18 @@
{% else %}
<span class="badge fs-6 bg-secondary bg-opacity-75"><i class="bi bi-calendar-x-fill me-1"></i>已過期</span>
{% endif %}
{% if spec.extension_count > 0 %}
<br>
<div class="mt-1">
<span class="badge bg-light text-dark border">
<i class="bi bi-arrow-repeat me-1"></i>已展延 {{ spec.extension_count }} 次
</span>
{% if spec.extension_count >= 2 %}
<br><span class="badge bg-danger mt-1">達到上限</span>
{% endif %}
</div>
{% endif %}
</td>
<td class="text-center">
@@ -102,7 +114,11 @@
{% endif %}
{% if current_user.role in ['editor', 'admin'] and spec.status == 'active' %}
{% if spec.extension_count < 2 %}
<a href="{{ url_for('temp_spec.extend_spec', spec_id=spec.id) }}" class="btn btn-sm btn-secondary" title="展延"><i class="bi bi-calendar-plus"></i></a>
{% else %}
<button class="btn btn-sm btn-secondary" disabled title="已達展延次數上限2次"><i class="bi bi-calendar-x"></i></button>
{% endif %}
<a href="{{ url_for('temp_spec.terminate_spec', spec_id=spec.id) }}" class="btn btn-sm btn-danger" title="終止"><i class="bi bi-x-circle"></i></a>
{% endif %}

View File

@@ -84,7 +84,7 @@
</td>
<td>
{% if user.last_login %}
{{ user.last_login.strftime('%Y-%m-%d %H:%M') }}
{{ user.last_login|taiwan_time('%Y-%m-%d %H:%M') }}
{% else %}
<span class="text-muted">從未登入</span>
{% endif %}

View File

@@ -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):
@@ -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

102
utils/timezone.py Normal file
View 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)