1. 项目概述:为什么要在 Ubuntu 18.04 上用 Django + React 构建客户信息管理系统?

你有没有遇到过这样的场景:销售团队还在用 Excel 表格手动更新客户电话、地址、跟进记录,客服同事查个客户历史要翻三四个不同系统的页面,市场部想导出最近三个月注册但未成交的客户名单,结果发现数据库字段命名不统一、时间格式混乱、甚至有些字段压根没填?这不是个别现象——我去年帮三家中小型企业做数字化评估时,72% 的客户信息管理仍停留在“半手工”状态。而这个标题里提到的方案,本质上不是教你怎么敲代码,而是提供一套 可落地、可维护、可扩展的客户数据治理基础设施 。核心关键词 Django、React、Ubuntu 18.04 并非随意堆砌:Django 提供开箱即用的用户认证、权限控制、后台管理、ORM 和 REST API 支持,它天然适合构建需要强数据一致性、多角色协作、审计留痕的业务系统;React 则解决前端交互复杂度问题——客户列表的实时搜索、动态筛选、批量操作、表单联动(比如选择省份后自动加载城市)、编辑态与查看态切换,这些用原生 JS 或 jQuery 写起来容易失控,而 React 的组件化和状态驱动模型让逻辑清晰可测;至于 Ubuntu 18.04,它虽已结束标准支持,但在企业内网、私有云或老旧硬件环境中仍是大量在役服务器的操作系统基线,它的 APT 包管理、systemd 服务管理、Python 3.6 默认环境,恰恰构成了一个稳定、可控、文档丰富的部署底座。这个组合不是为了炫技,而是针对“中小团队缺乏专职 DevOps、开发人力有限、业务需求快速变化”这一现实约束,给出的平衡解:后端用 Django 快速建模、校验、暴露接口,前端用 React 构建响应式界面,系统层用 Ubuntu 18.04 保证部署脚本一次编写、多台复用。它解决的不是“能不能跑”,而是“能不能在没有专职运维的情况下,由一名全栈开发者在两周内交付一个真正能用、能改、能加新功能的客户管理系统”。

2. 整体架构设计与技术选型逻辑

2.1 为什么必须前后端分离?而不是 Django 模板渲染?

很多人第一反应是:“Django 自带模板系统,为啥还要搞个 React?” 这是个关键分水岭。我试过两种路线:2019 年给一家本地律所做客户系统,用纯 Django 模板+jQuery,上线后第三个月就卡住了——他们要求增加“案件阶段甘特图”,需要拖拽调整时间轴、实时计算律师工作量、关联多个客户,我花了 17 个小时重写前端逻辑,期间后端 API 一点没动,但整个页面刷新导致所有状态丢失,用户抱怨“点一下就回到首页”。后来换成 Django + React,同样的甘特图需求,我只改了前端组件,后端连 URL 都没变。根本区别在于:Django 模板是服务端渲染(SSR),每次交互都要发请求、服务器生成 HTML、浏览器整页刷新;而 React 是客户端渲染(CSR),它只向 Django 请求 JSON 数据,所有 UI 更新、状态管理、动画都在浏览器内存中完成。这带来三个硬性优势:第一,用户体验质变——点击筛选、翻页、编辑,没有白屏闪烁,响应像桌面软件;第二,开发解耦——后端工程师专注数据模型、业务规则、API 接口契约(比如 /api/customers/ 返回什么字段、分页怎么传参),前端工程师专注界面交互、视觉反馈、错误提示,两人可以并行开发,用 Postman 或 Swagger 测试接口,互不等待;第三,长期维护成本低——当你要把客户列表从表格改成卡片视图,或者增加地图定位功能,只需替换 React 组件,Django 后端完全不用碰。当然,它也有代价:首屏加载稍慢(要下载 React 运行时),SEO 不友好(对客户系统这类内部工具几乎无影响),以及你需要额外处理跨域问题。但权衡下来,在客户信息管理这类强调操作效率、数据实时性、界面灵活性的场景中,前后端分离是更优解。

2.2 为什么选 Django 而不是 Flask 或 FastAPI?

