This commit is contained in:
beabigegg
2025-08-28 11:51:04 +08:00
parent 4f7f46b07a
commit bdfda30ca8
12 changed files with 441 additions and 222 deletions

View File

@@ -21,11 +21,32 @@ LDAP_BIND_USER_PASSWORD=service_password
LDAP_USER_LOGIN_ATTR=userPrincipalName
# SMTP 郵件設定
SMTP_SERVER=smtp.company.com
SMTP_PORT=587
SMTP_USE_TLS=True
SMTP_SENDER_EMAIL=noreply@company.com
SMTP_SENDER_PASSWORD=smtp_password
# 方案 1: 使用 Port 25 (無需認證) - 推薦用於內部郵件伺服器
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=
# 方案 2: 使用 Port 587 (需要認證) - 用於外部 SMTP 或有認證需求的伺服器
# SMTP_SERVER=smtp.company.com
# SMTP_PORT=587
# SMTP_USE_TLS=true
# SMTP_USE_SSL=false
# SMTP_AUTH_REQUIRED=true
# SMTP_SENDER_EMAIL=noreply@company.com
# SMTP_SENDER_PASSWORD=smtp_password
# 方案 3: 使用 Port 465 (SSL + 認證) - 用於安全連接
# SMTP_SERVER=smtp.gmail.com
# SMTP_PORT=465
# SMTP_USE_TLS=false
# SMTP_USE_SSL=true
# SMTP_AUTH_REQUIRED=true
# SMTP_SENDER_EMAIL=yourapp@gmail.com
# SMTP_SENDER_PASSWORD=app_password
# ONLYOFFICE Document Server 設定
ONLYOFFICE_PORT=8080

View File

@@ -0,0 +1,92 @@
# 郵件通知功能優化說明
## 更新日期2025-01-28
## 一、優化內容概述
本次優化主要改善了郵件通知的管理邏輯,實現了郵件通知對象的自動記憶和帶出功能,讓系統更符合實際使用需求。
## 二、主要變更
### 1. 資料庫結構調整
-`ts_temp_spec` 表新增 `notification_emails` 欄位
- 用於儲存規範生效時設定的郵件通知對象清單
- 郵件地址以分號 (;) 分隔儲存
### 2. 功能邏輯改進
#### 規範生效時Activate
- 使用者輸入郵件通知對象
- 系統自動儲存郵件清單到資料庫
- 發送生效通知郵件
#### 規範停止時Terminate
- 系統自動從資料庫讀取之前儲存的郵件清單
- 在介面上預先填入這些郵件地址
- 顯示提示訊息告知使用者這是生效時使用的通知對象
- 使用者可以:
- 直接使用預設的郵件清單
- 修改郵件清單後再發送
- 清空並重新選擇收件者
#### 規範展延時Extend
- 系統自動從資料庫讀取之前儲存的郵件清單
- 在介面上預先填入這些郵件地址
- 顯示提示訊息說明可以編輯郵件清單
- 若使用者修改了郵件清單,系統會更新資料庫中的記錄
- 確保後續的通知都會發送給更新後的收件者
## 三、實施步驟
### 1. 執行資料庫遷移
```bash
python migrate_add_email_column.py
```
此腳本會:
- 檢查 `notification_emails` 欄位是否已存在
- 如果不存在,自動新增該欄位
- 驗證欄位是否成功新增
### 2. 重啟應用程式
完成資料庫遷移後,重啟 Flask 應用程式以載入新的功能。
## 四、使用說明
### 對於管理員
1. **首次生效**:在啟用規範時,輸入所有需要通知的郵件地址
2. **後續操作**:系統會自動記憶這些地址,無需重複輸入
### 對於編輯者
1. **終止規範**:可以看到並使用之前設定的通知對象
2. **展延規範**
- 可以保留原有的通知對象
- 也可以根據需要調整通知名單
- 修改後的名單會被儲存供下次使用
## 五、優點
1. **減少重複輸入**:不需要每次都重新輸入相同的郵件地址
2. **保持一致性**:確保同一規範的相關人員都能收到通知
3. **提供彈性**:允許使用者在需要時修改通知名單
4. **提升效率**:加快操作流程,減少錯誤
5. **改善使用體驗**:更符合實際業務邏輯
## 六、注意事項
1. 舊的規範資料不會有預存的郵件清單,需要在首次操作時輸入
2. 郵件地址以分號分隔儲存,系統會自動處理格式
3. 如果需要完全清空通知名單,可以在介面上刪除所有地址
## 七、技術細節
### 修改的檔案
1. `models.py` - 新增資料庫欄位
2. `routes/temp_spec.py` - 更新郵件處理邏輯
3. `templates/terminate_spec.html` - 新增郵件預填功能
4. `templates/extend_spec.html` - 新增郵件預填功能
5. `migrate_add_email_column.py` - 資料庫遷移腳本
### 相容性
- 完全向後相容,不影響現有功能
- 舊資料可正常使用,新功能會在首次使用時生效

