2025年6月18日 星期三

Django CRM 從零開始:為初學者打造的逐步教學

Django CRM 從零開始:為初學者打造的逐步教學 (重新排版)

嗨,Django 新手們!準備好踏上開發一個實用且功能強大的 CRM (客戶關係管理) 系統的旅程了嗎?Django 是一個高效的 Web 框架,它能讓您快速地建立複雜的資料庫驅動型應用程式。本教學將帶領您從零開始,逐步搭建一個基礎的 CRM 系統,涵蓋專案的核心組件,並引入現代開發實踐。

注意: 本教學文章基於https://github.com/BpsEason/FullStackDjangoCRM.git的概念和結構,但為了提供更簡潔、易於理解的初學者體驗,某些檔案的內容(特別是 HTML 模板中的 JavaScript)已被簡化和優化,以專注於 Django 核心概念的教學。當您完成本教學後,您可以回到我的 GitHub 專案,探索更多進階功能和實戰範例,將其作為您進一步學習和提升的資源。

目標讀者

本教學適合對 Python 有基本了解,並對 Web 開發有興趣的 Django 初學者。

我們將構建什麼?

一個基礎的 CRM 系統,具備以下功能:

  • 客戶管理: 追蹤客戶的基本資訊。
  • 活動追蹤: 記錄與客戶相關的通話、會議等活動。
  • 用戶認證: 確保只有登錄用戶才能訪問系統。
  • 管理介面: 利用 Django Admin 快速管理資料。
  • Docker 化開發環境: 使用 Docker Compose 輕鬆搭建開發環境。

為什麼選擇 Django?

  • 快速開發: Django 提供了許多開箱即用的組件和約定,加速開發流程。
  • ORM: 強大的物件關係映射 (ORM) 讓您無需撰寫原始 SQL 即可操作資料庫。
  • Admin 介面: 自動生成的功能齊全的管理後台,極大地提高了開發效率。
  • 安全性: 內建了多種安全保護機制,如 CSRF、XSS 防禦、密碼哈希等。
  • 可擴展性: 從小型專案到大型複雜應用,Django 都能良好支持。

一、環境準備

在開始之前,請確保您的系統已安裝以下軟體:

  1. Python 3.8+
  2. pip (Python 套件管理器)
  3. Docker Desktop (包含 Docker Engine 和 Docker Compose)
  4. Git

二、專案初始化與結構概覽

