1089 lines
31 KiB
Markdown
1089 lines
31 KiB
Markdown
# PANJIT To-Do System - 開發者最佳實踐指南
|
||
|
||
> **⚠️ 重要提醒**:本文件包含敏感的系統配置和最佳實踐資訊,僅供開發團隊內部使用。
|
||
> 此文件已在 .gitignore 中排除,請勿提交至版本控制系統。
|
||
|
||
## 🎯 文件目的
|
||
|
||
本文件記錄在開發 PANJIT To-Do System 過程中遇到的技術難點及最佳解決方案,包含前後端整合、資料庫設計、LDAP整合、郵件系統等關鍵技術決策,避免後續開發者重複踩坑。
|
||
|
||
---
|
||
|
||
## 🔐 LDAP/Active Directory 整合最佳實踐
|
||
|
||
### 1. LDAP 連接配置
|
||
|
||
**關鍵發現**:LDAP 連接的穩定性很大程度取決於正確的配置參數組合。
|
||
|
||
#### 正確的配置模式
|
||
|
||
```python
|
||
# config.py - 推薦配置
|
||
LDAP_SERVER = "ldap://dc.company.com" # 或使用 IP
|
||
LDAP_PORT = 389 # 標準 LDAP port
|
||
LDAP_USE_SSL = False # 內網環境通常不需要 SSL
|
||
LDAP_SEARCH_BASE = "DC=company,DC=com"
|
||
LDAP_BIND_USER_DN = "CN=ServiceAccount,OU=ServiceAccounts,DC=company,DC=com"
|
||
LDAP_USER_LOGIN_ATTR = "userPrincipalName" # AD 環境必須使用此屬性
|
||
```
|
||
|
||
#### 常見錯誤及解決方案
|
||
|
||
**錯誤 1**:使用 `sAMAccountName` 作為登入屬性
|
||
```python
|
||
# ❌ 錯誤做法
|
||
LDAP_USER_LOGIN_ATTR = "sAMAccountName"
|
||
|
||
# ✅ 正確做法
|
||
LDAP_USER_LOGIN_ATTR = "userPrincipalName"
|
||
```
|
||
|
||
**錯誤 2**:服務帳號權限不足
|
||
```python
|
||
# 服務帳號至少需要以下權限:
|
||
# - Read permission on the search base
|
||
# - List Contents permission
|
||
# - Read All Properties permission
|
||
```
|
||
|
||
### 2. LDAP 搜尋最佳化
|
||
|
||
**關鍵發現**:正確的搜尋篩選器可以大幅提升效能並避免權限問題。
|
||
|
||
#### 用戶搜尋最佳實踐
|
||
|
||
```python
|
||
# ldap_utils.py - 優化後的搜尋篩選器
|
||
def search_ldap_principals(search_term):
|
||
# 多屬性搜尋,提高命中率
|
||
search_filter = f"""
|
||
(&
|
||
(objectClass=person)
|
||
(objectCategory=person)
|
||
(!(userAccountControl:1.2.840.113556.1.4.803:=2)) # 排除已停用帳號
|
||
(|
|
||
(displayName=*{search_term}*)
|
||
(mail=*{search_term}*)
|
||
(sAMAccountName=*{search_term}*)
|
||
(userPrincipalName=*{search_term}*)
|
||
)
|
||
)
|
||
"""
|
||
```
|
||
|
||
**關鍵技巧**:
|
||
1. 使用 `objectCategory=person` 而不是只用 `objectClass=user`
|
||
2. 排除停用帳號避免無效結果
|
||
3. 多屬性搜尋提高使用者體驗
|
||
4. 限制搜尋結果數量避免效能問題
|
||
|
||
#### 群組搜尋最佳實踐
|
||
|
||
```python
|
||
# 同時支援 AD 群組和 OU
|
||
def get_ldap_group_members(group_name):
|
||
# 先嘗試搜尋 AD 群組
|
||
group_filter = f"(&(objectClass=group)(cn={group_name}))"
|
||
|
||
# 如果找不到群組,嘗試搜尋 OU
|
||
if not found:
|
||
ou_filter = f"(&(objectClass=organizationalUnit)(name=*{group_name}*))"
|
||
```
|
||
|
||
### 3. LDAP 連接穩定性
|
||
|
||
**關鍵發現**:連接池和重試機制對生產環境至關重要。
|
||
|
||
```python
|
||
# ldap_utils.py - 連接重試機制
|
||
def create_ldap_connection(retries=3):
|
||
for attempt in range(retries):
|
||
try:
|
||
server = Server(ldap_server, port=ldap_port, use_ssl=use_ssl)
|
||
conn = Connection(server, user=bind_dn, password=bind_password, auto_bind=True)
|
||
return conn
|
||
except Exception as e:
|
||
if attempt == retries - 1:
|
||
raise e
|
||
time.sleep(1) # 短暫等待後重試
|
||
```
|
||
|
||
---
|
||
|
||
## 📧 SMTP 郵件系統最佳實踐
|
||
|
||
### 1. 多種 SMTP 配置支援
|
||
|
||
**關鍵發現**:企業環境中可能遇到多種 SMTP 配置需求,系統必須具備彈性。
|
||
|
||
#### 配置架構設計
|
||
|
||
```python
|
||
# config.py - 彈性 SMTP 配置
|
||
class Config:
|
||
SMTP_SERVER = os.getenv('SMTP_SERVER', 'mail.company.com')
|
||
SMTP_PORT = int(os.getenv('SMTP_PORT', 25))
|
||
SMTP_USE_TLS = os.getenv('SMTP_USE_TLS', 'false').lower() in ['true', '1', 't']
|
||
SMTP_USE_SSL = os.getenv('SMTP_USE_SSL', 'false').lower() in ['true', '1', 't']
|
||
SMTP_AUTH_REQUIRED = os.getenv('SMTP_AUTH_REQUIRED', 'false').lower() in ['true', '1', 't']
|
||
SMTP_SENDER_EMAIL = os.getenv('SMTP_SENDER_EMAIL', 'temp-spec-system@company.com')
|
||
SMTP_SENDER_PASSWORD = os.getenv('SMTP_SENDER_PASSWORD', '')
|
||
```
|
||
|
||
#### 智能連接邏輯
|
||
|
||
```python
|
||
# utils.py - 智能 SMTP 連接
|
||
def send_email(to_addrs, subject, body):
|
||
# 根據 port 和配置自動選擇連接方式
|
||
if use_ssl and smtp_port == 465:
|
||
server = smtplib.SMTP_SSL(smtp_server, smtp_port)
|
||
else:
|
||
server = smtplib.SMTP(smtp_server, smtp_port)
|
||
if use_tls and smtp_port == 587:
|
||
server.starttls()
|
||
|
||
# 只在需要認證時才登入
|
||
if auth_required and sender_password:
|
||
server.login(sender_email, sender_password)
|
||
```
|
||
|
||
### 2. 郵件發送可靠性
|
||
|
||
**關鍵發現**:詳細的日誌和錯誤處理對於診斷郵件問題至關重要。
|
||
|
||
```python
|
||
# utils.py - 完整的錯誤處理
|
||
def send_email(to_addrs, subject, body):
|
||
try:
|
||
# ... 發送邏輯 ...
|
||
result = server.sendmail(sender_email, to_addrs, msg.as_string())
|
||
|
||
# 檢查發送結果
|
||
if result:
|
||
# 某些收件者失敗
|
||
print(f"[EMAIL WARNING] 部分收件者發送失敗: {result}")
|
||
else:
|
||
print(f"[EMAIL SUCCESS] 郵件成功發送至: {', '.join(to_addrs)}")
|
||
|
||
return True
|
||
|
||
except smtplib.SMTPAuthenticationError as e:
|
||
print(f"[EMAIL ERROR] SMTP 認證失敗: {e}")
|
||
return False
|
||
except smtplib.SMTPConnectError as e:
|
||
print(f"[EMAIL ERROR] SMTP 連接失敗: {e}")
|
||
return False
|
||
# ... 其他異常處理 ...
|
||
```
|
||
|
||
### 3. 郵件內容最佳化
|
||
|
||
**關鍵發現**:HTML 格式郵件必須考慮各種郵件客戶端的相容性。
|
||
|
||
```python
|
||
# 推薦的 HTML 郵件格式
|
||
def create_email_body(spec, action):
|
||
body = f"""
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<style>
|
||
body {{ font-family: Arial, sans-serif; }}
|
||
.header {{ color: #2c3e50; border-bottom: 2px solid #3498db; }}
|
||
.content {{ margin: 20px 0; }}
|
||
.highlight {{ background-color: #f8f9fa; padding: 10px; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<h2>[暫規通知] 規範 '{spec.spec_code}' 已{action}</h2>
|
||
</div>
|
||
<div class="content">
|
||
<p>您好,</p>
|
||
<!-- 內容... -->
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
return body
|
||
```
|
||
|
||
---
|
||
|
||
## 🗄️ 資料庫設計最佳實踐
|
||
|
||
### 1. 資料庫遷移策略
|
||
|
||
**關鍵發現**:平滑的資料庫升級對於生產環境至關重要。
|
||
|
||
#### 遷移腳本模板
|
||
|
||
```python
|
||
# migrate_*.py - 標準遷移腳本結構
|
||
def migrate_database():
|
||
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 = 'table_name' AND COLUMN_NAME = 'new_column'
|
||
AND TABLE_SCHEMA = DATABASE()
|
||
"""))
|
||
|
||
if result.fetchone():
|
||
print("✓ 遷移已完成,無需重複執行")
|
||
return True
|
||
|
||
# 執行遷移
|
||
conn.execute(text("ALTER TABLE table_name ADD COLUMN new_column TYPE"))
|
||
conn.commit()
|
||
|
||
# 驗證遷移結果
|
||
# ...
|
||
|
||
except Exception as e:
|
||
print(f"✗ 遷移失敗:{str(e)}")
|
||
return False
|
||
```
|
||
|
||
### 2. 資料模型設計
|
||
|
||
**關鍵發現**:適當的索引和關聯設計可以大幅提升查詢效能。
|
||
|
||
```python
|
||
# models.py - 最佳實踐
|
||
class TempSpec(db.Model):
|
||
__tablename__ = 'ts_temp_spec'
|
||
|
||
# 主鍵
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
|
||
# 業務鍵,建立索引
|
||
spec_code = db.Column(db.String(20), nullable=False, index=True)
|
||
|
||
# 常用查詢欄位,建立索引
|
||
status = db.Column(db.Enum(...), nullable=False, index=True)
|
||
end_date = db.Column(db.Date, index=True) # 用於到期查詢
|
||
|
||
# 新功能擴展欄位
|
||
notification_emails = db.Column(db.Text, nullable=True)
|
||
|
||
# 正確的關聯設置
|
||
uploads = db.relationship('Upload', back_populates='spec',
|
||
cascade='all, delete-orphan')
|
||
```
|
||
|
||
---
|
||
|
||
## 🔄 Flask 應用架構最佳實踐
|
||
|
||
### 1. 藍圖(Blueprint)組織
|
||
|
||
**關鍵發現**:良好的模組分離有助於維護和擴展。
|
||
|
||
```python
|
||
# 推薦的路由組織結構
|
||
routes/
|
||
├── __init__.py # 藍圖註冊
|
||
├── auth.py # 認證相關
|
||
├── api.py # API 介面
|
||
├── temp_spec.py # 核心業務邏輯
|
||
├── admin.py # 管理功能
|
||
└── upload.py # 檔案處理
|
||
```
|
||
|
||
### 2. 錯誤處理策略
|
||
|
||
```python
|
||
# app.py - 全局錯誤處理
|
||
@app.errorhandler(403)
|
||
def forbidden(error):
|
||
return render_template('403.html'), 403
|
||
|
||
@app.errorhandler(404)
|
||
def not_found(error):
|
||
return render_template('404.html'), 404
|
||
|
||
@app.errorhandler(500)
|
||
def internal_error(error):
|
||
db.session.rollback()
|
||
return render_template('500.html'), 500
|
||
```
|
||
|
||
### 3. 配置管理
|
||
|
||
```python
|
||
# config.py - 環境配置分離
|
||
class DevelopmentConfig(Config):
|
||
DEBUG = True
|
||
TESTING = False
|
||
|
||
class ProductionConfig(Config):
|
||
DEBUG = False
|
||
TESTING = False
|
||
# 生產環境特定設定
|
||
|
||
class TestingConfig(Config):
|
||
TESTING = True
|
||
# 測試環境設定
|
||
```
|
||
|
||
---
|
||
|
||
## 🏗️ 前端整合最佳實踐
|
||
|
||
### 1. ONLYOFFICE 整合要點
|
||
|
||
**關鍵發現**:Docker 環境下的網路配置是最大的挑戰。
|
||
|
||
```python
|
||
# routes/temp_spec.py - URL 修正邏輯
|
||
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)
|
||
|
||
# Docker 環境 URL 修正
|
||
if '127.0.0.1' in doc_url or 'localhost' in doc_url:
|
||
doc_url = doc_url.replace('127.0.0.1', 'host.docker.internal')
|
||
doc_url = doc_url.replace('localhost', 'host.docker.internal')
|
||
callback_url = callback_url.replace('127.0.0.1', 'host.docker.internal')
|
||
callback_url = callback_url.replace('localhost', 'host.docker.internal')
|
||
```
|
||
|
||
### 2. 前端元件最佳化
|
||
|
||
**關鍵發現**:Tom Select 元件需要正確配置才能提供良好的使用者體驗。
|
||
|
||
```javascript
|
||
// 推薦的 Tom Select 配置
|
||
const recipientSelect = new TomSelect('#recipients', {
|
||
valueField: 'value',
|
||
labelField: 'text',
|
||
searchField: 'text',
|
||
placeholder: '請輸入姓名或 Email 來搜尋...',
|
||
plugins: ['remove_button'],
|
||
maxItems: null,
|
||
create: false,
|
||
load: function(query, callback) {
|
||
if (!query || query.length < 2) {
|
||
callback();
|
||
return;
|
||
}
|
||
|
||
// 實作搜尋邏輯...
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 部署最佳實踐
|
||
|
||
### 1. Docker 配置優化
|
||
|
||
```yaml
|
||
# docker-compose.yml - 生產環境配置
|
||
version: '3.8'
|
||
|
||
services:
|
||
app:
|
||
build: .
|
||
environment:
|
||
- FLASK_ENV=production
|
||
- PYTHONUNBUFFERED=1
|
||
volumes:
|
||
- ./uploads:/app/uploads
|
||
- ./logs:/app/logs
|
||
restart: unless-stopped
|
||
|
||
mysql:
|
||
image: mysql:8.0
|
||
environment:
|
||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||
MYSQL_DATABASE: ${DB_NAME}
|
||
volumes:
|
||
- mysql_data:/var/lib/mysql
|
||
restart: unless-stopped
|
||
|
||
volumes:
|
||
mysql_data:
|
||
```
|
||
|
||
### 2. 日誌管理
|
||
|
||
```python
|
||
# app.py - 生產環境日誌配置
|
||
if not app.debug:
|
||
if not os.path.exists('logs'):
|
||
os.mkdir('logs')
|
||
|
||
file_handler = RotatingFileHandler('logs/tempspec.log',
|
||
maxBytes=10240000, backupCount=10)
|
||
file_handler.setFormatter(logging.Formatter(
|
||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
|
||
))
|
||
file_handler.setLevel(logging.INFO)
|
||
app.logger.addHandler(file_handler)
|
||
```
|
||
|
||
### 3. 安全性配置
|
||
|
||
```python
|
||
# 推薦的安全標頭設置
|
||
@app.after_request
|
||
def after_request(response):
|
||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||
response.headers['X-Frame-Options'] = 'DENY'
|
||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||
return response
|
||
```
|
||
|
||
---
|
||
|
||
## 🐛 除錯與監控
|
||
|
||
### 1. 開發階段除錯
|
||
|
||
```python
|
||
# 推薦的除錯配置
|
||
DEBUG_LDAP = os.getenv('DEBUG_LDAP', 'false').lower() == 'true'
|
||
DEBUG_EMAIL = os.getenv('DEBUG_EMAIL', 'false').lower() == 'true'
|
||
DEBUG_DATABASE = os.getenv('DEBUG_DATABASE', 'false').lower() == 'true'
|
||
|
||
def debug_log(category, message):
|
||
if category == 'ldap' and DEBUG_LDAP:
|
||
print(f"[LDAP DEBUG] {message}")
|
||
elif category == 'email' and DEBUG_EMAIL:
|
||
print(f"[EMAIL DEBUG] {message}")
|
||
# ...
|
||
```
|
||
|
||
### 2. 生產環境監控
|
||
|
||
```python
|
||
# tasks.py - 健康檢查任務
|
||
@scheduler.task('cron', id='health_check', hour='*/1')
|
||
def health_check():
|
||
try:
|
||
# 檢查資料庫連接
|
||
db.session.execute(text('SELECT 1'))
|
||
|
||
# 檢查 LDAP 連接
|
||
test_ldap_connection()
|
||
|
||
# 檢查 SMTP 連接
|
||
test_smtp_connection()
|
||
|
||
app.logger.info("Health check passed")
|
||
except Exception as e:
|
||
app.logger.error(f"Health check failed: {e}")
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 效能優化要點
|
||
|
||
### 1. 資料庫查詢優化
|
||
|
||
```python
|
||
# 推薦的查詢模式
|
||
def get_active_specs_expiring_soon():
|
||
return TempSpec.query.filter(
|
||
TempSpec.status == 'active',
|
||
TempSpec.end_date <= datetime.now().date() + timedelta(days=7)
|
||
).options(
|
||
joinedload(TempSpec.uploads) # 預載關聯資料
|
||
).all()
|
||
```
|
||
|
||
### 2. 快取策略
|
||
|
||
```python
|
||
# 推薦使用 Flask-Caching
|
||
from flask_caching import Cache
|
||
|
||
cache = Cache(app, config={'CACHE_TYPE': 'simple'})
|
||
|
||
@cache.memoize(timeout=300) # 5分鐘快取
|
||
def get_ldap_group_members(group_name):
|
||
# LDAP 查詢邏輯...
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 維護與升級
|
||
|
||
### 1. 版本控制策略
|
||
|
||
```bash
|
||
# 推薦的版本標籤格式
|
||
git tag v3.2.0-rc1 # 發布候選版本
|
||
git tag v3.2.0 # 正式版本
|
||
git tag v3.2.1 # 修正版本
|
||
```
|
||
|
||
### 2. 備份策略
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# backup.sh - 定期備份腳本
|
||
DATE=$(date +%Y%m%d_%H%M%S)
|
||
|
||
# 資料庫備份
|
||
mysqldump -u $DB_USER -p$DB_PASSWORD $DB_NAME > backup_${DATE}.sql
|
||
|
||
# 檔案備份
|
||
tar -czf uploads_${DATE}.tar.gz uploads/
|
||
```
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## 🎨 前端開發最佳實踐
|
||
|
||
### 1. Next.js + TypeScript 架構
|
||
|
||
**關鍵發現**:App Router 架構提供更好的開發體驗和效能。
|
||
|
||
#### 推薦的專案結構
|
||
```
|
||
frontend/src/
|
||
├── app/ # Next.js 13+ App Router
|
||
│ ├── (auth)/ # Route Groups
|
||
│ ├── dashboard/ # Dashboard 路由
|
||
│ ├── todos/ # 待辦事項頁面
|
||
│ ├── layout.tsx # 根版面
|
||
│ └── page.tsx # 首頁
|
||
├── components/ # React 組件
|
||
│ ├── layout/ # 版面組件
|
||
│ ├── todos/ # 待辦事項組件
|
||
│ └── ui/ # 通用 UI 組件
|
||
├── lib/ # 工具函數
|
||
├── store/ # Redux 狀態管理
|
||
├── types/ # TypeScript 類型定義
|
||
└── providers/ # Context Providers
|
||
```
|
||
|
||
#### TypeScript 最佳實踐
|
||
```typescript
|
||
// types/todo.ts - 完整的類型定義
|
||
export interface TodoItem {
|
||
id: string;
|
||
title: string;
|
||
description?: string;
|
||
status: 'NEW' | 'DOING' | 'BLOCKED' | 'DONE';
|
||
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
||
due_date?: string;
|
||
created_at: string;
|
||
creator_ad: string;
|
||
starred: boolean;
|
||
is_public: boolean;
|
||
tags: string[];
|
||
responsible_users: string[];
|
||
followers: string[];
|
||
}
|
||
|
||
// API 回應類型
|
||
export interface ApiResponse<T> {
|
||
data: T;
|
||
message?: string;
|
||
total?: number;
|
||
}
|
||
```
|
||
|
||
### 2. Material-UI 整合
|
||
|
||
**關鍵發現**:主題系統和組件客制化是關鍵。
|
||
|
||
#### 主題設計
|
||
```typescript
|
||
// providers/ThemeProvider.tsx
|
||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||
|
||
const lightTheme = createTheme({
|
||
palette: {
|
||
mode: 'light',
|
||
primary: {
|
||
main: '#3b82f6', // 藍色主題
|
||
},
|
||
secondary: {
|
||
main: '#10b981', // 綠色輔助
|
||
},
|
||
},
|
||
typography: {
|
||
fontFamily: '"Noto Sans TC", "Roboto", sans-serif',
|
||
},
|
||
});
|
||
|
||
const darkTheme = createTheme({
|
||
palette: {
|
||
mode: 'dark',
|
||
primary: {
|
||
main: '#60a5fa',
|
||
},
|
||
background: {
|
||
default: '#0f172a',
|
||
paper: '#1e293b',
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
#### 響應式設計
|
||
```typescript
|
||
// hooks/useResponsive.ts
|
||
import { useTheme, useMediaQuery } from '@mui/material';
|
||
|
||
export const useResponsive = () => {
|
||
const theme = useTheme();
|
||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||
const isTablet = useMediaQuery(theme.breakpoints.between('md', 'lg'));
|
||
const isDesktop = useMediaQuery(theme.breakpoints.up('lg'));
|
||
|
||
return { isMobile, isTablet, isDesktop };
|
||
};
|
||
```
|
||
|
||
### 3. 狀態管理策略
|
||
|
||
**關鍵發現**:Redux Toolkit + React Query 組合提供最佳的開發體驗。
|
||
|
||
#### Redux Store 設計
|
||
```typescript
|
||
// store/todoSlice.ts
|
||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||
|
||
interface TodoState {
|
||
filterMode: 'all' | 'created' | 'responsible' | 'following';
|
||
appliedFilters: TodoFilters;
|
||
selectedItems: string[];
|
||
viewMode: 'list' | 'card' | 'calendar';
|
||
}
|
||
|
||
const todoSlice = createSlice({
|
||
name: 'todo',
|
||
initialState,
|
||
reducers: {
|
||
setFilterMode: (state, action: PayloadAction<FilterMode>) => {
|
||
state.filterMode = action.payload;
|
||
},
|
||
toggleSelectItem: (state, action: PayloadAction<string>) => {
|
||
const id = action.payload;
|
||
if (state.selectedItems.includes(id)) {
|
||
state.selectedItems = state.selectedItems.filter(item => item !== id);
|
||
} else {
|
||
state.selectedItems.push(id);
|
||
}
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
#### React Query 集成
|
||
```typescript
|
||
// lib/api/todos.ts
|
||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||
|
||
export const useTodos = (filters: TodoFilters) => {
|
||
return useQuery({
|
||
queryKey: ['todos', filters],
|
||
queryFn: () => fetchTodos(filters),
|
||
staleTime: 30000, // 30秒內數據視為新鮮
|
||
});
|
||
};
|
||
|
||
export const useCreateTodo = () => {
|
||
const queryClient = useQueryClient();
|
||
|
||
return useMutation({
|
||
mutationFn: createTodo,
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
||
toast.success('待辦事項建立成功!');
|
||
},
|
||
onError: (error) => {
|
||
toast.error(error.message || '建立失敗');
|
||
},
|
||
});
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 🔄 後端 API 設計最佳實踐
|
||
|
||
### 1. Flask 應用架構
|
||
|
||
**關鍵發現**:Blueprint 和工廠模式提供最佳的可維護性。
|
||
|
||
#### 應用程式工廠模式
|
||
```python
|
||
# app.py - 推薦的應用程式結構
|
||
def create_app(config_name=None):
|
||
if config_name is None:
|
||
config_name = os.environ.get('FLASK_ENV', 'development')
|
||
|
||
app = Flask(__name__)
|
||
app.config.from_object(config[config_name])
|
||
|
||
# 初始化擴展
|
||
db.init_app(app)
|
||
migrate.init_app(app, db)
|
||
jwt.init_app(app)
|
||
mail.init_app(app)
|
||
CORS(app, origins=app.config['CORS_ORIGINS'])
|
||
|
||
# 註冊 Blueprint
|
||
from routes import register_blueprints
|
||
register_blueprints(app)
|
||
|
||
# 設定錯誤處理
|
||
setup_error_handlers(app)
|
||
|
||
return app
|
||
```
|
||
|
||
#### RESTful API 設計
|
||
```python
|
||
# routes/todos.py - 標準化的 API 結構
|
||
@todos_bp.route('', methods=['GET'])
|
||
@jwt_required()
|
||
def get_todos():
|
||
try:
|
||
# 參數驗證
|
||
page = request.args.get('page', 1, type=int)
|
||
per_page = min(request.args.get('per_page', 20, type=int), 100)
|
||
|
||
# 業務邏輯
|
||
todos, total = todo_service.get_user_todos(
|
||
user_ad=get_jwt_identity(),
|
||
page=page,
|
||
per_page=per_page,
|
||
filters=request.args
|
||
)
|
||
|
||
# 統一回應格式
|
||
return jsonify({
|
||
'data': [todo.to_dict() for todo in todos],
|
||
'pagination': {
|
||
'page': page,
|
||
'per_page': per_page,
|
||
'total': total,
|
||
'pages': (total + per_page - 1) // per_page
|
||
}
|
||
}), 200
|
||
|
||
except ValidationError as e:
|
||
return jsonify({'error': str(e)}), 400
|
||
except Exception as e:
|
||
logger.error(f"Error getting todos: {str(e)}")
|
||
return jsonify({'error': 'Internal server error'}), 500
|
||
```
|
||
|
||
### 2. 資料驗證策略
|
||
|
||
**關鍵發現**:輸入驗證和序列化分離提供更好的可維護性。
|
||
|
||
```python
|
||
# validators/todo.py
|
||
from marshmallow import Schema, fields, validate
|
||
|
||
class CreateTodoSchema(Schema):
|
||
title = fields.Str(required=True, validate=validate.Length(min=1, max=200))
|
||
description = fields.Str(missing=None, validate=validate.Length(max=2000))
|
||
status = fields.Str(missing='NEW', validate=validate.OneOf(['NEW', 'DOING', 'BLOCKED', 'DONE']))
|
||
priority = fields.Str(missing='MEDIUM', validate=validate.OneOf(['LOW', 'MEDIUM', 'HIGH', 'URGENT']))
|
||
due_date = fields.Date(missing=None)
|
||
responsible_users = fields.List(fields.Str(), missing=list)
|
||
followers = fields.List(fields.Str(), missing=list)
|
||
tags = fields.List(fields.Str(), missing=list)
|
||
is_public = fields.Bool(missing=False)
|
||
starred = fields.Bool(missing=False)
|
||
|
||
# 使用裝飾器進行驗證
|
||
def validate_json(schema):
|
||
def decorator(f):
|
||
def wrapper(*args, **kwargs):
|
||
try:
|
||
data = schema.load(request.json or {})
|
||
return f(data, *args, **kwargs)
|
||
except ValidationError as e:
|
||
return jsonify({'error': e.messages}), 400
|
||
return wrapper
|
||
return decorator
|
||
|
||
@todos_bp.route('', methods=['POST'])
|
||
@jwt_required()
|
||
@validate_json(CreateTodoSchema())
|
||
def create_todo(data):
|
||
# 已驗證的資料直接使用
|
||
todo = todo_service.create_todo(
|
||
creator_ad=get_jwt_identity(),
|
||
**data
|
||
)
|
||
return jsonify({'data': todo.to_dict()}), 201
|
||
```
|
||
|
||
### 3. 服務層架構
|
||
|
||
**關鍵發現**:業務邏輯分離到服務層提供更好的測試性。
|
||
|
||
```python
|
||
# services/todo_service.py
|
||
class TodoService:
|
||
def __init__(self):
|
||
self.logger = get_logger(__name__)
|
||
|
||
def create_todo(self, creator_ad: str, **kwargs) -> TodoItem:
|
||
"""建立新的待辦事項"""
|
||
try:
|
||
# 驗證負責人帳號
|
||
if kwargs.get('responsible_users'):
|
||
valid_users = validate_ad_accounts(kwargs['responsible_users'])
|
||
if len(valid_users) != len(kwargs['responsible_users']):
|
||
invalid_users = set(kwargs['responsible_users']) - set(valid_users.keys())
|
||
raise ValidationError(f"無效的負責人帳號: {', '.join(invalid_users)}")
|
||
|
||
# 建立待辦事項
|
||
todo = TodoItem(
|
||
title=kwargs['title'],
|
||
description=kwargs.get('description'),
|
||
status=kwargs.get('status', 'NEW'),
|
||
priority=kwargs.get('priority', 'MEDIUM'),
|
||
due_date=kwargs.get('due_date'),
|
||
creator_ad=creator_ad,
|
||
is_public=kwargs.get('is_public', False),
|
||
starred=kwargs.get('starred', False),
|
||
tags=kwargs.get('tags', [])
|
||
)
|
||
|
||
# 設定使用者資訊
|
||
user_info = get_user_info(creator_ad)
|
||
if user_info:
|
||
todo.creator_display_name = user_info['display_name']
|
||
todo.creator_email = user_info['email']
|
||
|
||
db.session.add(todo)
|
||
db.session.flush() # 取得 ID
|
||
|
||
# 新增負責人和追蹤者
|
||
self._add_users_to_todo(todo.id, kwargs.get('responsible_users', []), 'responsible')
|
||
self._add_users_to_todo(todo.id, kwargs.get('followers', []), 'follower')
|
||
|
||
db.session.commit()
|
||
|
||
# 發送通知
|
||
self._send_assignment_notifications(todo)
|
||
|
||
# 記錄稽核日誌
|
||
self._log_audit(todo.id, creator_ad, 'CREATE', '建立待辦事項')
|
||
|
||
return todo
|
||
|
||
except Exception as e:
|
||
db.session.rollback()
|
||
self.logger.error(f"Error creating todo: {str(e)}")
|
||
raise
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 Excel 處理最佳實踐
|
||
|
||
### 1. 安全的檔案處理
|
||
|
||
**關鍵發現**:檔案上傳和處理需要多層驗證。
|
||
|
||
```python
|
||
# utils/excel_utils.py
|
||
import pandas as pd
|
||
from openpyxl import load_workbook
|
||
import tempfile
|
||
import os
|
||
|
||
class ExcelProcessor:
|
||
ALLOWED_EXTENSIONS = {'.xlsx', '.xls', '.csv'}
|
||
MAX_FILE_SIZE = 16 * 1024 * 1024 # 16MB
|
||
MAX_ROWS = 10000
|
||
|
||
def validate_file(self, file):
|
||
"""檔案安全驗證"""
|
||
# 檔案大小檢查
|
||
if len(file.read()) > self.MAX_FILE_SIZE:
|
||
raise ValidationError("檔案大小超過限制 (16MB)")
|
||
|
||
file.seek(0) # 重置檔案指針
|
||
|
||
# 檔案類型檢查
|
||
filename = secure_filename(file.filename)
|
||
if not any(filename.lower().endswith(ext) for ext in self.ALLOWED_EXTENSIONS):
|
||
raise ValidationError("不支援的檔案格式")
|
||
|
||
return filename
|
||
|
||
def parse_excel_file(self, file):
|
||
"""安全的 Excel 解析"""
|
||
try:
|
||
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
||
file.save(temp_file.name)
|
||
|
||
# 使用 pandas 讀取檔案
|
||
if temp_file.name.endswith('.csv'):
|
||
df = pd.read_csv(temp_file.name, encoding='utf-8')
|
||
else:
|
||
df = pd.read_excel(temp_file.name)
|
||
|
||
# 行數限制
|
||
if len(df) > self.MAX_ROWS:
|
||
raise ValidationError(f"資料列數超過限制 ({self.MAX_ROWS})")
|
||
|
||
return self.validate_and_transform_data(df)
|
||
|
||
finally:
|
||
# 清理暫存檔案
|
||
if 'temp_file' in locals():
|
||
os.unlink(temp_file.name)
|
||
|
||
def validate_and_transform_data(self, df):
|
||
"""資料驗證和轉換"""
|
||
required_columns = ['標題', '負責人']
|
||
missing_columns = [col for col in required_columns if col not in df.columns]
|
||
|
||
if missing_columns:
|
||
raise ValidationError(f"缺少必要欄位: {', '.join(missing_columns)}")
|
||
|
||
validated_data = []
|
||
errors = []
|
||
|
||
for index, row in df.iterrows():
|
||
try:
|
||
todo_data = self.validate_row(row, index + 2) # +2 因為標題行
|
||
validated_data.append(todo_data)
|
||
except ValidationError as e:
|
||
errors.append(f"第 {index + 2} 行: {str(e)}")
|
||
|
||
return {
|
||
'data': validated_data,
|
||
'errors': errors,
|
||
'total': len(df)
|
||
}
|
||
```
|
||
|
||
### 2. 模板生成
|
||
|
||
**關鍵發現**:動態模板生成提供更好的使用者體驗。
|
||
|
||
```python
|
||
# routes/excel.py
|
||
@excel_bp.route('/template', methods=['GET'])
|
||
@jwt_required()
|
||
def download_template():
|
||
"""生成並下載 Excel 匯入模板"""
|
||
try:
|
||
from openpyxl import Workbook
|
||
from openpyxl.styles import Font, PatternFill, Alignment
|
||
from openpyxl.worksheet.datavalidation import DataValidation
|
||
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.title = "待辦事項匯入模板"
|
||
|
||
# 標題行
|
||
headers = [
|
||
'標題*', '描述', '狀態', '優先級',
|
||
'到期日', '負責人*', '追蹤人員', '標籤', '備註'
|
||
]
|
||
|
||
# 設定標題樣式
|
||
title_font = Font(bold=True, color='FFFFFF')
|
||
title_fill = PatternFill(start_color='366092', end_color='366092', fill_type='solid')
|
||
|
||
for col, header in enumerate(headers, 1):
|
||
cell = ws.cell(row=1, column=col, value=header)
|
||
cell.font = title_font
|
||
cell.fill = title_fill
|
||
cell.alignment = Alignment(horizontal='center')
|
||
|
||
# 資料驗證
|
||
status_validation = DataValidation(
|
||
type="list",
|
||
formula1='"NEW,DOING,BLOCKED,DONE"',
|
||
showErrorMessage=True,
|
||
errorTitle="狀態錯誤",
|
||
error="請選擇: NEW, DOING, BLOCKED, DONE"
|
||
)
|
||
priority_validation = DataValidation(
|
||
type="list",
|
||
formula1='"LOW,MEDIUM,HIGH,URGENT"',
|
||
showErrorMessage=True,
|
||
errorTitle="優先級錯誤",
|
||
error="請選擇: LOW, MEDIUM, HIGH, URGENT"
|
||
)
|
||
|
||
ws.add_data_validation(status_validation)
|
||
ws.add_data_validation(priority_validation)
|
||
status_validation.add(f'C2:C{MAX_ROWS}')
|
||
priority_validation.add(f'D2:D{MAX_ROWS}')
|
||
|
||
# 範例資料
|
||
sample_data = [
|
||
['完成月報撰寫', '包含各部門數據統計和分析', 'NEW', 'HIGH', '2024/01/15', 'user1', 'manager1', '報告,月度', '請於期限內完成'],
|
||
['系統維護檢查', '定期檢查伺服器狀態', 'DOING', 'MEDIUM', '2024/01/20', 'user2', 'user1,user3', 'IT,維護', ''],
|
||
]
|
||
|
||
for row, data in enumerate(sample_data, 2):
|
||
for col, value in enumerate(data, 1):
|
||
ws.cell(row=row, column=col, value=value)
|
||
|
||
# 調整欄寬
|
||
column_widths = [20, 30, 12, 12, 12, 15, 20, 15, 20]
|
||
for col, width in enumerate(column_widths, 1):
|
||
ws.column_dimensions[ws.cell(row=1, column=col).column_letter].width = width
|
||
|
||
# 生成檔案
|
||
from io import BytesIO
|
||
output = BytesIO()
|
||
wb.save(output)
|
||
output.seek(0)
|
||
|
||
return send_file(
|
||
output,
|
||
as_attachment=True,
|
||
download_name=f'todo_import_template_{datetime.now().strftime("%Y%m%d")}.xlsx',
|
||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error generating template: {str(e)}")
|
||
return jsonify({'error': '模板生成失敗'}), 500
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 總結
|
||
|
||
本文件記錄了開發 PANJIT To-Do System 過程中的關鍵技術決策和最佳實踐。這些經驗可以幫助後續開發者:
|
||
|
||
1. **避免常見陷阱**:特別是 LDAP 配置、前後端整合、Excel 處理
|
||
2. **提升開發效率**:使用經過驗證的架構模式和工具鏈
|
||
3. **確保程式碼品質**:遵循 TypeScript、Python 最佳實踐
|
||
4. **提升使用者體驗**:響應式設計、效能優化、錯誤處理
|
||
5. **簡化維護工作**:清晰的架構分離、完整的日誌記錄
|
||
|
||
**重要技術決策總結**:
|
||
- **前端**:Next.js 14 App Router + TypeScript + Material-UI + Redux Toolkit
|
||
- **後端**:Flask + SQLAlchemy + Celery + Redis + JWT
|
||
- **資料庫**:MySQL 8.0 + 適當索引設計
|
||
- **部署**:Docker + Nginx + 健康檢查
|
||
|
||
**重要提醒**:本文件包含敏感資訊,請勿外洩或提交至公開版本控制系統。
|
||
|
||
---
|
||
|
||
*最後更新:2025年1月*
|
||
*文件版本:V2.0*
|
||
*適用系統:PANJIT To-Do System v1.0* |