View File

@@ -0,0 +1,152 @@
# SMTP 配置更新說明
## 更新日期2025-01-28
## 一、更新背景
根據與 IT 部門確認,郵件伺服器 `mail.panjit.com.tw` 的 Port 25 支援無密碼認證的系統派送郵件功能。因此將系統從原本的 Port 465需要認證改為 Port 25無需認證的方式發送郵件。
## 二、主要變更內容
### 1. Config 配置新增項目
`config.py` 中新增以下配置選項:
```python
SMTP_USE_SSL = os.getenv('SMTP_USE_SSL', 'false').lower() in ['true', '1', 't']
SMTP_SENDER_EMAIL = os.getenv('SMTP_SENDER_EMAIL', 'temp-spec-system@panjit.com.tw')
SMTP_SENDER_PASSWORD = os.getenv('SMTP_SENDER_PASSWORD', '') # Port 25 不需要密碼
SMTP_AUTH_REQUIRED = os.getenv('SMTP_AUTH_REQUIRED', 'false').lower() in ['true', '1', 't']
```
### 2. 發送邏輯優化
更新 `utils.py` 中的 `send_email` 函式:
- 支援多種 SMTP 連接方式
- Port 465SSL + 認證
- Port 587TLS + 認證
- Port 25無加密 + 無認證(系統派送)
### 3. 環境變數配置
更新 `.env.example` 提供三種配置方案的範例。
## 三、新的配置方案
### 方案 1Port 25 無認證(推薦)
```env
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=
```
**適用場景**
- 內部郵件伺服器
- 系統自動發送通知郵件
- 不需要個人帳號認證
### 方案 2Port 587 + TLS + 認證
```env
SMTP_SERVER=smtp.company.com
SMTP_PORT=587
SMTP_USE_TLS=true
SMTP_USE_SSL=false
SMTP_AUTH_REQUIRED=true
SMTP_SENDER_EMAIL=noreply@company.com
SMTP_SENDER_PASSWORD=smtp_password
```
**適用場景**
- 外部 SMTP 服務
- 需要認證的郵件伺服器
- 使用 STARTTLS 加密
### 方案 3Port 465 + SSL + 認證
```env
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=465
SMTP_USE_TLS=false
SMTP_USE_SSL=true
SMTP_AUTH_REQUIRED=true
SMTP_SENDER_EMAIL=yourapp@gmail.com
SMTP_SENDER_PASSWORD=app_password
```
**適用場景**
- Gmail 等外部服務
- 需要 SSL 加密連接
- 高安全性要求
## 四、實施步驟
### 1. 更新環境變數
修改您的 `.env` 檔案,使用方案 1 的配置:
```env
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=
```
### 2. 設定適當的寄件者郵件地址
建議使用有意義的系統郵件地址,例如:
- `temp-spec-system@panjit.com.tw`
- `tempspec-notification@panjit.com.tw`
- `noreply-tempspec@panjit.com.tw`
### 3. 重啟應用程式
更新配置後,重啟 Flask 應用程式以載入新的 SMTP 設定。
### 4. 測試郵件功能
建議在正式環境使用前,先測試郵件發送功能:
- 啟用一個測試規範
- 驗證郵件是否正常發送
- 檢查郵件格式和內容
## 五、向後相容性
此次更新完全向後相容:
- 原有使用 Port 465/587 + 認證的配置仍然有效
- 系統會根據配置自動選擇適當的連接方式
- 不會影響現有的郵件發送功能
## 六、除錯訊息
系統提供詳細的除錯訊息,包括:
- SMTP 連接方式SSL/TLS/一般)
- 認證狀態
- 郵件發送結果
- 錯誤診斷訊息
查看應用程式日誌可以了解郵件發送的詳細過程。
## 七、注意事項
1. **寄件者地址**:使用系統郵件地址,避免使用個人郵件地址
2. **網路安全**Port 25 通常在防火牆中開放,但請確認網路連通性
3. **郵件限制**:部分郵件伺服器可能有發送頻率限制
4. **監控建議**:建議監控郵件發送成功率和錯誤日誌
## 八、疑難排解
### 常見問題
**Q: 郵件發送失敗,顯示連接拒絕**
A: 檢查 SMTP_SERVER 和 SMTP_PORT 設定是否正確
**Q: 郵件可以發送但收不到**
A: 檢查寄件者郵件地址是否為有效的公司郵件地址
**Q: 系統顯示認證失敗**
A: 確認 SMTP_AUTH_REQUIRED=falsePort 25 不需要認證
**Q: 要如何切換回認證模式**
A: 修改 .env 檔案,設定 SMTP_AUTH_REQUIRED=true 並提供正確的密碼