我們將從一個空的專案開始。

  1. 創建專案目錄:

    Bash
    mkdir FullStackDjangoCRM
    cd FullStackDjangoCRM
    
  2. 創建 .gitignore 檔案:

    為了避免將不必要的檔案提交到版本控制,創建 .gitignore 檔案。

    檔案內容: FullStackDjangoCRM/.gitignore

    *.pyc
    __pycache__/
    .env
    db.sqlite3
    /staticfiles/
    .DS_Store
    .idea/
    .vscode/
    
  3. 創建 requirements.txt:

    列出所有專案的 Python 依賴。

    檔案內容: FullStackDjangoCRM/requirements.txt

    django==5.1.1
    djangorestframework==3.15.2
    psycopg2-binary==2.9.9
    redis==5.0.8
    celery==5.4.0
    argon2-cffi==23.1.0
    django-environ==0.11.2
    gunicorn==23.0.0
    django-bootstrap5==24.2
    
  4. 創建 Dockerfile:

    用於構建我們的 Django 應用程式 Docker 映像。

    檔案內容: FullStackDjangoCRM/Dockerfile

    Dockerfile
    FROM python:3.11-slim-buster
    
    ENV PYTHONUNBUFFERED 1
    ENV PYTHONDONTWRITEBYTECODE 1
    
    WORKDIR /app
    
    COPY requirements.txt /app/
    
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY . /app/
    
    CMD ["gunicorn", "--bind", "0.0.0.0:8000", "crm.wsgi:application"]
    
  5. 創建 docker-compose.yaml:

    定義我們的多容器開發環境,包括 Django 應用、PostgreSQL 資料庫、Redis 和 Nginx。

    檔案內容: FullStackDjangoCRM/docker-compose.yaml

    YAML
    version: '3.9'
    
    services:
      web:
        build: .
        command: gunicorn --bind 0.0.0.0:8000 crm.wsgi:application
        volumes:
          - .:/app
          - static_volume:/app/staticfiles # 持久化靜態檔案
        ports:
          - "8000:8000"
        environment:
          # 從 .env 文件載入環境變數
          - DEBUG=${DEBUG}
          - SECRET_KEY=${SECRET_KEY}
          - DB_NAME=${DB_NAME}
          - DB_USER=${DB_USER}
          - DB_PASSWORD=${DB_PASSWORD}
          - DB_HOST=${DB_HOST}
          - DB_PORT=${DB_PORT}
          - REDIS_URL=${REDIS_URL}
          - CELERY_BROKER_URL=${CELERY_BROKER_URL}
          - CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
        depends_on:
          - db
          - redis
        networks:
          - crm_network
    
      db:
        image: postgres:16
        environment:
          - POSTGRES_DB=${DB_NAME}
          - POSTGRES_USER=${DB_USER}
          - POSTGRES_PASSWORD=${DB_PASSWORD}
        volumes:
          - postgres_data:/var/lib/postgresql/data # 持久化資料庫資料
        networks:
          - crm_network
    
      redis:
        image: redis:7
        networks:
          - crm_network
    
      celery:
        build: .
        command: celery -A crm worker --loglevel=info # 運行 Celery worker
        volumes:
          - .:/app
        environment:
          - DEBUG=${DEBUG}
          - SECRET_KEY=${SECRET_KEY}
          - DB_NAME=${DB_NAME}
          - DB_USER=${DB_USER}
          - DB_PASSWORD=${DB_PASSWORD}
          - DB_HOST=${DB_HOST}
          - DB_PORT=${DB_PORT}
          - REDIS_URL=${REDIS_URL}
          - CELERY_BROKER_URL=${CELERY_BROKER_URL}
          - CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
        depends_on:
          - db
          - redis
        networks:
          - crm_network
    
      nginx:
        image: nginx:1.25
        volumes:
          - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro # 掛載 Nginx 配置
          - static_volume:/static # Nginx 服務靜態檔案
        ports:
          - "80:80" # 將主機的 80 端口映射到 Nginx 容器的 80 端口
        depends_on:
          - web # 等待 web 服務啟動
        networks:
          - crm_network
    
    volumes:
      postgres_data:
      static_volume:
    
    networks:
      crm_network:
        driver: bridge
    
  6. 創建 Nginx 配置目錄和檔案:

    Bash
    mkdir docker
    

    檔案內容: FullStackDjangoCRM/docker/nginx.conf

    Nginx
    server {
        listen 80;
        server_name localhost; # 在生產環境請替換為你的域名
    
        location /static/ {
            alias /static/; # 確保這個路徑與 docker-compose.yaml 中的靜態卷軸掛載路徑一致
            expires 30d; # 靜態檔案緩存 30 天
            add_header Cache-Control "public, no-transform";
        }
    
        location / {
            proxy_pass http://web:8000; # 轉發請求到 Django 應用 (web 服務)
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    
  7. 創建 .env 檔案:

    保存敏感信息和環境配置。

    檔案內容: FullStackDjangoCRM/.env

    SECRET_KEY=your_super_secret_key_here # 請替換為一個真正隨機且複雜的密鑰
    DEBUG=True
    ALLOWED_HOSTS=localhost,127.0.0.1
    
    DB_NAME=crm_db
    DB_USER=crm_user
    DB_PASSWORD=crm_password
    DB_HOST=db
    DB_PORT=5432
    
    REDIS_URL=redis://redis:6379/0
    CELERY_BROKER_URL=redis://redis:6379/0
    CELERY_RESULT_BACKEND=redis://redis:6379/0
    
    # 生產環境下應設置為 True 並啟用 HTTPS
    SECURE_SSL_REDIRECT=False
    SESSION_COOKIE_SECURE=False
    CSRF_COOKIE_SECURE=False
    SECURE_HSTS_SECONDS=0
    SECURE_HSTS_INCLUDE_SUBDOMAINS=False
    SECURE_HSTS_PRELOAD=False
    

    安全提示: SECRET_KEY 必須是唯一且複雜的。在生產環境中,DEBUG 必須設置為 False,並且應啟用所有 SECURE_ 相關的設定(將 False 改為 True,並為 SECURE_HSTS_SECONDS 設置一個非零值)。

  8. 構建並啟動 Docker 服務:

    Bash
    docker compose build
    docker compose up -d
    

    這將構建 Docker 映像並在後台啟動所有服務。

三、Django 專案核心設定

現在我們將進入 Django 專案內部。

  1. 創建 Django 專案:

    Bash
    docker compose exec web django-admin startproject crm .
    

    這會在當前目錄 (FullStackDjangoCRM) 下創建一個名為 crm 的 Django 專案,其中包含 settings.py, urls.py 等。

  2. 修改 crm/settings.py:

    打開 crm/settings.py 檔案,進行以下關鍵修改。

    • 導入 environ 和基礎路徑:

      Python
      # crm/settings.py
      import environ
      import os
      
      # 初始化 django-environ
      env = environ.Env()
      # 讀取 .env 文件,如果存在的話
      environ.Env.read_env() 
      
      # 定義專案的根目錄
      BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
      
      # 從環境變數獲取配置
      SECRET_KEY = env('SECRET_KEY')
      DEBUG = env.bool('DEBUG', default=True) # 開發環境下預設為 True
      ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['localhost', '127.0.0.1'])
      
    • INSTALLED_APPS:

      將您會用到的應用程式添加到這裡。

      Python
      # crm/settings.py
      INSTALLED_APPS = [
          'django.contrib.admin',
          'django.contrib.auth',
          'django.contrib.contenttypes',
          'django.contrib.sessions',
          'django.contrib.messages',
          'django.contrib.staticfiles',
      
          'rest_framework',      # 如果未來要開發 API
          'django_bootstrap5',   # 整合 Bootstrap
          'customers',           # 我們自己的應用
          'activities',          # 我們自己的應用
          # ... 其他您可能添加的應用
      ]
      
    • MIDDLEWARE:

      這是 Django 請求處理流程中的中間件,保持默認並確保安全相關的在最前面。

      Python
      # crm/settings.py
      MIDDLEWARE = [
          'django.middleware.security.SecurityMiddleware',
          'django.contrib.sessions.middleware.SessionMiddleware',
          'django.middleware.common.CommonMiddleware',
          'django.middleware.csrf.CsrfViewMiddleware',
          'django.contrib.auth.middleware.AuthenticationMiddleware',
          'django.contrib.messages.middleware.MessageMiddleware',
          'django.middleware.clickjacking.XFrameOptionsMiddleware',
      ]
      
    • ROOT_URLCONF:

      指定專案的主 URL 配置文件。

      Python
      # crm/settings.py
      ROOT_URLCONF = 'crm.urls'
      
    • TEMPLATES:

      配置 Django 模板引擎。確保 DIRS 包含 'templates' 目錄,這樣可以存放共用模板。

      Python
      # crm/settings.py
      TEMPLATES = [
          {
              'BACKEND': 'django.template.backends.django.DjangoTemplates',
              'DIRS': [os.path.join(BASE_DIR, 'templates')], # 添加這個路徑來存放共用模板
              'APP_DIRS': True, # 讓 Django 搜尋每個 app 的 templates 目錄
              'OPTIONS': {
                  'context_processors': [
                      'django.template.context_processors.debug',
                      'django.template.context_processors.request',
                      'django.contrib.auth.context_processors.auth',
                      'django.contrib.messages.context_processors.messages',
                  ],
              },
          },
      ]
      
    • WSGI_APPLICATION 和 ASGI_APPLICATION:

      指定應用程式的 WSGI 和 ASGI 入口點。

      Python
      # crm/settings.py
      WSGI_APPLICATION = 'crm.wsgi.application'
      ASGI_APPLICATION = 'crm.asgi.application' # 如果未來需要 ASGI (例如用於 WebSocket)
      
    • DATABASES:

      配置 PostgreSQL 資料庫,所有連接參數都從 .env 環境變數中獲取。

      Python
      # crm/settings.py
      DATABASES = {
          'default': {
              'ENGINE': 'django.db.backends.postgresql',
              'NAME': env('DB_NAME'),
              'USER': env('DB_USER'),
              'PASSWORD': env('DB_PASSWORD'),
              'HOST': env('DB_HOST'), # Docker 服務名稱 (在 docker-compose.yaml 中定義)
              'PORT': env('DB_PORT'),
          }
      }
      
    • AUTH_PASSWORD_VALIDATORS:

      用於強化密碼安全,這是一組 Django 內建的密碼驗證器。

      Python
      # crm/settings.py
      AUTH_PASSWORD_VALIDATORS = [
          {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
          {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
          {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
          {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
      ]
      
    • 國1際化和時區:

      設置語言和時區,方便未來應用程式的本地化。

      Python
      # crm/settings.py
      LANGUAGE_CODE = 'zh-hant' # 設置為繁體中文
      TIME_ZONE = 'Asia/Taipei' # 設置為台北時區
      USE_I18N = True # 啟用國際化
      USE_TZ = True # 啟用時區感知
      
    • 靜態檔案配置:

      配置 Django 如何處理 CSS、JavaScript 和圖片等靜態檔案。

      Python
      # crm/settings.py
      STATIC_URL = '/static/'
      STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # 部署時,所有靜態檔案會被收集到此目錄
      STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # 開發時,您可以將應用程式共用的靜態檔案放在這裡
      
    • 緩存 (Redis):

      配置 Redis 作為 Django 的預設緩存後端,提高應用程式效能。

      Python
      # crm/settings.py
      CACHES = {
          'default': {
              'BACKEND': 'django_redis.cache.RedisCache',
              'LOCATION': env('REDIS_URL'),
              'OPTIONS': {
                  'CLIENT_CLASS': 'django_redis.client.DefaultClient',
              }
          }
      }
      
    • Celery 配置:

      設置 Celery,用於處理異步任務(例如發送電子郵件或複雜的資料處理)。

      Python
      # crm/settings.py
      CELERY_BROKER_URL = env('CELERY_BROKER_URL')
      CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND')
      CELERY_ACCEPT_CONTENT = ['json']
      CELERY_TASK_SERIALIZER = 'json'
      
    • 安全相關設定 (生產環境務必啟用):

      這些設定在開發環境通常設置為 False,但在生產環境中為了安全,務必將其設置為 True 並確保您的網站使用 HTTPS。

      Python
      # crm/settings.py
      SECURE_SSL_REDIRECT = env.bool('SECURE_SSL_REDIRECT', default=False)
      SESSION_COOKIE_SECURE = env.bool('SESSION_COOKIE_SECURE', default=False)
      CSRF_COOKIE_SECURE = env.bool('CSRF_COOKIE_SECURE', default=False)
      # HSTS (HTTP Strict Transport Security) 配置,防止降級攻擊
      SECURE_HSTS_SECONDS = env.int('SECURE_HSTS_SECONDS', default=0) # 在生產環境設置為非零值 (例如 31536000 1年)
      SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool('SECURE_HSTS_INCLUDE_SUBDOMAINS', default=False)
      SECURE_HSTS_PRELOAD = env.bool('SECURE_HSTS_PRELOAD', default=False)
      
  3. 修改 crm/wsgi.py 和 crm/asgi.py:

    這兩個檔案通常由 Django 自動生成,並且是正確的,無需額外修改。它們是應用程式與 Web 服務器溝通的標準接口。

    • crm/wsgi.py 內容:
      Python
      import os
      from django.core.wsgi import get_wsgi_application
      
      os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crm.settings')
      application = get_wsgi_application()
      
    • crm/asgi.py 內容:
      Python
      import os
      from django.core.asgi import get_asgi_application
      
      os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crm.settings')
      application = get_asgi_application()
      
  4. 修改 crm/urls.py (專案層級):

    這是專案的總路由配置。我們將在此處包含各個應用程式的 URL。

    檔案內容: crm/urls.py

    Python
    from django.contrib import admin
    from django.urls import path, include
    from django.contrib.auth import views as auth_views # 導入 Django 內建的認證視圖
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('customers/', include('customers.urls')),
        path('activities/', include('activities.urls')),
        # 登入/登出 URL:使用 Django 內建的認證視圖
        path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
        path('logout/', auth_views.LogoutView.as_view(), name='logout'),
        # 你可能還需要一個首頁 URL,例如:
        # path('', views.home, name='home'),
    ]
    

四、創建 Django 應用程式

我們將為客戶管理和活動追蹤分別創建兩個 Django 應用程式:customersactivities

  1. 創建應用程式:

    在您的終端機中,確保您在 FullStackDjangoCRM 目錄下,然後運行以下命令來創建應用程式。

    Bash
    docker compose exec web python manage.py startapp customers
    docker compose exec web python manage.py startapp activities
    

    這將在專案根目錄下創建兩個新的子目錄:customersactivities,每個目錄都包含了 Django 應用程式的標準骨架檔案(如 models.py, views.py 等)。

  2. 定義模型 (Models):

    模型是 Django 應用程式的核心,它定義了資料庫的結構以及如何與資料庫互動。

    • customers/models.py (客戶模型):

      這個檔案定義了 Customer 模型的結構,它將映射到資料庫中的一個表。

      檔案內容: FullStackDjangoCRM/customers/models.py

      Python
      from django.db import models
      from django.contrib.auth.models import User # 引入 Django 的內建 User 模型
      
      class Customer(models.Model):
          name = models.CharField(max_length=100, verbose_name="客戶名稱")
          email = models.EmailField(unique=True, blank=True, null=True, verbose_name="電子郵件")
          phone = models.CharField(max_length=20, blank=True, verbose_name="電話")
          address = models.TextField(blank=True, verbose_name="地址")
          # 透過 ForeignKey 關聯到 Django 內建的 User 模型,表示哪個用戶創建了這個客戶
          # on_delete=models.CASCADE 表示如果關聯的用戶被刪除,則其創建的所有客戶也會被刪除
          created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="創建者")
          created_at = models.DateTimeField(auto_now_add=True, verbose_name="創建時間") # 首次保存時自動設置時間
          updated_at = models.DateTimeField(auto_now=True, verbose_name="更新時間")   # 每次保存時自動更新時間
      
          class Meta:
              verbose_name = "客戶"          # 在 Django Admin 中顯示的單數名稱
              verbose_name_plural = "客戶"   # 在 Django Admin 中顯示的複數名稱
              ordering = ['name']            # 默認按照客戶名稱升序排序
      
          def __str__(self):
              """
              定義模型的字串表示,方便在 Django Admin 或調試時識別物件。
              """
              return self.name
      

      重要提示: created_by 字段的添加是為了確保每個客戶記錄都與一個用戶關聯,這對於實現基於用戶的數據隔離(例如每個用戶只能看到自己的客戶)非常重要。

    • activities/models.py (活動模型):

      這個檔案定義了 Activity 模型的結構,它將追蹤與客戶相關的各類活動。

      檔案內容: FullStackDjangoCRM/activities/models.py

      Python
      from django.db import models
      from django.contrib.auth.models import User
      from customers.models import Customer # 從客戶應用程式引入 Customer 模型
      
      class Activity(models.Model):
          # 定義活動類型的選擇項。每個元組的第一個元素是儲存到資料庫的值,第二個是人類可讀的名稱。
          ACTIVITY_TYPES = (
              ('CALL', '電話'),
              ('MEETING', '會議'),
              ('TASK', '任務'),
              ('EMAIL', '電子郵件'), # 增加一個常見的活動類型
          )
          activity_type = models.CharField(max_length=20, choices=ACTIVITY_TYPES, verbose_name="活動類型")
          # 透過 ForeignKey 關聯到 Customer 模型,表示這個活動是關於哪個客戶的
          customer = models.ForeignKey(Customer, on_delete=models.CASCADE, verbose_name="相關客戶")
          description = models.TextField(blank=True, verbose_name="描述") # 活動的詳細描述,允許為空
          created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="創建者") # 關聯創建該活動的用戶
          created_at = models.DateTimeField(auto_now_add=True, verbose_name="創建時間")
          updated_at = models.DateTimeField(auto_now=True, verbose_name="更新時間")
      
          class Meta:
              verbose_name = "活動"
              verbose_name_plural = "活動"
              ordering = ['-created_at'] # 默認按照創建時間倒序排序(最新活動在前)
      
          def __str__(self):
              """
              定義模型的字串表示。
              get_activity_type_display() 方法會返回 choices 中定義的人類可讀名稱。
              """
              return f"{self.get_activity_type_display()} - {self.customer.name}"
      
  3. 註冊應用程式:

    您需要在 crm/settings.py 檔案中,將新創建的 customers 和 activities 應用程式添加到 INSTALLED_APPS 列表中,這樣 Django 才能識別它們。

    這部分我們已經在之前的「修改 crm/settings.py」步驟中完成,請確保您的 settings.py 包含了它們:

    Python
    # crm/settings.py (部分內容)
    INSTALLED_APPS = [
        # ...
        'customers',
        'activities',
        # ...
    ]
    
  4. 執行資料庫遷移:

    定義了模型之後,您需要告訴 Django 根據這些模型創建或修改資料庫表。

    首先,創建遷移文件:

    Bash
    docker compose exec web python manage.py makemigrations
    

    這會檢查您的模型定義,並生成相應的遷移文件(位於每個應用程式的 migrations 目錄下)。

    然後,應用這些遷移到資料庫:

    Bash
    docker compose exec web python manage.py migrate
    

    這會執行所有未應用的遷移,包括 Django 內建應用程式的遷移(如用戶、權限)以及您自己定義的 CustomerActivity 模型的遷移。

  5. 創建超級用戶:

    為了能夠登錄 Django Admin 介面和測試應用程式,您需要創建一個超級用戶。

    Bash
    docker compose exec web python manage.py createsuperuser
    

    按照提示輸入您想要的用戶名、電子郵件(可選)和密碼。請記住這個用戶名和密碼,稍後會用到。

好的,這是教學文章的下一部分,關於 Django Admin 管理介面視圖 (Views) 和 URL 配置


五、Django Admin 管理介面

Django Admin 是一個非常強大且開箱即用的管理後台,它能讓您快速地查看、創建、編輯和刪除應用程式的資料,無需編寫額外的視圖或模板。

  1. 為模型註冊 Admin:

    您需要告訴 Django Admin 哪些模型應該顯示在管理介面中。這通常在每個應用程式的 admin.py 檔案中完成。

    • customers/admin.py:

      打開 FullStackDjangoCRM/customers/admin.py 並添加以下內容。

      Python
      # customers/admin.py
      from django.contrib import admin
      from .models import Customer # 導入我們定義的 Customer 模型
      
      # 使用 @admin.register 裝飾器來註冊模型到 Admin 介面
      @admin.register(Customer)
      class CustomerAdmin(admin.ModelAdmin):
          # list_display 定義了在列表頁面顯示的字段
          list_display = ('name', 'email', 'phone', 'created_by', 'created_at')
          # search_fields 允許您在 Admin 列表頁面中按這些字段進行搜索
          search_fields = ('name', 'email', 'phone')
          # list_filter 允許您在 Admin 列表頁面中按這些字段進行過濾
          list_filter = ('created_by',)
      
    • activities/admin.py:

      打開 FullStackDjangoCRM/activities/admin.py 並添加以下內容。

      Python
      # activities/admin.py
      from django.contrib import admin
      from .models import Activity # 導入我們定義的 Activity 模型
      
      @admin.register(Activity)
      class ActivityAdmin(admin.ModelAdmin):
          list_display = ('activity_type', 'customer', 'created_by', 'created_at')
          search_fields = ('description',) # 允許按描述搜索
          list_filter = ('activity_type', 'created_by') # 允許按活動類型和創建者過濾
      
  2. 訪問 Admin 介面:

    現在,您的 Django 應用程式應該正在 Docker 容器中運行。您可以在瀏覽器中訪問以下 URL:

    http://localhost/admin/
    

    使用您之前創建的超級用戶的用戶名和密碼登錄。您應該能看到 "客戶" 和 "活動" 兩個選項,點擊它們即可開始管理資料。

六、視圖 (Views) 和 URL 配置

視圖是 Django 應用程式中處理業務邏輯的地方。它們接收來自 URL 的請求,與模型互動獲取資料,然後將資料傳遞給模板進行渲染。URL 配置則負責將特定的 URL 路徑映射到這些視圖。

  1. customers/views.py (客戶視圖):

    這個檔案將包含用於顯示客戶列表和客戶詳情的視圖函數。

    檔案內容: FullStackDjangoCRM/customers/views.py

    Python
    from django.shortcuts import render, get_object_or_404, redirect
    from django.contrib.auth.decorators import login_required # 確保只有登錄用戶才能訪問
    from .models import Customer
    # from .forms import CustomerForm # 稍後我們可能會添加表單來處理數據提交
    
    @login_required # 這個裝飾器要求用戶必須登錄才能訪問此視圖
    def customer_list(request):
        """
        顯示當前登錄用戶創建的所有客戶列表。
        """
        # 過濾只顯示由當前請求用戶 (request.user) 創建的客戶
        # order_by('-created_at') 表示按創建時間倒序排列(最新創建的客戶在前)
        customers = Customer.objects.filter(created_by=request.user).order_by('-created_at')
        return render(request, 'customers/customer_list.html', {'customers': customers})
    
    @login_required
    def customer_detail(request, pk):
        """
        顯示特定客戶的詳細信息。
        pk 是從 URL 中捕獲的客戶主鍵 (primary key)。
        """
        # 嘗試獲取指定 pk 的客戶,並且該客戶必須是由當前用戶創建的。
        # 如果找不到,則返回 404 錯誤。
        customer = get_object_or_404(Customer, pk=pk, created_by=request.user)
        return render(request, 'customers/customer_detail.html', {'customer': customer})
    
    # 後續您可以根據需求添加更多視圖,例如用於創建、更新、刪除客戶的功能。
    
  2. customers/urls.py (客戶 URL 配置):

    這個檔案定義了 customers 應用程式內部的 URL 模式。

    檔案內容: FullStackDjangoCRM/customers/urls.py

    Python
    from django.urls import path
    from . import views # 從當前目錄導入 views.py
    
    # 定義應用程式命名空間,避免與其他應用程式的 URL 名稱衝突
    app_name = 'customers'
    
    urlpatterns = [
        # 當路徑為空字串 '' 時(例如 /customers/),調用 views.customer_list 視圖
        # name='customer_list' 為這個 URL 模式指定一個名稱,方便在模板中引用
        path('', views.customer_list, name='customer_list'),
        # 當路徑包含一個整數參數時(例如 /customers/1/),調用 views.customer_detail 視圖
        # <int:pk> 會將匹配到的整數作為 pk 參數傳遞給視圖函數
        path('<int:pk>/', views.customer_detail, name='customer_detail'),
    ]
    
  3. activities/views.py (活動視圖):

    這個檔案將包含用於顯示活動列表的視圖函數。

    檔案內容: FullStackDjangoCRM/activities/views.py

    Python
    from django.shortcuts import render
    from django.contrib.auth.decorators import login_required # 確保只有登錄用戶才能訪問
    from .models import Activity
    
    @login_required # 要求用戶登錄
    def activity_list(request):
        """
        顯示當前登錄用戶創建的所有活動列表。
        """
        # 過濾只顯示由當前請求用戶 (request.user) 創建的活動
        # order_by('-created_at') 表示按創建時間倒序排列
        activities = Activity.objects.filter(created_by=request.user).order_by('-created_at')
        return render(request, 'activities/activity_list.html', {'activities': activities})
    
    # 後續您可以添加更多視圖,例如用於創建、更新、刪除活動的功能。
    
  4. activities/urls.py (活動 URL 配置):

    這個檔案定義了 activities 應用程式內部的 URL 模式。

    檔案內容: FullStackDjangoCRM/activities/urls.py

    Python
    from django.urls import path
    from . import views # 從當前目錄導入 views.py
    
    # 定義應用程式命名空間
    app_name = 'activities'
    
    urlpatterns = [
        # 當路徑為空字串 '' 時(例如 /activities/),調用 views.activity_list 視圖
        path('', views.activity_list, name='activity_list'),
    ]
    

七、模板 (Templates)

模板是 Django 應用程式中呈現用戶界面的 HTML 檔案。它們使用 Django 模板語言來顯示來自視圖的資料,並繼承 base.html 來保持一致的網站佈局。

  1. 創建 templates 目錄:

    在您的 Django 專案根目錄 (FullStackDjangoCRM) 下,創建一個名為 templates 的目錄。這個目錄將用於存放您專案範圍內的共用模板,例如 base.html。

    Bash
    mkdir templates
    
  2. 創建應用程式專屬模板目錄:

    在 templates 目錄下,為每個應用程式創建一個與應用程式同名的子目錄。

    Bash
    mkdir templates/customers
    mkdir templates/activities
    mkdir templates/registration # 用於認證系統的模板
    
  3. templates/base.html (基礎佈局模板):

    這是您網站的基礎骨架,包含了導航欄、腳部、以及所有頁面共用的 CSS 和 JavaScript 連結。其他模板將會繼承這個基礎模板。

    檔案內容: FullStackDjangoCRM/templates/base.html

    HTML
    {% load static %} {# 加載靜態檔案相關的模板標籤 #}
    <!DOCTYPE html>
    <html lang="zh-Hant">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Django CRM - {% block title %}{% endblock %}</title> {# 頁面標題,由子模板填充 #}
    
        {# 引入 Bootstrap 5 CSS #}
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
        {# 引入自定義 CSS (如果有的話) #}
        <link rel="stylesheet" href="{% static 'css/custom.css' %}"> 
    </head>
    <body>
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
            <div class="container-fluid">
                <a class="navbar-brand" href="#">Django CRM</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarNav">
                    <ul class="navbar-nav ms-auto"> {# 使用 ms-auto 將導航項目靠右對齊 #}
                        {% if user.is_authenticated %} {# 判斷用戶是否已登錄 #}
                            <li class="nav-item">
                                <a class="nav-link" href="{% url 'customers:customer_list' %}">客戶</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="{% url 'activities:activity_list' %}">活動</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="{% url 'admin:index' %}">管理</a> {# 指向 Django Admin 介面 #}
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="{% url 'logout' %}">登出</a>
                            </li>
                        {% else %}
                            <li class="nav-item">
                                <a class="nav-link" href="{% url 'login' %}">登入</a>
                            </li>
                        {% endif %}
                    </ul>
                </div>
            </div>
        </nav>
        <div class="container mt-5">
            {# 這是內容區塊,子模板會填充這裡 #}
            {% block content %}
            {% endblock %}
        </div>
    
        {# 引入 Bootstrap 5 JavaScript #}
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
        {# 引入自定義 JavaScript (如果有的話) #}
        <script src="{% static 'js/custom.js' %}"></script>
    </body>
    </html>
    

    注意: 我已經將之前您提供的 base.html 中可能導致問題的 XLSX 相關的 JavaScript 程式碼移除,因為它們不屬於核心佈局,且通常應該放在單獨的 JavaScript 檔案中,或者僅在需要該功能的特定頁面引入。這樣 base.html 更加簡潔和可讀。

  4. templates/customers/customer_list.html (客戶列表模板):

    這個模板用於顯示客戶列表。

    檔案內容: FullStackDjangoCRM/templates/customers/customer_list.html

    HTML
    {% extends 'base.html' %} {# 繼承 base.html 模板 #}
    {% block title %}客戶列表{% endblock %} {# 設定頁面標題 #}
    {% block content %} {# 填充 base.html 中定義的 content 區塊 #}
        <h1>客戶列表</h1>
        <table class="table table-striped"> {# 使用 Bootstrap 表格樣式 #}
            <thead>
                <tr>
                    <th>姓名</th>
                    <th>電子郵件</th>
                    <th>電話</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                {% for customer in customers %} {# 循環遍歷從視圖傳遞過來的 'customers' 列表 #}
                <tr>
                    <td>{{ customer.name }}</td>
                    <td>{{ customer.email }}</td>
                    <td>{{ customer.phone }}</td>
                    <td>
                        {# 創建一個指向客戶詳情頁的連結,使用 Django 的 url 標籤和命名空間 #}
                        <a href="{% url 'customers:customer_detail' customer.pk %}" class="btn btn-primary btn-sm">查看</a>
                    </td>
                </tr>
                {% empty %} {# 如果 'customers' 列表為空,則顯示以下內容 #}
                <tr><td colspan="4">無客戶資料</td></tr>
                {% endfor %}
            </tbody>
        </table>
    {% endblock %}
    

    注意: 同樣地,我也移除了您提供的 customer_list.html 中不屬於列表顯示核心的 JavaScript 程式碼。

  5. templates/customers/customer_detail.html (客戶詳情模板):

    這個模板用於顯示單個客戶的詳細資訊。

    檔案內容: FullStackDjangoCRM/templates/customers/customer_detail.html

    HTML
    {% extends 'base.html' %}
    {% block title %}客戶詳情 - {{ customer.name }}{% endblock %} {# 動態顯示客戶名稱作為標題 #}
    {% block content %}
        <h1>客戶詳情</h1>
        <div class="card"> {# 使用 Bootstrap 的卡片組件來美化顯示 #}
            <div class="card-body">
                <h5 class="card-title">{{ customer.name }}</h5>
                <p class="card-text"><strong>電子郵件:</strong> {{ customer.email }}</p>
                <p class="card-text"><strong>電話:</strong> {{ customer.phone }}</p>
                <p class="card-text"><strong>地址:</strong> {{ customer.address }}</p>
                <p class="card-text"><strong>創建者:</strong> {{ customer.created_by }}</p>
                <p class="card-text"><strong>創建時間:</strong> {{ customer.created_at }}</p>
                {# 返回客戶列表頁的連結 #}
                <a href="{% url 'customers:customer_list' %}" class="btn btn-secondary mt-3">返回客戶列表</a>
            </div>
        </div>
    {% endblock %}
    

    注意: 同樣地,我也移除了您提供的 customer_detail.html 中不屬於詳情顯示核心的 JavaScript 程式碼。

  6. templates/activities/activity_list.html (活動列表模板):

    這個模板用於顯示活動列表。

    檔案內容: FullStackDjangoCRM/templates/activities/activity_list.html

    HTML
    {% extends 'base.html' %}
    {% block title %}活動列表{% endblock %}
    {% block content %}
        <h1>活動列表</h1>
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>類型</th>
                    <th>客戶</th>
                    <th>描述</th>
                    <th>創建時間</th>
                </tr>
            </thead>
            <tbody>
                {% for activity in activities %}
                <tr>
                    {# activity.get_activity_type_display 會返回 choices 中定義的人類可讀名稱 #}
                    <td>{{ activity.get_activity_type_display }}</td>
                    <td>{{ activity.customer.name }}</td> {# 透過 ForeignKey 訪問相關客戶的名稱 #}
                    <td>{{ activity.description }}</td>
                    <td>{{ activity.created_at }}</td>
                </tr>
                {% empty %}
                <tr><td colspan="4">無活動資料</td></tr>
                {% endfor %}
            </tbody>
        </table>
    {% endblock %}
    

    注意: 同樣地,我也移除了您提供的 activity_list.html 中不屬於列表顯示核心的 JavaScript 程式碼。

好的,這是教學文章的下一部分,關於認證系統 (Authentication System)測試 (Testing)


八、認證系統 (Authentication System)

Django 內建了一個功能強大且易於使用的用戶認證系統,它處理了用戶註冊、登錄、登出、密碼重置等常見功能。我們只需要配置好 URL 和模板,就可以利用它。

  1. 添加登入/登出 URL 到專案層級 urls.py:

    在 crm/urls.py 中,我們已經添加了指向 Django 內建認證視圖的 URL。這裡我們將更詳細地解釋它們。

    檔案內容: FullStackDjangoCRM/crm/urls.py (此處僅顯示新增或修改部分)

    Python
    # ... (其他 import 語句)
    from django.contrib.auth import views as auth_views # 導入 Django 內建的認證視圖
    
    urlpatterns = [
        # ... (您之前定義的其他 URL)
    
        # 登入 URL:
        # 使用 Django 內建的 LoginView。
        # template_name='registration/login.html' 指定了登入頁面使用的模板檔案。
        # name='login' 為這個 URL 模式指定一個名稱,方便在模板中引用。
        path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
    
        # 登出 URL:
        # 使用 Django 內建的 LogoutView。
        # LogoutView 預設會將用戶導向到 settings.LOGOUT_REDIRECT_URL 或一個名為 '/' 的 URL。
        path('logout/', auth_views.LogoutView.as_view(), name='logout'),
    
        # 你可能還需要一個登入後的首頁,例如:
        # path('', views.home, name='home'),
    ]
    
  2. 創建登入模板:

    Django 內建的 LoginView 預設會尋找位於 registration/ 目錄下的 login.html 模板。我們需要在 templates/registration/ 目錄中創建這個檔案。

    檔案內容: FullStackDjangoCRM/templates/registration/login.html

    HTML
    {% extends 'base.html' %} {# 繼承基礎佈局 #}
    {% block title %}登入{% endblock %} {# 設定頁面標題 #}
    {% block content %}
        <div class="row justify-content-center"> {# 使用 Bootstrap 居中佈局 #}
            <div class="col-md-6"> {# 設置列寬 #}
                <div class="card"> {# 使用卡片組件 #}
                    <div class="card-header">登入</div> {# 卡片標題 #}
                    <div class="card-body"> {# 卡片內容 #}
                        <form method="post"> {# 登入表單,使用 POST 方法提交 #}
                            {% csrf_token %} {# Django CSRF 保護,必不可少 #}
                            {# form.as_p 會將表單字段渲染為 <p> 標籤。
                               您也可以手動渲染每個字段以獲得更精細的控制,例如:
                               <div class="mb-3">
                                   <label for="{{ form.username.id_for_label }}" class="form-label">用戶名:</label>
                                   {{ form.username }}
                               </div>
                               <div class="mb-3">
                                   <label for="{{ form.password.id_for_label }}" class="form-label">密碼:</label>
                                   {{ form.password }}
                               </div>
                            #}
                            {{ form.as_p }}
                            <button type="submit" class="btn btn-primary">登入</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    {% endblock %}
    

    說明:LoginView 被調用時,它會自動實例化一個 AuthenticationForm(用於處理用戶名和密碼輸入)並將其傳遞給模板,名稱為 form

九、測試 (Testing)

測試是軟體開發中至關重要的一環,它能幫助您驗證程式碼的行為是否符合預期,及早發現錯誤,並確保在修改程式碼後不會引入新的問題(迴歸測試)。Django 提供了一個強大的測試框架。

  1. activities/tests.py:

    我們將為 Activity 模型編寫一個簡單的單元測試。

    檔案內容: FullStackDjangoCRM/activities/tests.py

    Python
    from django.test import TestCase # 導入 Django 的測試基類
    from django.contrib.auth.models import User # 導入 Django 內建的 User 模型
    from customers.models import Customer # 導入 Customer 模型
    from .models import Activity # 導入 Activity 模型
    
    class ActivityModelTests(TestCase):
        """
        為 Activity 模型編寫測試。
        """
        def setUp(self):
            """
            在每個測試方法運行之前準備測試資料。
            """
            # 創建一個測試用戶,用於模擬創建客戶和活動
            self.user = User.objects.create_user(username='testuser', password='password123')
    
            # 創建一個測試客戶,並關聯到上述測試用戶
            self.customer = Customer.objects.create(
                name='Test Customer',
                email='test@example.com',
                created_by=self.user # 關聯到創建者
            )
    
            # 創建一個測試活動,並關聯到測試用戶和客戶
            self.activity = Activity.objects.create(
                activity_type='CALL', # 使用模型中定義的 CHOICE 值
                customer=self.customer,
                description='Test call for test customer.',
                created_by=self.user
            )
    
        def test_activity_str(self):
            """
            測試 Activity 模型的 __str__ 方法是否返回正確的字串表示。
            """
            # 預期的字串應該是 "電話 - Test Customer"
            # 這取決於 Activity 模型中 ACTIVITY_TYPES 的定義,如果 'CALL' 對應 '電話'。
            # 請檢查您 models.py 中的定義:
            # ('CALL', '電話')
            self.assertEqual(str(self.activity), '電話 - Test Customer')
    
        def test_activity_creation(self):
            """
            測試 Activity 對象是否成功創建並儲存到資料庫,以及其屬性是否正確。
            """
            # 驗證資料庫中 Activity 模型的實例數量為 1
            self.assertEqual(Activity.objects.count(), 1)
    
            # 從資料庫中獲取剛創建的 Activity 對象
            new_activity = Activity.objects.get(activity_type='CALL')
    
            # 驗證新活動的客戶名稱和創建者用戶名是否正確
            self.assertEqual(new_activity.customer.name, 'Test Customer')
            self.assertEqual(new_activity.created_by.username, 'testuser')
    
        # 您可以根據需要添加更多測試方法,例如:
        # - test_activity_update:測試活動更新功能
        # - test_activity_delete:測試活動刪除功能
        # - test_activity_list_view:測試活動列表頁面是否正確顯示數據
        # - test_login_required_for_views:測試未登錄用戶是否被重定向到登錄頁面
    
  2. 運行測試:

    在您的終端機中,確保您在 FullStackDjangoCRM 目錄下,然後運行以下命令來執行測試:

    Bash
    docker compose exec web python manage.py test
    

    Django 將會發現並運行所有應用程式中的 tests.py 文件中的測試。如果所有測試都通過,您將會看到類似 "OK" 的輸出。

十、下一步和進階概念

恭喜您!您已經成功建立了一個基礎的 Django CRM 系統,並學習了如何使用 Docker Compose 進行開發。這只是冰山一角,Django 世界還有許多值得探索的地方。

  • 表單 (Forms):

    目前我們還沒有提供創建或編輯客戶/活動的表單。您可以為 Customer 和 Activity 模型創建 ModelForm,以處理用戶輸入和驗證。

    Python
    # customers/forms.py (範例,您需要自己創建這個檔案)
    from django import forms
    from .models import Customer, Activity
    
    class CustomerForm(forms.ModelForm):
        class Meta:
            model = Customer
            fields = ['name', 'email', 'phone', 'address'] # 不包含 created_by, created_at, updated_at,這些會自動處理
    
    class ActivityForm(forms.ModelForm):
        class Meta:
            model = Activity
            fields = ['activity_type', 'customer', 'description'] # created_by, created_at, updated_at 也會自動處理
    

    然後在您的視圖 (views.py) 中使用這些表單來處理 POST 請求。

  • CRUD 功能:

    為客戶和活動實現完整的 CRUD (創建、讀取、更新、刪除) 功能。可以利用 Django 的 Class-Based Views (CBV),如 CreateView, UpdateView, DeleteView,它們能大大簡化程式碼並提供統一的結構。

  • 分頁 (Pagination):

    當資料量很大時,實現分頁來提高列表頁的載入速度和用戶體驗。Django 內建了分頁器。

  • Celery 異步任務:

    利用 Celery 處理耗時任務,如發送郵件通知、生成大型報告、數據導入導出等,避免阻塞用戶界面。

    要配置 Celery,您需要:

    1. crm/ 目錄下創建 celery.py 檔案: 檔案內容: FullStackDjangoCRM/crm/celery.py
      Python
      import os
      from celery import Celery
      
      # 設置 Django settings 模組的路徑
      os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crm.settings')
      
      app = Celery('crm')
      
      # 使用 Django settings 中的配置來配置 Celery (例如 CELERY_BROKER_URL)
      app.config_from_object('django.conf:settings', namespace='CELERY')
      
      # 自動發現所有已註冊 Django 應用程式中的任務
      app.autodiscover_tasks()
      
    2. crm/__init__.py 中導入它,確保 Celery 應用程式在 Django 啟動時加載: 檔案內容: FullStackDjangoCRM/crm/__init__.py
      Python
      from .celery import app as celery_app
      
      __all__ = ('celery_app',)
      
    3. 在您的應用程式(例如 activities)中創建 tasks.py 檔案來定義異步任務:
      Python
      # activities/tasks.py (範例)
      from celery import shared_task
      import time
      
      @shared_task
      def send_email_notification(customer_id, message):
          """
          模擬發送電子郵件的異步任務。
          """
          # 這裡應該是發送郵件的實際邏輯
          print(f"Sending email to customer {customer_id}: {message}")
          time.sleep(5) # 模擬耗時操作
          print(f"Email sent to customer {customer_id}.")
          return f"Email sent successfully to customer {customer_id}"
      
      然後您可以在視圖中調用 send_email_notification.delay(customer.id, '您的活動已更新') 來非同步執行任務。
  • API 開發 (Django REST Framework):

    如果未來需要與移動應用或前端框架 (如 React, Vue.js) 集成,可以學習 Django REST Framework (DRF) 來構建 RESTful API。DRF 可以讓您非常快速地創建功能強大的 API 端點。

  • 更細緻的權限控制:

    Django 內建的權限系統已經很強大,但對於更複雜的角色和權限需求,可以探索第三方庫如 django-rules 或 Django Guardian。

  • 部署到生產環境:

    這是一個完整的專案,包含了 Dockerfile, docker-compose.yaml, nginx.conf 等,為生產部署打下了基礎。在部署到生產環境時,請務必:

    • DEBUG 設置為 False
    • 更新 SECRET_KEY 為一個更安全的隨機字串。
    • ALLOWED_HOSTS 設置為您的實際域名。
    • 啟用並配置 SECURE_SSL_REDIRECT, SESSION_COOKIE_SECURE, CSRF_COOKIE_SECURESECURE_HSTS_SECONDS 等安全設定。
    • 使用 SSL/TLS 證書 (例如 Let's Encrypt) 來啟用 HTTPS,並在 Nginx 配置中相應設置。
    • 考慮使用雲服務供應商 (如 AWS ECS/EC2, Google Cloud Run/Compute Engine, Azure App Service) 來部署 Docker 容器,並使用更健壯的日誌和監控解決方案。

結語

本教學為您提供了一個 Django CRM 專案的基礎框架和逐步指南。通過實踐,您不僅會學習到 Django 的核心概念,還會接觸到現代 Web 開發中重要的工具和最佳實踐,如 Docker、環境變數管理、緩存和異步任務。

記住,軟體開發是一個持續學習和迭代的過程。多動手,多嘗試,祝您在 Django 的世界中玩得開心!


沒有留言:

張貼留言

網誌存檔