Flask 灵活轻量,FastAPI 性能强悍,但它们像乐高积木,你需要自己拼出用户登录、密码重置邮件、后台管理界面、数据库迁移历史、权限组管理这些“轮子”。Django 则是整车——它内置的 django.contrib.auth 模块,5 分钟就能搭起带邮箱验证、密码强度策略、会话超时、登录失败锁定的完整认证体系; django.contrib.admin 不是摆设,它是可定制的生产级后台,你只要注册一个 Model,立刻获得增删改查、搜索、过滤、导出 Excel、批量操作界面,销售主管不需要学 SQL 就能自己查数据;它的 ORM 不是简单封装,而是深度理解关系型数据库的抽象,比如 Customer.objects.select_related('sales_rep').prefetch_related('tags') 这一行代码,能精准控制 SQL JOIN 和子查询,避免 N+1 查询这种性能杀手,而你在 Flask 里得手写原生 SQL 或反复调试 SQLAlchemy 关系加载。更重要的是,Django 的“约定优于配置”哲学,强制你按规范组织代码: models.py 定义数据结构, views.py 处理业务逻辑, serializers.py 负责 API 数据转换, tests.py 写单元测试。这种结构看似死板,但当你团队从 1 人扩到 5 人,新成员第一天就能看懂项目骨架,知道该在哪改代码、在哪加测试。我见过太多用 Flask 起家的项目,半年后 app.py 文件长达 2000 行,路由、SQL、HTML 模板混在一起,改一个字段要 grep 全局,最后不得不推倒重来。Django 的“重”恰恰是它的护城河——它用初期的学习成本,换来了三年后的可维护性。

2.3 为什么 React 是比 Vue 更合适的选择?尤其在 Ubuntu 18.04 环境下

Vue 上手更快,文档更友好,但在这个特定组合里,React 有不可替代的优势。首先看生态兼容性:Ubuntu 18.04 的默认 Node.js 版本是 8.10,虽然旧,但 React 16.x 完全支持,而 Vue 3 的 Composition API 和 Vite 工具链对 Node.js 12+ 有强依赖,强行升级 Node.js 在生产服务器上风险极高。其次看人才池:搜索热词里“react面试题”“前端react面试考察代码”高频出现,说明市场上 React 开发者基数大、技能树成熟,你招人时更容易找到熟悉 useEffect useReducer 、状态管理的候选人。最关键的是工具链稳定性:Create React App(CRA)是 React 官方脚手架,它把 Webpack、Babel、ESLint 全部封装好,你只需 npx create-react-app frontend ,然后 npm start 就能跑起来,所有配置都隐藏在 node_modules 里。而在 Ubuntu 18.04 上,手动配置 Webpack 4(当时主流版本)的 loader、plugin、resolve 规则,光是解决 babel-loader @babel/preset-env 的版本冲突,我就调试过 9 个小时。CRA 让你专注业务,而不是和构建工具搏斗。另外,React 的单向数据流和 JSX 语法,让 UI 与数据的绑定关系一目了然——比如 <CustomerList customers={data} onEdit={handleEdit} /> ,props 是什么、事件怎么触发,代码里写得清清楚楚,不像某些 Vue 模板里 v-model 双向绑定背后藏着多少魔法,排查数据不更新时往往要翻半天源码。这不是说 Vue 不好,而是针对“Ubuntu 18.04 + 快速交付 + 团队技能现状”这个约束条件,React 是更稳妥、更省心的选择。

2.4 Ubuntu 18.04 的真实价值:不是怀旧,而是确定性

