204 lines
8.1 KiB
Plaintext
204 lines
8.1 KiB
Plaintext
功能升級計畫:智慧通知模組
|
||
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 的其他部分) |