View File

@@ -27,5 +27,7 @@ class Config:
SMTP_SERVER = os.getenv('SMTP_SERVER', 'mail.panjit.com.tw')
SMTP_PORT = int(os.getenv('SMTP_PORT', 25))
SMTP_USE_TLS = os.getenv('SMTP_USE_TLS', 'false').lower() in ['true', '1', 't']
SMTP_SENDER_EMAIL = os.getenv('SMTP_SENDER_EMAIL')
SMTP_SENDER_PASSWORD = os.getenv('SMTP_SENDER_PASSWORD')
SMTP_USE_SSL = os.getenv('SMTP_USE_SSL', 'false').lower() in ['true', '1', 't']
SMTP_SENDER_EMAIL = os.getenv('SMTP_SENDER_EMAIL', 'temp-spec-system@panjit.com.tw')
SMTP_SENDER_PASSWORD = os.getenv('SMTP_SENDER_PASSWORD', '') # Port 25 不需要密碼
SMTP_AUTH_REQUIRED = os.getenv('SMTP_AUTH_REQUIRED', 'false').lower() in ['true', '1', 't']

View File

@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""
資料庫遷移腳本:新增 notification_emails 欄位到 ts_temp_spec 表
執行方式python migrate_add_email_column.py
"""
import os
import sys
from sqlalchemy import create_engine, text
from config import Config
def migrate_database():
"""執行資料庫遷移,新增 notification_emails 欄位"""
# 使用 config.py 中的資料庫配置
engine = create_engine(Config.SQLALCHEMY_DATABASE_URI)
try:
with engine.connect() as conn:
# 檢查欄位是否已存在
result = conn.execute(text("""
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'ts_temp_spec'
AND COLUMN_NAME = 'notification_emails'
AND TABLE_SCHEMA = DATABASE()
"""))
if result.fetchone():
print("✓ notification_emails 欄位已存在,無需遷移")
return True
# 新增欄位
print("正在新增 notification_emails 欄位到 ts_temp_spec 表...")
conn.execute(text("""
ALTER TABLE ts_temp_spec
ADD COLUMN notification_emails TEXT DEFAULT NULL
COMMENT '通知郵件清單,以分號分隔'
"""))
conn.commit()
print("✓ 成功新增 notification_emails 欄位")
# 驗證欄位是否成功新增
result = conn.execute(text("""
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'ts_temp_spec'
AND COLUMN_NAME = 'notification_emails'
AND TABLE_SCHEMA = DATABASE()
"""))
column_info = result.fetchone()
if column_info:
print(f"✓ 欄位資訊:")
print(f" - 欄位名稱:{column_info[0]}")
print(f" - 資料類型:{column_info[1]}")
print(f" - 可否為空:{column_info[2]}")
return True
else:
print("✗ 欄位新增後無法驗證")
return False
except Exception as e:
print(f"✗ 遷移失敗:{str(e)}")
return False
if __name__ == "__main__":
print("=" * 50)
print("資料庫遷移腳本")
print("=" * 50)
success = migrate_database()
if success:
print("\n✓ 資料庫遷移完成!")
print(" 現在可以使用新的郵件通知功能了。")
else:
print("\n✗ 資料庫遷移失敗,請檢查錯誤訊息。")
sys.exit(1)

View File

@@ -27,6 +27,7 @@ class TempSpec(db.Model):
created_at = db.Column(db.DateTime)
extension_count = db.Column(db.Integer, default=0)
termination_reason = db.Column(db.Text, nullable=True)
notification_emails = db.Column(db.Text, nullable=True) # 儲存通知郵件清單,以分號分隔
# 關聯到 Upload 和 SpecHistory並設定級聯刪除
uploads = db.relationship('Upload', back_populates='spec', cascade='all, delete-orphan')

View File

@@ -265,12 +265,17 @@ def activate_spec(spec_id):
db.session.add(new_upload)
spec.status = 'active'
# 儲存通知郵件清單到資料庫
recipients_str = request.form.get('recipients')
if recipients_str:
spec.notification_emails = recipients_str.strip()
add_history_log(spec.id, '啟用', f"上傳已簽核檔案 '{filename}'")
db.session.commit()
flash(f"規範 '{spec.spec_code}' 已生效!", 'success')
# --- Start of Dynamic Email Notification ---
recipients_str = request.form.get('recipients')
if recipients_str:
recipients = process_recipients(recipients_str)
if recipients:
@@ -312,7 +317,10 @@ def terminate_spec(spec_id):
add_history_log(spec.id, '終止', f"原因: {reason}")
# --- Start of Dynamic Email Notification ---
# 優先使用表單提交的收件者,如果沒有則使用資料庫中儲存的
recipients_str = request.form.get('recipients')
if not recipients_str and spec.notification_emails:
recipients_str = spec.notification_emails
if recipients_str:
recipients = process_recipients(recipients_str)
if recipients:
@@ -338,7 +346,8 @@ def terminate_spec(spec_id):
flash(f"規範 '{spec.spec_code}' 已被提早終止。", 'warning')
return redirect(url_for('temp_spec.spec_list'))
return render_template('terminate_spec.html', spec=spec)
# 將儲存的郵件清單傳遞給模板
return render_template('terminate_spec.html', spec=spec, saved_emails=spec.notification_emails)
@temp_spec_bp.route('/download_initial_word/<int:spec_id>')
@login_required
@@ -408,7 +417,14 @@ def extend_spec(spec_id):
add_history_log(spec.id, '展延', details)
# --- Start of Dynamic Email Notification ---
# 優先使用表單提交的收件者,如果沒有則使用資料庫中儲存的
recipients_str = request.form.get('recipients')
if not recipients_str and spec.notification_emails:
recipients_str = spec.notification_emails
# 如果使用者有更新郵件清單,儲存回資料庫
if recipients_str:
spec.notification_emails = recipients_str.strip()
if recipients_str:
recipients = process_recipients(recipients_str)
if recipients:
@@ -434,7 +450,8 @@ def extend_spec(spec_id):
return redirect(url_for('temp_spec.spec_list'))
default_new_end_date = spec.end_date + timedelta(days=30)
return render_template('extend_spec.html', spec=spec, default_new_end_date=default_new_end_date)
# 將儲存的郵件清單傳遞給模板
return render_template('extend_spec.html', spec=spec, default_new_end_date=default_new_end_date, saved_emails=spec.notification_emails)
@temp_spec_bp.route('/history/<int:spec_id>')
@login_required # 補上登入驗證

Binary file not shown.

View File

@@ -30,6 +30,11 @@
<!-- 郵件通知對象選擇 -->
<div class="mb-3">
<label for="recipients" class="form-label"><strong>郵件通知對象</strong></label>
{% if saved_emails %}
<div class="alert alert-info mb-2">
<small>以下為生效時設定的通知對象,您可以直接使用或進行編輯。如果修改,展延後將更新為新的通知對象。</small>
</div>
{% endif %}
<select id="recipients" name="recipients" multiple placeholder="請輸入姓名或 Email 來搜尋...">
</select>
<div class="form-text">可搜尋姓名或 Email 地址,支援多人選擇</div>
@@ -45,6 +50,13 @@
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// 預先載入已儲存的郵件清單
{% if saved_emails %}
const savedEmails = "{{ saved_emails }}".split(';').filter(email => email.trim());
{% else %}
const savedEmails = [];
{% endif %}
const recipientSelect = new TomSelect('#recipients', {
valueField: 'value',
labelField: 'text',
@@ -89,6 +101,17 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
});
// 預填已儲存的郵件
if (savedEmails.length > 0) {
savedEmails.forEach(email => {
const trimmedEmail = email.trim();
if (trimmedEmail) {
recipientSelect.addOption({value: trimmedEmail, text: trimmedEmail});
recipientSelect.addItem(trimmedEmail);
}
});
}
});
</script>
{% endblock %}

View File

@@ -23,6 +23,11 @@
<!-- 郵件通知對象選擇 -->
<div class="mb-3">
<label for="recipients" class="form-label"><strong>郵件通知對象</strong></label>
{% if saved_emails %}
<div class="alert alert-info mb-2">
<small>以下為生效時設定的通知對象,您可以直接使用或進行編輯。</small>
</div>
{% endif %}
<select id="recipients" name="recipients" multiple placeholder="請輸入姓名或 Email 來搜尋...">
</select>
<div class="form-text">可搜尋姓名或 Email 地址,支援多人選擇</div>
@@ -38,6 +43,13 @@
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// 預先載入已儲存的郵件清單
{% if saved_emails %}
const savedEmails = "{{ saved_emails }}".split(';').filter(email => email.trim());
{% else %}
const savedEmails = [];
{% endif %}
const recipientSelect = new TomSelect('#recipients', {
valueField: 'value',
labelField: 'text',
@@ -82,6 +94,17 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
});
// 預填已儲存的郵件
if (savedEmails.length > 0) {
savedEmails.forEach(email => {
const trimmedEmail = email.trim();
if (trimmedEmail) {
recipientSelect.addOption({value: trimmedEmail, text: trimmedEmail});
recipientSelect.addItem(trimmedEmail);
}
});
}
});
</script>
{% endblock %}

View File

@@ -213,7 +213,7 @@ def process_recipients(recipients_str):
def send_email(to_addrs, subject, body):
"""
Sends an email using the SMTP settings from the config.
Enhanced with detailed debugging information.
Supports both authenticated (Port 465/587) and unauthenticated (Port 25) methods.
"""
print(f"[EMAIL DEBUG] 開始發送郵件...")
print(f"[EMAIL DEBUG] 收件者數量: {len(to_addrs)}")
@@ -224,14 +224,18 @@ def send_email(to_addrs, subject, body):
# 取得 SMTP 設定
smtp_server = current_app.config['SMTP_SERVER']
smtp_port = current_app.config['SMTP_PORT']
use_tls = current_app.config['SMTP_USE_TLS']
use_tls = current_app.config.get('SMTP_USE_TLS', False)
use_ssl = current_app.config.get('SMTP_USE_SSL', False)
sender_email = current_app.config['SMTP_SENDER_EMAIL']
sender_password = current_app.config['SMTP_SENDER_PASSWORD']
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}")
print(f"[EMAIL DEBUG] - 使用 SSL: {use_ssl}")
print(f"[EMAIL DEBUG] - 寄件者: {sender_email}")
print(f"[EMAIL DEBUG] - 需要認證: {auth_required}")
print(f"[EMAIL DEBUG] - 有密碼: {'' if sender_password else ''}")
# 建立郵件內容
@@ -243,21 +247,29 @@ def send_email(to_addrs, subject, body):
print(f"[EMAIL DEBUG] 郵件內容建立完成")
# 連接 SMTP 伺服器
print(f"[EMAIL DEBUG] 連接 SMTP 伺服器 {smtp_server}:{smtp_port}...")
server = smtplib.SMTP(smtp_server, smtp_port)
if use_ssl and smtp_port == 465:
# Port 465 使用 SSL
print(f"[EMAIL DEBUG] 使用 SSL 連接 SMTP 伺服器 {smtp_server}:{smtp_port}...")
server = smtplib.SMTP_SSL(smtp_server, smtp_port)
else:
# 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:
if use_tls and smtp_port == 587:
print(f"[EMAIL DEBUG] 啟用 TLS...")
server.starttls()
print(f"[EMAIL DEBUG] TLS 啟用成功")
if sender_password:
# 只在需要認證時才登入
if auth_required and sender_password:
print(f"[EMAIL DEBUG] 登入 SMTP 伺服器...")
server.login(sender_email, sender_password)
print(f"[EMAIL DEBUG] SMTP 登入成功")
else:
print(f"[EMAIL DEBUG] 無需密碼認證")
print(f"[EMAIL DEBUG] 使用匿名發送Port 25 無需認證")
# 發送郵件
print(f"[EMAIL DEBUG] 發送郵件...")

View File

@@ -1,204 +0,0 @@
功能升級計畫:智慧通知模組
1. 專案願景
本次升級旨在將系統的郵件通知功能從一個靜態的範例,全面進化為一個智慧化、高彈性、自動化的通知中樞。完成後,系統將具備以下三大核心能力:
互動式通知:在所有關鍵操作(啟用、展延、終止)中,提供類似 Outlook 的動態收件人搜尋介面。
全流程覆蓋:確保暫規生命週期中的每一個重要節點,都能觸發對應的事件通知。
前瞻性提醒:從被動通知轉向主動預警,自動發送即將到期的提醒郵件,防止暫規失效。
2. 專案藍圖 (Roadmap)
本計畫將分為三個獨立但環環相扣的模組進行開發,建議依序實施。
模組 核心功能 開發重點 狀態
模組 A 動態收件人介面 建置 LDAP 搜尋 API、前端 UI 元件整合 全新開發
模組 B 全流程事件通知 將模組 A 的成果應用於「展延」與「終止」流程 功能擴展
模組 C 自動化排程提醒 引入排程器、建立每日檢查任務、定義提醒規則 全新開發
匯出到試算表
3. 模組 A動態收件人介面 (互動式通知)
此模組是整個計畫的基石,目標是打造一個可複用的收件人選擇器。
A.1. 後端:建置 LDAP 搜尋 API
強化 ldap_utils.py新增 search_ldap_principals 函式,使用服務帳號安全地查詢 AD 中符合條件的使用者,並回傳其姓名與 Email。
建立 API 路由 routes/api.py建立一個新的 Blueprint提供 /api/ldap-search 端點,並使用 @login_required 保護,僅供登入者呼叫。
註冊 API 路由:在 app.py 中註冊 api_bp使 API 生效。
(詳細程式碼請參考前次對話中的計畫,此處不再贅述)
A.2. 前端:整合 Tom Select 動態搜尋元件
修改 templates/base.html透過 CDN 引入 Tom Select 的 CSS 與 JavaScript 函式庫。
修改 templates/activate_spec.html
新增一個 <select multiple> 輸入框,用於選擇收件人。
編寫 JavaScript初始化 Tom Select 元件,並設定其 load 事件去呼叫後端的 /api/ldap-search API實現使用者輸入時的動態搜尋與載入功能。
A.3. 邏輯:更新啟用流程
修改 routes/temp_spec.py 中的 activate_spec 函式:
移除原先寫死的 get_ldap_group_members('TempSpec_Approvers') 邏輯。
改為從 request.form.get('recipients') 獲取由前端 Tom Select 傳來、以逗號分隔的 Email 字串。
解析字串為 Email 列表,並傳遞給 send_email 函式。
4. 模組 B全流程事件通知 (全流程覆蓋)
此模組的目標是將 模組 A 的成果,擴展應用到「展延」與「終止」這兩個操作上。
B.1. 修改 templates/extend_spec.html
複製 activate_spec.html 中新增的「郵件通知對象」HTML 區塊。
將其貼到 extend_spec.html 的表單中,<button type="submit"> 之前。
複製 activate_spec.html 中 {% block scripts %} 內的 Tom Select 初始化腳本,並貼到 extend_spec.html 的 scripts 區塊中。
B.2. 修改 routes/temp_spec.py 中的 extend_spec 函式
Python
# In: routes/temp_spec.py
@temp_spec_bp.route('/extend/<int:spec_id>', methods=['GET', 'POST'])
@editor_or_admin_required
def extend_spec(spec_id):
spec = TempSpec.query.get_or_404(spec_id)
if request.method == 'POST':
# ... (原有的檔案上傳、日期更新等邏輯不變) ...
# ===== START: 新增郵件通知邏輯 =====
recipients_str = request.form.get('recipients')
if recipients_str:
recipients = [email.strip() for email in recipients_str.split(',')]
subject = f"[暫規通知] 規範 '{spec.spec_code}' 已展延"
body = f"""
<html><body>
<p>您好,</p>
<p>暫時規範 <b>{spec.spec_code} - {spec.title}</b> 已成功展延。</p>
<p>新的結束日期為: <b>{spec.end_date.strftime('%Y-%m-%d')}</b></p>
<p>詳細資訊請登入系統查看。</p>
</body></html>
"""
send_email(recipients, subject, body)
# ===== END: 新增郵件通知邏輯 =====
db.session.commit()
flash(f"規範 '{spec.spec_code}' 已成功展延!", 'success')
return redirect(url_for('temp_spec.spec_list'))
default_new_end_date = spec.end_date + timedelta(days=30)
return render_template('extend_spec.html', spec=spec, default_new_end_date=default_new_end_date)
B.3. 針對 terminate_spec 重複 B.1 與 B.2 的步驟
對 templates/terminate_spec.html 和 routes/temp_spec.py 中的 terminate_spec 函式執行相同的修改,調整郵件的 subject 和 body 以符合「終止」的情境。
5. 模組 C自動化排程提醒 (前瞻性提醒)
此模組將為系統引入大腦,使其能夠主動進行管理。
C.1. 安裝與設定排程器
安裝套件:
Bash
pip install Flask-APScheduler
並將 Flask-APScheduler 新增到 requirements.txt 檔案中。
在 app.py 中初始化排程器:
Python
# In: app.py
from flask_apscheduler import APScheduler # <-- 新增
# ...
app = Flask(__name__)
app.config.from_object('config.Config')
# ===== START: 初始化排程器 =====
scheduler = APScheduler()
scheduler.init_app(app)
scheduler.start()
# ===== END: 初始化排程器 =====
db.init_app(app)
# ...
C.2. 建立排程任務 (tasks.py)
為了保持程式碼的模組化,我們建立一個新檔案來存放排程任務。
建立新檔案 tasks.py:
Python
# 檔案位置: beabigegg/temp_spec_system_v3/TEMP_spec_system_V3-b9557250a410cf778a51ece25ffe28543f494ffb/tasks.py (新檔案)
from datetime import date, timedelta
from models import TempSpec
from utils import send_email
from ldap_utils import get_ldap_group_members
def check_expiring_specs(app):
"""
每日執行的排程任務:檢查即將到期的暫規並發送提醒郵件。
"""
with app.app_context():
print("Running scheduled task: Checking for expiring specs...")
today = date.today()
seven_days_later = today + timedelta(days=7)
three_days_later = today + timedelta(days=3)
# 找出 7 天後 和 3 天後到期的暫規
expiring_soon = TempSpec.query.filter(
TempSpec.status == 'active',
TempSpec.end_date.in_([seven_days_later, three_days_later])
).all()
if not expiring_soon:
print("No specs expiring in 3 or 7 days.")
return
# **重要**: 定義預設的通知對象,例如某個管理群組
# 您需要將 'TempSpec_Admins' 替換為實際的 AD 群組名稱
default_recipients = get_ldap_group_members('TempSpec_Admins')
if not default_recipients:
print("Warning: Could not find default recipients in AD group 'TempSpec_Admins'.")
return
for spec in expiring_soon:
remaining_days = (spec.end_date - today).days
# 組合通知郵件
subject = f"[暫規到期提醒] 規範 '{spec.spec_code}' 將於 {remaining_days} 天後到期"
body = f"""
<html><body>
<p>您好,</p>
<p>此為自動提醒郵件。</p>
<p>暫時規範 <b>{spec.spec_code} - {spec.title}</b> 即將到期。</p>
<p><b>結束日期: {spec.end_date.strftime('%Y-%m-%d')} (剩餘 {remaining_days} 天)</b></p>
<p>申請人: {spec.applicant}</p>
<p>請及時處理,如需展延請登入系統操作。</p>
</body></html>
"""
# 發送郵件給預設群組
send_email(default_recipients, subject, body)
print(f"Sent expiry reminder for spec {spec.spec_code} to {len(default_recipients)} recipients.")
C.3. 在 app.py 中註冊與啟動任務
最後,我們需要告訴排程器有這個任務,並設定它每天執行。
Python
# In: app.py
# ... (在 from ... import ... 之後)
from tasks import check_expiring_specs
# ... (在 scheduler.start() 之後)
# 註冊排程任務:每天凌晨 2:00 執行一次
@scheduler.task('cron', id='check_expiring_specs_job', hour=2, minute=0)
def scheduled_job():
check_expiring_specs(app)
# ... (Flask app 的其他部分)