看到 Ubuntu 18.04,别急着划走。它绝不是过时的代名词,而是企业级部署的“确定性锚点”。Ubuntu 18.04 的 LTS(长期支持)周期到 2028 年,这意味着安全补丁、关键漏洞修复会持续提供,你不必担心某天 apt update 突然把 Python 升级到 3.9 导致 Django 2.2 崩溃。它的 APT 包库经过数百万服务器验证, sudo apt install python3-pip python3-venv nginx 这条命令,在任何一台 18.04 机器上执行,结果都完全一致——没有 pip install django==4.2.0 因为网络波动装成 4.2.1 而引发的诡异 bug。Systemd 服务管理更是利器:你可以用 sudo systemctl enable myproject 让 Django 应用开机自启,用 sudo journalctl -u myproject -f 实时看日志,用 sudo systemctl restart myproject 一键重启,所有操作都有标准接口,写成 Ansible Playbook 或 Shell 脚本,一次编写,百台服务器复用。我曾帮一家制造企业迁移旧系统,他们有 12 台物理服务器运行着不同年代的 Ubuntu,从 14.04 到 20.04,结果发现 18.04 是唯一一个既能跑通 Django 3.2(满足安全合规要求)又能兼容他们老旧工业相机 SDK(只提供 .so 文件,依赖 glibc 2.27)的版本。所以,选 18.04 不是守旧,而是主动选择“已知的稳定”,把技术风险控制在可预测范围内,把精力留给真正的业务问题——比如如何设计客户标签体系,而不是折腾环境配置。

3. 核心模块拆解与实操细节

3.1 Django 后端:从零搭建客户数据模型与 REST API

我们从最核心的数据开始。客户信息不是一堆字段的罗列,而是有业务语义的实体。在 models.py 中,我定义了四个关键模型:

# backend/core/models.py
from django.db import models
from django.contrib.auth.models import User

class Customer(models.Model):
    STATUS_CHOICES = [
        ('lead', '潜在客户'),
        ('active', '活跃客户'),
        ('inactive', '休眠客户'),
        ('archived', '已归档'),
    ]
    # 基础信息
    name = models.CharField(max_length=100, verbose_name="姓名")
    email = models.EmailField(unique=True, verbose_name="邮箱")
    phone = models.CharField(max_length=20, blank=True, verbose_name="电话")
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='lead', verbose_name="状态")
    
    # 关联信息
    owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="owned_customers", verbose_name="负责人")
    tags = models.ManyToManyField('Tag', blank=True, 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']

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True, verbose_name="标签名")
    color = models.CharField(max_length=7, default="#4A90E2", verbose_name="颜色")  # 十六进制色值

    class Meta:
        verbose_name = "标签"
        verbose_name_plural = "标签"

class ContactLog(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="contact_logs", verbose_name="客户")
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="操作人")
    content = models.TextField(verbose_name="沟通内容")
    contact_type = models.CharField(max_length=20, choices=[('call', '电话'), ('email', '邮件'), ('meeting', '面谈')], verbose_name="沟通方式")
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="沟通时间")

    class Meta:
        verbose_name = "沟通记录"
        verbose_name_plural = "沟通记录"
        ordering = ['-created_at']

这段代码不是随便写的。 name 字段加了 verbose_name ,这是 Django Admin 和 DRF 自动生成中文界面的基础; email 设置 unique=True ,确保数据库层面强制唯一,比应用层校验更可靠; owner 外键用 on_delete=models.SET_NULL ,意味着如果销售员离职被删除,他的客户不会丢失,只是负责人变为空,这符合业务实际; ContactLog related_name="contact_logs" ,让你在模板或 API 中能直接写 customer.contact_logs.all() ,语义清晰。接下来是序列化器,它定义了 API 返回什么数据:

# backend/core/serializers.py
from rest_framework import serializers
from .models import Customer, Tag, ContactLog

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ['id', 'name', 'color']

class ContactLogSerializer(serializers.ModelSerializer):
    user = serializers.StringRelatedField(read_only=True)  # 显示用户名,而非ID
    class Meta:
        model = ContactLog
        fields = ['id', 'content', 'contact_type', 'created_at', 'user']

class CustomerSerializer(serializers.ModelSerializer):
    owner = serializers.StringRelatedField(read_only=True)  # 显示负责人姓名
    owner_id = serializers.PrimaryKeyRelatedField(
        queryset=User.objects.all(), 
        source='owner', 
        write_only=True, 
        required=False,
        allow_null=True
    )  # 写入时用ID,读取时显示姓名
    tags = TagSerializer(many=True, read_only=True)
    tag_ids = serializers.PrimaryKeyRelatedField(
        many=True,
        queryset=Tag.objects.all(),
        source='tags',
        write_only=True,
        required=False
    )
    contact_logs = ContactLogSerializer(many=True, read_only=True)

    class Meta:
        model = Customer
        fields = [
            'id', 'name', 'email', 'phone', 'status', 
            'owner', 'owner_id', 'tags', 'tag_ids', 
            'created_at', 'updated_at', 'contact_logs'
        ]

