Files
TODO_list_system/BEST_PRACTICES.md
beabigegg 22a231d78c 5th
2025-09-01 11:17:09 +08:00

31 KiB
Raw Blame History

PANJIT To-Do System - 開發者最佳實踐指南

⚠️ 重要提醒:本文件包含敏感的系統配置和最佳實踐資訊,僅供開發團隊內部使用。 此文件已在 .gitignore 中排除,請勿提交至版本控制系統。

🎯 文件目的

本文件記錄在開發 PANJIT To-Do System 過程中遇到的技術難點及最佳解決方案包含前後端整合、資料庫設計、LDAP整合、郵件系統等關鍵技術決策避免後續開發者重複踩坑。


🔐 LDAP/Active Directory 整合最佳實踐

1. LDAP 連接配置

關鍵發現LDAP 連接的穩定性很大程度取決於正確的配置參數組合。

正確的配置模式

# 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 作為登入屬性

# ❌ 錯誤做法
LDAP_USER_LOGIN_ATTR = "sAMAccountName"

# ✅ 正確做法
LDAP_USER_LOGIN_ATTR = "userPrincipalName"

錯誤 2:服務帳號權限不足

# 服務帳號至少需要以下權限:
# - Read permission on the search base
# - List Contents permission  
# - Read All Properties permission

2. LDAP 搜尋最佳化

關鍵發現:正確的搜尋篩選器可以大幅提升效能並避免權限問題。

用戶搜尋最佳實踐

# 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. 限制搜尋結果數量避免效能問題

群組搜尋最佳實踐

# 同時支援 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 連接穩定性

關鍵發現:連接池和重試機制對生產環境至關重要。

# 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 配置需求,系統必須具備彈性。

配置架構設計

# 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', '')

智能連接邏輯

# 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. 郵件發送可靠性

關鍵發現:詳細的日誌和錯誤處理對於診斷郵件問題至關重要。

# 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 格式郵件必須考慮各種郵件客戶端的相容性。

# 推薦的 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. 資料庫遷移策略

關鍵發現:平滑的資料庫升級對於生產環境至關重要。

遷移腳本模板

# 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. 資料模型設計

關鍵發現:適當的索引和關聯設計可以大幅提升查詢效能。

# 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組織

關鍵發現:良好的模組分離有助於維護和擴展。

# 推薦的路由組織結構
routes/
├── __init__.py          # 藍圖註冊
├── auth.py             # 認證相關
├── api.py              # API 介面
├── temp_spec.py        # 核心業務邏輯
├── admin.py            # 管理功能
└── upload.py           # 檔案處理

2. 錯誤處理策略

# 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. 配置管理

# config.py - 環境配置分離
class DevelopmentConfig(Config):
    DEBUG = True
    TESTING = False

class ProductionConfig(Config):
    DEBUG = False
    TESTING = False
    # 生產環境特定設定

class TestingConfig(Config):
    TESTING = True
    # 測試環境設定

🏗️ 前端整合最佳實踐

1. ONLYOFFICE 整合要點

關鍵發現Docker 環境下的網路配置是最大的挑戰。

# 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 元件需要正確配置才能提供良好的使用者體驗。

// 推薦的 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 配置優化

# 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. 日誌管理

# 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. 安全性配置

# 推薦的安全標頭設置
@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. 開發階段除錯

# 推薦的除錯配置
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. 生產環境監控

# 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. 資料庫查詢優化

# 推薦的查詢模式
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. 快取策略

# 推薦使用 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. 版本控制策略

# 推薦的版本標籤格式
git tag v3.2.0-rc1  # 發布候選版本
git tag v3.2.0      # 正式版本
git tag v3.2.1      # 修正版本

2. 備份策略

#!/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 最佳實踐

// 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 整合

關鍵發現:主題系統和組件客制化是關鍵。

主題設計

// 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',
    },
  },
});

響應式設計

// 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 設計

// 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 集成

// 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 和工廠模式提供最佳的可維護性。

應用程式工廠模式

# 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 設計

# 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. 資料驗證策略

關鍵發現:輸入驗證和序列化分離提供更好的可維護性。

# 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. 服務層架構

關鍵發現:業務邏輯分離到服務層提供更好的測試性。

# 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. 安全的檔案處理

關鍵發現:檔案上傳和處理需要多層驗證。

# 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. 模板生成

關鍵發現:動態模板生成提供更好的使用者體驗。

# 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