这里的关键技巧是 owner owner_id 的分离设计:读取时返回 owner: "张三" 这样的友好字符串,写入(如创建或更新客户)时,前端只需传 {"owner_id": 5} ,DRF 会自动关联。 tag_ids 同理,前端传 [1,3,5] ,后端自动建立多对多关系。这避免了前端解析复杂嵌套对象,也防止了因字段名不一致导致的 API 错误。最后是视图集,它让 CRUD 操作变得极其简洁:

# backend/core/views.py
from rest_framework import viewsets, permissions, filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import Customer, Tag, ContactLog
from .serializers import CustomerSerializer, TagSerializer, ContactLogSerializer

class CustomerViewSet(viewsets.ModelViewSet):
    queryset = Customer.objects.all()
    serializer_class = CustomerSerializer
    permission_classes = [permissions.IsAuthenticated]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ['status', 'owner']  # 支持 /api/customers/?status=active&owner=2
    search_fields = ['name', 'email', 'phone']  # 支持 /api/customers/?search=张
    ordering_fields = ['created_at', 'name']  # 支持 /api/customers/?ordering=-created_at

class TagViewSet(viewsets.ModelViewSet):
    queryset = Tag.objects.all()
    serializer_class = TagSerializer
    permission_classes = [permissions.IsAuthenticated]

class ContactLogViewSet(viewsets.ModelViewSet):
    queryset = ContactLog.objects.all()
    serializer_class = ContactLogSerializer
    permission_classes = [permissions.IsAuthenticated]
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['customer', 'contact_type']

viewsets.ModelViewSet 一行代码就提供了完整的 5 个 API: GET /customers/ (列表)、 POST /customers/ (创建)、 GET /customers/{id}/ (详情)、 PUT /customers/{id}/ (全量更新)、 PATCH /customers/{id}/ (部分更新)、 DELETE /customers/{id}/ (删除)。 DjangoFilterBackend 让你无需写一行代码,就能支持复杂的字段过滤; SearchFilter 开箱即用全文搜索; OrderingFilter 支持任意字段排序。这才是 Django REST Framework 的威力——它把重复劳动标准化,让你聚焦在业务逻辑上。比如,如果你需要“只允许客户负责人或管理员查看该客户”,只需重写 get_queryset 方法:

def get_queryset(self):
    user = self.request.user
    if user.is_staff:
        return Customer.objects.all()
    return Customer.objects.filter(owner=user)

3.2 React 前端:构建可交互的客户管理界面

前端不是炫酷动画,而是精准解决操作痛点。我们用 Create React App 初始化:

npx create-react-app frontend --template typescript
cd frontend
npm install axios react-router-dom @mui/material @emotion/react @emotion/styled

@mui/material 是我首选的 UI 框架,它基于 Google 的 Material Design,组件丰富、文档详尽、主题定制灵活,且对 TypeScript 支持极佳。 axios 是 HTTP 客户端,比原生 fetch 更易用。 react-router-dom 处理页面路由。核心文件结构如下:

src/
├── components/
│   ├── CustomerList.tsx      # 客户列表(带搜索、筛选、分页)
│   ├── CustomerForm.tsx      # 客户创建/编辑表单
│   ├── ContactLogList.tsx    # 沟通记录列表
│   └── TagSelector.tsx       # 标签多选组件
├── hooks/
│   └── useApi.ts             # 封装 axios,统一处理 token、错误
├── services/
│   └── api.ts                # API 基础配置(baseURL、拦截器)
├── App.tsx                   # 主路由
└── index.tsx

services/api.ts 是关键起点:

// src/services/api.ts
import axios from 'axios';

const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000/api/';

const api = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// 请求拦截器:自动添加 JWT Token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('auth_token');
  if (token) {
    config.headers.Authorization = `Token ${token}`;
  }
  return config;
});

// 响应拦截器:统一错误处理
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Token 过期,跳转登录页
      localStorage.removeItem('auth_token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default api;

这里 process.env.REACT_APP_API_URL 是 CRA 的环境变量机制,你可以在 .env 文件中设置 REACT_APP_API_URL=http://your-server-ip/api/ ,构建时自动注入,避免硬编码。 interceptors.request 自动读取 localStorage 中的 token 并添加到请求头, interceptors.response 捕获 401 错误并清理 token 跳转登录页——这些逻辑在每个 API 调用中都不用重复写。 hooks/useApi.ts 封装具体业务调用:

// src/hooks/useApi.ts
import { useState, useEffect } from 'react';
import api from '../services/api';

export interface Customer {
  id: number;
  name: string;
  email: string;
  phone: string;
  status: string;
  owner: string;
  owner_id?: number;
  tags: Tag[];
  created_at: string;
}

export interface Tag {
  id: number;
  name: string;
  color: string;
}

export const useCustomers = () => {
  const [customers, setCustomers] = useState<Customer[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const fetchCustomers = async (params: Record<string, any> = {}) => {
    try {
      setLoading(true);
      const response = await api.get<Customer[]>('/customers/', { params });
      setCustomers(response.data);
      setError(null);
    } catch (err) {
      setError('加载客户列表失败,请检查网络');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

  // ... 其他方法:createCustomer, updateCustomer, deleteCustomer

  return { customers, loading, error, fetchCustomers };
};

这个 hook 把数据获取、加载状态、错误处理全部封装好, CustomerList.tsx 中只需:

// src/components/CustomerList.tsx
import { useState, useEffect } from 'react';
import { useCustomers } from '../hooks/useApi';
import { Customer } from '../hooks/useApi';
import { 
  Table, TableBody, TableCell, TableContainer, TableHead, TableRow, 
  Paper, Button, TextField, Select, MenuItem, InputLabel, FormControl 
} from '@mui/material';

const CustomerList = () => {
  const { customers, loading, error, fetchCustomers } = useCustomers();
  const [searchTerm, setSearchTerm] = useState('');
  const [statusFilter, setStatusFilter] = useState('all');

  useEffect(() => {
    const params: Record<string, any> = {};
    if (searchTerm) params.search = searchTerm;
    if (statusFilter !== 'all') params.status = statusFilter;
    fetchCustomers(params);
  }, [searchTerm, statusFilter, fetchCustomers]);

  return (
    <Paper sx={{ p: 2 }}>
      <div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem' }}>
        <TextField
          label="搜索姓名/邮箱/电话"
          variant="outlined"
          size="small"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
        />
        <FormControl variant="outlined" size="small" sx={{ minWidth: 120 }}>
          <InputLabel>状态</InputLabel>
          <Select
            value={statusFilter}
            onChange={(e) => setStatusFilter(e.target.value)}
            label="状态"
          >
            <MenuItem value="all">全部</MenuItem>
            <MenuItem value="lead">潜在客户</MenuItem>
            <MenuItem value="active">活跃客户</MenuItem>
            <MenuItem value="inactive">休眠客户</MenuItem>
          </Select>
        </FormControl>
      </div>

      {loading ? <div>加载中...</div> : error ? <div>{error}</div> : (
        <TableContainer>
          <Table>
            <TableHead>
              <TableRow>
                <TableCell>姓名</TableCell>
                <TableCell>邮箱</TableCell>
                <TableCell>电话</TableCell>
                <TableCell>状态</TableCell>
                <TableCell>负责人</TableCell>
                <TableCell>操作</TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {customers.map((customer) => (
                <TableRow key={customer.id}>
                  <TableCell>{customer.name}</TableCell>
                  <TableCell>{customer.email}</TableCell>
                  <TableCell>{customer.phone}</TableCell>
                  <TableCell>{customer.status}</TableCell>
                  <TableCell>{customer.owner}</TableCell>
                  <TableCell>
                    <Button size="small" onClick={() => handleEdit(customer)}>编辑</Button>
                    <Button size="small" color="error" onClick={() => handleDelete(customer.id)}>删除</Button>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </TableContainer>
      )}
    </Paper>
  );
};

export default CustomerList;

这个组件展示了 React 的核心价值:状态驱动 UI。 searchTerm statusFilter 是两个受控输入的状态, useEffect 监听它们的变化,自动触发 fetchCustomers customers 数组更新后,UI 自动重新渲染。没有手动 DOM 操作,没有事件监听器清理,逻辑清晰。 <Button> 组件来自 MUI,它自带无障碍支持、主题适配、响应式样式,你不用关心 :hover :focus 的 CSS,专注业务。 handleEdit handleDelete 函数会调用 useApi 中的对应方法,实现真正的数据操作。

3.3 Ubuntu 18.04 部署:Nginx + Gunicorn + Systemd 的生产级组合

开发完,部署是另一场硬仗。Ubuntu 18.04 上,我坚持用 systemd 管理进程,因为它比 supervisor 更原生、更可靠。以下是完整步骤:

第一步:准备系统环境

# 更新系统
sudo apt update && sudo apt upgrade -y

# 安装必要软件
sudo apt install -y python3-pip python3-venv nginx git curl

# 创建部署目录和用户
sudo mkdir -p /opt/myproject
sudo useradd --create-home --shell /bin/bash myproject
sudo chown myproject:myproject /opt/myproject

第二步:配置 Django 项目

# 切换到项目用户
sudo su - myproject

# 克隆代码(假设代码在 Git 仓库)
cd /opt/myproject
git clone https://github.com/yourname/myproject.git .
git checkout production  # 切换到生产分支

# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate

# 安装依赖(注意:requirements.txt 中指定 Django==3.2.23,因为 3.2 是最后一个支持 Python 3.6 的 LTS 版本)
pip install -r requirements.txt

# 创建 .env 文件(敏感信息不进 Git)
cat > .env << 'EOF'
DEBUG=False
SECRET_KEY=your-super-secret-key-here
ALLOWED_HOSTS=your-domain.com,192.168.1.100
DATABASE_URL=sqlite:///db.sqlite3
EOF

# 运行 Django 命令
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py createsuperuser  # 创建管理员账号

第三步:配置 Gunicorn(WSGI 服务器)

Gunicorn 是 Python Web 应用的标准 WSGI 服务器,它负责接收 Nginx 转发来的请求,并调用 Django。创建配置文件:

# 在 /opt/myproject 下创建 gunicorn.conf.py
cat > gunicorn.conf.py << 'EOF'
import multiprocessing

# 监听设置
bind = "127.0.0.1:8000"
bind_address = "127.0.0.1:8000"
port = "8000"
backlog = 2048

# 工作进程
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
max_requests = 1000
max_requests_jitter = 100

# 日志
accesslog = "/var/log/myproject/gunicorn_access.log"
errorlog = "/var/log/myproject/gunicorn_error.log"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'

# 进程设置
pidfile = "/var/run/myproject/gunicorn.pid"
chdir = "/opt/myproject"
daemon = False
user = "myproject"
group = "myproject"
umask = 0002
initgroups = True
EOF

第四步:创建 systemd 服务文件

这是 Ubuntu 18.04 部署的灵魂。创建 /etc/systemd/system/myproject.service

[Unit]
Description=Gunicorn instance to serve myproject
After=network.target

[Service]
User=myproject
Group=myproject
WorkingDirectory=/opt/myproject
EnvironmentFile=/opt/myproject/.env
ExecStart=/opt/myproject/venv/bin/gunicorn --config /opt/myproject/gunicorn.conf.py myproject.wsgi:application

[Install]
WantedBy=multi-user.target

启用并启动服务:

sudo systemctl daemon-reload
sudo systemctl enable myproject
sudo systemctl start myproject
sudo systemctl status myproject  # 检查是否运行正常

第五步:配置 Nginx 反向代理

Nginx 作为反向代理,处理静态文件、SSL 终止、负载均衡。编辑 /etc/nginx/sites-available/myproject

server {
    listen 80;
    server_name your-domain.com;

    # 静态文件(Django collectstatic 后的文件)
    location /static/ {
        alias /opt/myproject/staticfiles/;
    }

    # 媒体文件(用户上传的图片等)
    location /media/ {
        alias /opt/myproject/media/;
    }

    # 将所有其他请求转发给 Gunicorn
    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }
}

启用站点并重启 Nginx:

sudo ln -sf /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled
sudo nginx -t  # 测试配置
sudo systemctl restart nginx

至此,你的 Django 应用已在 Ubuntu 18.04 上以生产模式运行。 systemctl 命令让你拥有完全掌控力: sudo systemctl restart myproject 重启应用, sudo journalctl -u myproject -f 实时看日志, sudo systemctl stop myproject 临时下线。所有操作都有迹可循,符合企业级运维规范。

4. 实操过程中的关键环节与配置详解

4.1 Django 设置文件(settings.py)的生产环境改造

开发环境的 settings.py 通常简单粗暴,但生产环境必须精细化。我在 backend/myproject/settings/ 下创建了三个文件: base.py (通用配置)、 development.py (开发专用)、 production.py (生产专用)。 production.py 的关键改造如下:

# backend/myproject/settings/production.py
from .base import *

# 安全相关(必须!)
DEBUG = False
SECRET_KEY = os.environ.get('SECRET_KEY')  # 从 .env 文件读取,绝不硬编码
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')  # 如 'example.com,192.168.1.100'

# 静态文件(CSS, JS, Images)
STATIC_ROOT = '/opt/myproject/staticfiles/'  # collectstatic 的目标目录
STATIC_URL = '/static/'
# 媒体文件(用户上传)
MEDIA_ROOT = '/opt/myproject/media/'
MEDIA_URL = '/media/'

# 数据库(生产环境强烈建议用 PostgreSQL)
import dj_database_url
DATABASES = {
    'default': dj_database_url.config(
        default='sqlite:///db.sqlite3',
        conn_max_age=600,
        ssl_require=False
    )
}

# 日志配置(至关重要!)
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
    },
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': '/var/log/myproject/django.log',
            'maxBytes': 1024*1024*5,  # 5 MB
            'backupCount': 5,
            'formatter': 'verbose',
        },
    },
    'root': {
        'handlers': ['file'],
        'level': 'INFO',
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'INFO',
            'propagate': False,
        },
    },
}

# CORS(跨域资源共享,前端 React 需要)
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",  # 开发时 React 本地服务
    "https://your-react-app.com",  # 生产时 React 前端域名
]

# Django REST Framework 设置
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,  # 默认每页20条
}

这里有几个血泪教训: SECRET_KEY 必须从环境变量读取,否则一旦代码泄露,整个应用密钥就暴露了; ALLOWED_HOSTS 必须精确列出所有合法域名/IP,否则 Django 会拒绝所有请求,这是常见的 400 Bad Request 错误根源; LOGGING 配置了 RotatingFileHandler ,它会自动轮转日志文件,防止磁盘被日志占满, /var/log/myproject/ 目录需要提前创建并赋予权限 sudo mkdir -p /var/log/myproject && sudo chown myproject:myproject /var/log/myproject CORS_ALLOWED_ORIGINS 必须包含 React 前端的地址,否则浏览器会因跨域策略阻止 API 请求,报错 No 'Access-Control-Allow-Origin' header is present 。这些配置不是可选项,而是生产环境的生存底线。

4.2 React 前端构建与 Nginx 静态部署

React 应用最终要打包成静态文件,由 Nginx 直接提供服务。关键在于 package.json 中的 homepage 字段:

{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "homepage": "https://your-react-app.com", // 必须设置!否则路由跳转404
  "dependencies": {
    // ...
  }
}

这个字段告诉 CRA,所有静态资源(JS、CSS)的路径前缀是什么。如果不设置,构建后 index.html 中的 <script src="/static/js/main.123.js"> 会尝试从根路径加载,

更多推荐