1. 项目概述:为什么要在 Ubuntu 18.04 上用 Django + React 做客户数据管理?
我从2015年开始做企业级后台系统,经手过三十多个客户关系类项目,从 PHP+jQuery 的单体架构,到 Java Spring Boot 微服务,再到如今 Python+React 的前后端分离方案。这个标题——“Создание с помощью Django и React современного веб-приложения для управления данными клиентов в Ubuntu 18.04”(用 Django 和 React 在 Ubuntu 18.04 上构建现代客户数据管理 Web 应用)——看似只是个技术组合描述,但背后藏着一套非常典型的、已被验证过十几次的生产级落地路径。它不是实验室玩具,而是中小型企业真正能跑起来、管得住、扩得开的数据中枢。
核心关键词Django、React、Ubuntu 18.04,三个词加在一起,就锁定了一个明确的技术坐标系:后端用 Python 生态最稳的全栈框架 Django,前端用生态最成熟、组件化最彻底的 React,部署环境则锚定在长期支持、企业级稳定、运维文档最丰富的 Linux 发行版 Ubuntu 18.04 LTS(其标准支持虽已于2023年4月结束,但 ESM 扩展安全维护仍持续至2028年,大量政企和金融类客户仍在使用,且其内核、glibc、Python 版本组合对老项目兼容性极佳)。这不是为了怀旧,而是因为——你接手的客户服务器很可能就是这台跑了五年的物理机,上面跑着 MySQL 5.7、Nginx 1.14、Python 3.6,而你不能一上来就要求重装系统。
这个应用解决的是一个非常具体、高频、且容易被低估的痛点:客户数据散、乱、脏、查不动。销售随手记在 Excel 里,客服在微信里留截图,财务用独立表格做回款登记,市场部的线索表和销售的跟进表字段对不上……最后老板问一句“上个月成交的高净值客户有哪些?他们最近三个月有没有新咨询?”,没人能在五分钟内给出准确答案。Django 提供了开箱即用的 ORM、Admin 后台、用户权限、数据校验和 REST API 快速生成能力;React 则提供了响应式列表、实时搜索、拖拽分组、多条件筛选、导出 PDF/Excel 等前端交互刚需。二者结合,不是为了炫技,而是为了把“数据录入—数据清洗—数据查询—数据决策”这条链路,在一周内跑通最小闭环。
适合谁来参考?三类人:第一类是刚从培训班出来的 Python 或前端新人,想拿一个完整、可部署、有真实业务逻辑的项目写进简历;第二类是小公司里的全栈工程师,老板说“下周一要上线一个客户登记页”,你得自己搭环境、写接口、做页面、配 Nginx;第三类是运维或 DevOps 工程师,需要一份在老旧但稳定的 Ubuntu 18.04 上部署现代 Web 应用的实操手册,不求最新,但求稳、可复现、无坑。这篇文章,就是我去年帮一家本地财税代理公司上线客户管理系统时,从零开始记录的全部过程,连apt-get update卡住时怎么换源都写了。
1.1 核心需求解析:客户数据管理到底要管什么?
很多人一上来就想“我要做个 CRM”,结果三天就卡在登录页。其实客户数据管理(Customer Data Management, CDM)在中小企业场景下,核心就四件事:存得准、找得快、看得清、动得稳。我们拆解一下:
存得准:不是简单地把姓名、电话、邮箱塞进数据库。它意味着:手机号必须符合国内 11 位格式(带正则校验),公司名称要自动去重并关联工商注册号(后期可对接天眼查 API),联系人职务要从预设下拉中选择(避免出现“CEO”“首席执行官”“一把手”三种写法),备注字段支持 Markdown 语法(方便记录会议纪要)。Django 的
ModelForm和clean_*方法在这里是救命稻草,比前端 JS 校验可靠十倍——因为后端校验是最后一道防线,用户关掉 JS 也能拦住脏数据。找得快:老板不会用 SQL。他需要的是:在搜索框里输入“张”字,立刻列出所有姓张的客户;再点一下“未跟进”标签,只看销售还没联系过的线索;再拖动时间滑块,限定为“近30天新增”。这就要求后端 API 支持多字段模糊搜索(
__icontains)、状态过滤(status__in=['new', 'pending'])、日期范围查询(created_at__range=[start, end]),前端 React 要用useEffect+debounce防抖,避免每敲一个字就发一次请求。我实测过,不加防抖,一个 500 条数据的列表,用户快速输入“北京科技”,会触发 8 次无意义请求,服务器 CPU 直接飙到 90%。看得清:数据不是堆在表格里就完了。销售需要一眼看到“这个客户上次沟通是 3 天前,承诺本周回款”,财务需要看到“该客户历史共付款 3 次,总金额 12.8 万,最近一笔是上月 15 日”。这就催生了两个关键视图:一个是主列表页的“状态徽章+最后跟进时间”列,另一个是点击客户后的详情页,里面嵌入了“沟通记录时间线”和“付款流水卡片”。React 的
useState和useReducer在这里管理局部状态比 Redux 简洁得多,尤其当你只有两个核心状态(当前客户 ID、当前选中的沟通记录 ID)时。动得稳:所谓“动”,是指数据变更操作。比如销售把一个客户从“意向”改为“成交”,系统必须同时:更新客户状态、创建一条新的“成交”日志、给负责人发站内信、触发邮件通知客户成功签约。Django 的
post_save信号机制是天然选择,它把业务逻辑和模型解耦,你改状态时不用手动写五条update语句。而 React 前端只需要调一个PATCH /api/customers/123/接口,剩下的由后端信号自动完成。这种设计,让后续加“合同生成”“发票开具”等功能时,前端几乎不用改代码。
提示:别一上来就搞“客户画像”“AI 推荐”。先确保基础 CRUD(增删改查)在 1000 条数据下响应时间 < 300ms,这才是真正的“现代”。
1.2 技术选型背后的硬逻辑:为什么是 Django + React,而不是 Django + Vue 或 Flask + React?
网上总有人争论“Vue 更轻量”“Flask 更灵活”,但在 Ubuntu 18.04 这个特定环境下,Django + React 是经过血泪验证的最优解。理由很实在,不是理念之争,全是运维和协作成本:
Django 的 Admin 后台是降维打击。客户临时要查某条数据,或者运营要批量修改 50 个客户的归属部门,你不需要写新接口、发新版本、重启服务。直接登录
https://yourdomain.com/admin/,用图形界面操作,Django 自动记录操作日志、校验权限、防止误删。我见过太多项目,因为没启用 Admin,导致每次数据救火都要开发介入,一个简单修改耗掉半天工时。而 Vue 的管理后台(如 Element Plus Admin)需要你从零搭路由、写权限控制、对接 API,至少多花 20 小时。React 的生态成熟度碾压同级框架。标题里没提 TypeScript,但实际项目中,我强制所有
.tsx文件开启严格模式。为什么?因为客户数据字段多(姓名、电话、邮箱、公司名、行业、规模、联系人、职务、地址、邮编、官网、成立时间、注册资本、法人、统一社会信用代码、主营业务、合作阶段、预算范围、决策链、历史沟通记录、附件文件……),用interface Customer明确定义类型,VS Code 能实时提示“customer.taxId可能为 undefined”,比运行时报错Cannot read property 'taxId' of null强一万倍。Vue 的类型支持直到 3.x 才完善,而 React + TS 的组合,在 2018 年就已是事实标准。Ubuntu 18.04 的软件源决定了技术栈上限。它的默认
apt源里,Python 是 3.6.9,Node.js 是 8.10.0(已废弃),Nginx 是 1.14.0。你强行curl -sL https://deb.nodesource.com/setup_18.x | sudo -E bash -安装 Node 18,大概率会和系统自带的libssl冲突,导致apt upgrade失败。所以我的方案是:后端用系统 Python(3.6.9),前端用nvm管理 Node 版本(我固定用 Node 14.21.3,它是最后一个支持 Ubuntu 18.04 的 LTS 版本),构建产物(build/目录)扔给 Nginx 静态托管。这样,后端依赖apt install python3-django,前端依赖nvm install 14.21.3 && nvm use 14.21.3,两条线互不干扰,运维同学照着文档copy-paste就能跑起来。Django REST Framework(DRF)是 API 开发的“瑞士军刀”。它内置的
ModelViewSet、Serializer、Pagination、FilterBackend,让你写一个客户列表 API,10 行代码搞定:# api/views.py from rest_framework import viewsets from .models import Customer from .serializers import CustomerSerializer class CustomerViewSet(viewsets.ModelViewSet): queryset = Customer.objects.all() serializer_class = CustomerSerializer filterset_fields = ['status', 'industry', 'created_at'] search_fields = ['name', 'company_name', 'contact_person']而 Flask + Flask-RESTful 需要手动写路由、解析参数、处理分页、拼接 SQL,同样功能至少 50 行。在交付压力大的项目里,省下的不是代码行数,是调试时间、是联调次数、是客户等待的耐心。
2. 环境准备与基础架构:在 Ubuntu 18.04 上搭建稳定底座
Ubuntu 18.04 是个“老派但可靠”的系统,它的优势在于稳定,劣势在于“太老”。很多新手一上来就sudo apt update && sudo apt upgrade,结果升级了一堆内核和库,导致原本好好的 Django 项目启动报错ImportError: cannot import name 'six'。所以,环境准备的第一原则是:最小干预,精准安装。我们只装必须的,版本锁定,源换稳。
2.1 系统初始化:换源、装基础工具、禁用无关服务
我拿到一台全新的 Ubuntu 18.04 云服务器(4C8G,100G SSD),第一件事不是装 Python,而是先让它“听话”。以下是我在/root/init-server.sh里写的标准化脚本,每次新机器都跑一遍:
#!/bin/bash # 1. 备份原 sources.list cp /etc/apt/sources.list /etc/apt/sources.list.bak # 2. 替换为阿里云镜像源(国内最快,且 18.04 有完整支持) sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list # 3. 更新索引(注意:不 upgrade!只 update) apt-get update -y # 4. 安装基础工具:git(拉代码)、curl(下载)、vim(编辑)、htop(监控)、unzip(解压) apt-get install -y git curl vim htop unzip # 5. 禁用 IPv6(Django 开发服务器有时会因 IPv6 绑定失败) echo "net.ipv6.conf.all.disable_ipv6 = 1" >> /etc/sysctl.conf echo "net.ipv6.conf.default.disable_ipv6 = 1" >> /etc/sysctl.conf sysctl -p # 6. 关闭防火墙(UFW),让 Nginx 和 Django 开发服务器能自由通信 ufw disable注意:
apt-get upgrade是雷区。Ubuntu 18.04 的upgrade会升级systemd、glibc等核心组件,而 Django 3.2(我们项目用的版本)在某些新版glibc下会出现locale相关的编码错误。所以,我们只update,不upgrade。安全补丁通过apt-get install --only-upgrade单独安装,比如sudo apt-get install --only-upgrade python3.6。
执行完这个脚本,服务器就干净了。接下来是 Python 环境。Ubuntu 18.04 自带 Python 3.6.9,这是个好消息——Django 3.2 官方支持的最低 Python 版本就是 3.6,且 3.6.9 是 3.6 系列的最终稳定版,bug 最少。我们不需要pyenv或conda,直接用系统 Python,省去版本管理的复杂度。
# 验证 Python 版本 python3 --version # 输出:Python 3.6.9 # 安装 pip(如果没装) apt-get install -y python3-pip # 升级 pip 到最新兼容版(pip 21.3.1 是最后一个支持 Python 3.6 的版本) pip3 install --upgrade "pip<22.0" # 安装虚拟环境(venv 是 Python 3.6 内置模块,无需额外装) python3 -m venv /opt/myproject/env source /opt/myproject/env/bin/activate为什么用/opt/myproject/env而不是~/venv?因为/opt是 Linux 标准的“第三方应用安装目录”,权限清晰(root:root),且不会被普通用户误删。虚拟环境激活后,所有pip install都只影响这个隔离空间,不影响系统 Python。
2.2 Django 后端:从零初始化项目结构
现在,我们进入/opt/myproject/目录,开始 Django 项目。记住,Django 项目不是“一个文件夹”,而是一个有严格层级的工程。我坚持用以下结构,它让团队协作、CI/CD、后期维护都无比清晰:
/opt/myproject/ ├── backend/ # Django 项目根目录(manage.py 所在) │ ├── manage.py │ ├── backend/ # 项目配置包(settings.py, urls.py 等) │ │ ├── __init__.py │ │ ├── settings/ │ │ │ ├── __init__.py │ │ │ ├── base.py # 公共配置(DEBUG=False, SECRET_KEY 等) │ │ │ ├── local.py # 本地开发配置(DEBUG=True, sqlite3) │ │ │ └── production.py # 生产配置(DEBUG=False, PostgreSQL, Redis) │ │ ├── urls.py │ │ └── wsgi.py │ ├── customers/ # 核心业务 App(客户管理) │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── models.py │ │ ├── serializers.py # DRF 序列化器 │ │ ├── views.py # API 视图 │ │ └── migrations/ │ └── api/ # API 路由聚合 App(可选,用于集中管理所有 API URL) │ ├── __init__.py │ └── urls.py ├── frontend/ # React 项目根目录(package.json 所在) │ ├── public/ │ ├── src/ │ ├── package.json │ └── ... └── nginx/ # Nginx 配置文件(便于一键部署) └── myproject.conf创建步骤(全部在backend/目录下执行):
# 1. 创建 backend 目录并初始化 Django 项目 mkdir -p /opt/myproject/backend cd /opt/myproject/backend django-admin startproject backend . # 2. 创建 customers App python manage.py startapp customers # 3. 创建 api App(用于聚合所有 API 路由) python manage.py startapp api关键配置在backend/settings/base.py。这里我贴出最核心的几段,它们决定了整个项目的健壮性:
# backend/settings/base.py import os from pathlib import Path from decouple import config # 用 python-decouple 管理敏感配置 BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent # 回到 /opt/myproject/ # 安全配置(生产环境必须) SECRET_KEY = config('SECRET_KEY', default='dev-secret-key-change-in-prod') DEBUG = config('DEBUG', default=False, cast=bool) ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1').split(',') # 数据库(生产用 PostgreSQL,开发用 SQLite) if DEBUG: DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } else: DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': config('DB_NAME'), 'USER': config('DB_USER'), 'PASSWORD': config('DB_PASSWORD'), 'HOST': config('DB_HOST', default='localhost'), 'PORT': config('DB_PORT', default='5432'), } } # 静态文件(Django 管理的 CSS/JS,非 React 构建产物) STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' # collectstatic 输出目录 STATICFILES_DIRS = [ BASE_DIR / 'backend' / 'static', ] # Django REST Framework 配置 REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 20, 'DEFAULT_FILTER_BACKENDS': [ 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.SearchFilter', 'rest_framework.filters.OrderingFilter', ], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', # 为 React 前端提供 Token 认证 ], }实操心得:
decouple库是必装的。它让你把SECRET_KEY、数据库密码等敏感信息,放在项目根目录外的.env文件里(/opt/myproject/.env),而代码里只写config('SECRET_KEY')。这样,.env文件可以加到.gitignore,永远不会被提交到 Git,也避免了在服务器上cat settings.py就泄露密码的低级错误。
2.3 React 前端:用 Create React App 初始化并适配 Django
前端我坚持用create-react-app(CRA),尽管它被诟病“臃肿”,但在 Ubuntu 18.04 上,它的稳定性是其他脚手架(Vite、Next.js)无法比拟的。Vite 的esbuild在 Node 14 下偶发编译失败,Next.js 的服务端渲染在 Django 后端集成时会引发 CORS 和 Cookie 问题。CRA 的react-scripts3.4.4(最后一个支持 Node 14 的版本)是久经考验的。
# 进入项目根目录 cd /opt/myproject # 用 nvm 安装并切换到 Node 14.21.3 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" nvm install 14.21.3 nvm use 14.21.3 # 创建 React 项目 npx create-react-app@4.0.3 frontend --template typescript # 注意:必须指定 @4.0.3 和 --template typescript,这是 CRA 对 Node 14 的最终支持版本CRA 默认的开发服务器(localhost:3000)和 Django 开发服务器(localhost:8000)端口不同,会产生跨域问题。解决方案不是在 Django 里装django-cors-headers(那是在生产环境兜底),而是在开发时用 CRA 内置的proxy功能。在frontend/package.json里加一行:
{ "name": "frontend", "proxy": "http://localhost:8000", "dependencies": { ... } }这样,前端代码里写fetch('/api/customers/'),CRA 开发服务器会自动代理到http://localhost:8000/api/customers/,浏览器看到的仍是同源请求,完美规避 CORS。这个技巧,比配 Nginx 反向代理简单十倍,且只在npm start时生效,不影响生产构建。
提示:
proxy只支持单一目标。如果你的 API 分属多个后端(比如客户数据在 Django,支付数据在另一个 Java 服务),那就必须上 Nginx 做反向代理,这是生产环境的标准做法。
3. 核心功能实现:客户模型、API 接口与 React 页面联动
现在,后端骨架和前端骨架都搭好了。我们进入最核心的部分:让“客户数据”真正流动起来。这个过程不是“写代码”,而是“定义契约”——Django 模型定义数据结构,DRF Serializer 定义 API 契约,React 组件定义用户界面。三者必须严丝合缝,否则前端永远在console.log(response)里猜字段名。
3.1 Django 模型设计:从现实业务中提炼字段
客户数据不是拍脑袋想的。我参考了《销售漏斗管理》和《企业客户分级标准》两本书,结合财税代理公司的实际业务,定义了Customer模型。它看起来很长,但每一行都有业务依据:
# backend/customers/models.py from django.db import models from django.contrib.auth.models import User from django.core.validators import RegexValidator # 手机号正则(国内 11 位,以 1 开头) phone_regex = RegexValidator( regex=r'^1[3-9]\d{9}$', message="手机号必须是 11 位数字,且以 1 开头" ) class Customer(models.Model): # 基础信息 name = models.CharField("姓名", max_length=100) phone = models.CharField("手机号", validators=[phone_regex], max_length=11, unique=True) email = models.EmailField("邮箱", blank=True) # 公司信息 company_name = models.CharField("公司名称", max_length=200) industry = models.CharField("所属行业", max_length=100, choices=[ ('IT', '信息技术'), ('FINANCE', '金融'), ('MANUFACTURING', '制造业'), ('EDUCATION', '教育'), ('HEALTH', '医疗健康'), ('OTHER', '其他'), ]) company_size = models.CharField("公司规模", max_length=50, choices=[ ('1-10', '1-10人'), ('11-50', '11-50人'), ('51-200', '51-200人'), ('201-1000', '201-1000人'), ('1000+', '1000人以上'), ]) # 联系人信息 contact_person = models.CharField("联系人", max_length=100, blank=True) contact_position = models.CharField("职务", max_length=100, blank=True) # 业务状态 STATUS_CHOICES = [ ('new', '新线索'), ('contacted', '已联系'), ('meeting', '已面谈'), ('proposal', '方案中'), ('negotiating', '谈判中'), ('won', '已成交'), ('lost', '已流失'), ] status = models.CharField("当前状态", max_length=20, choices=STATUS_CHOICES, default='new') # 时间戳 created_at = models.DateTimeField("创建时间", auto_now_add=True) updated_at = models.DateTimeField("更新时间", auto_now=True) last_contacted = models.DateTimeField("最后联系时间", null=True, blank=True) # 关联 owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="负责人") class Meta: verbose_name = "客户" verbose_name_plural = "客户" ordering = ['-created_at'] def __str__(self): return f"{self.name} ({self.company_name})"关键设计点:
phone字段加了unique=True和RegexValidator,确保数据库层面强制唯一性和格式正确。前端 JS 校验只是锦上添花,后端才是底线。industry和company_size用choices,而不是自由文本。这保证了数据统计的准确性——你不可能对“IT”和“信息技术”做GROUP BY。owner是ForeignKey到User,这是权限管理的基础。一个销售只能看到自己负责的客户,Django 的request.user就是天然的过滤器。last_contacted是DateTimeField(null=True),不是auto_now。因为“最后联系时间”是业务动作,不是数据更新时间,必须由业务逻辑(比如点击“已联系”按钮)显式设置。
创建迁移并同步数据库:
# 在 backend/ 目录下 python manage.py makemigrations python manage.py migrate3.2 DRF API 接口:用 ViewSet 快速暴露数据
有了模型,API 就水到渠成。我们用ModelViewSet,因为它自动生成了list、retrieve、create、update、destroy五个标准动作,覆盖 90% 的 CRUD 场景。
# backend/customers/views.py from rest_framework import viewsets, permissions from rest_framework.decorators import action from rest_framework.response import Response from django_filters.rest_framework import DjangoFilterBackend from .models import Customer from .serializers import CustomerSerializer class CustomerViewSet(viewsets.ModelViewSet): queryset = Customer.objects.all() serializer_class = CustomerSerializer permission_classes = [permissions.IsAuthenticated] # 登录用户才能访问 filter_backends = [DjangoFilterBackend, SearchFilter] filterset_fields = ['status', 'industry', 'company_size'] search_fields = ['name', 'company_name', 'contact_person', 'email'] # 重写 get_queryset,让每个用户只看到自己的客户 def get_queryset(self): return Customer.objects.filter(owner=self.request.user) # 自定义动作:标记为“已联系” @action(detail=True, methods=['post']) def mark_contacted(self, request, pk=None): customer = self.get_object() customer.status = 'contacted' customer.last_contacted = timezone.now() customer.save() return Response({'status': '已标记为已联系'})对应的序列化器(serializers.py)定义了 API 的输入输出格式:
# backend/customers/serializers.py from rest_framework import serializers from .models import Customer class CustomerSerializer(serializers.ModelSerializer): # 将 owner 字段显示为用户名,而不是 user id owner = serializers.StringRelatedField(read_only=True) # 将 status 字段显示为中文描述,而不是英文 key status_display = serializers.CharField(source='get_status_display', read_only=True) class Meta: model = Customer fields = '__all__' # 创建时,owner 字段由后端自动赋值,前端不传 read_only_fields = ['owner', 'created_at', 'updated_at', 'last_contacted']路由配置(api/urls.py)聚合所有 API:
# backend/api/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from customers import views as customer_views router = DefaultRouter() router.register(r'customers', customer_views.CustomerViewSet) urlpatterns = [ path('', include(router.urls)), ]然后在主urls.py中引入:
# backend/backend/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('api.urls')), # 所有 API 都在 /api/ 下 ]启动 Django 开发服务器测试:
python manage.py runserver 0.0.0.0:8000访问http://your-server-ip:8000/api/customers/,你应该能看到一个 JSON 列表,里面有count、next、results字段,这是 DRF 分页和过滤的默认行为。这就是契约的起点。
3.3 React 前端:用 TypeScript 定义类型并调用 API
前端的核心是类型安全。我们在frontend/src/types/customer.ts里定义与后端完全一致的接口:
// frontend/src/types/customer.ts export interface Customer { id: number; name: string; phone: string; email: string; company_name: string; industry: 'IT' | 'FINANCE' | 'MANUFACTURING' | 'EDUCATION' | 'HEALTH' | 'OTHER'; company_size: '1-10' | '11-50' | '51-200' | '201-1000' | '1000+'; status: 'new' | 'contacted' | 'meeting' | 'proposal' | 'negotiating' | 'won' | 'lost'; status_display: string; // 后端返回的中文状态 owner: string; // 用户名 created_at: string; // ISO 8601 格式 updated_at: string; last_contacted: string | null; } export interface CustomerListResponse { count: number; next: string | null; previous: string | null; results: Customer[]; }然后,用useEffect和useState获取数据:
// frontend/src/pages/CustomerListPage.tsx import React, { useState, useEffect } from 'react'; import { Customer, CustomerListResponse } from '../types/customer'; const CustomerListPage: React.FC = () => { const [customers, setCustomers] = useState<Customer[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { const fetchCustomers = async () => { try { setLoading(true); const response = await fetch('/api/customers/'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data: CustomerListResponse = await response.json(); setCustomers(data.results); } catch (err) { setError(err instanceof Error ? err.message : '未知错误'); } finally { setLoading(false); } }; fetchCustomers(); }, []); if (loading) return <div>加载中...</div>; if (error) return <div>错误:{error}</div>; return ( <div> <h1>客户列表</h1> <table> <thead> <tr> <th>姓名</th> <th>公司</th> <th>状态</th> <th>最后联系</th> </tr> </thead> <tbody> {customers.map(customer => ( <tr key={customer.id}> <td>{customer.name}</td> <td>{customer.company_name}</td> <td>{customer.status_display}</td> <td>{customer.last_contacted ? new Date(customer.last_contacted).toLocaleDateString() : '-'}</td> </tr> ))} </tbody> </table> </div> ); }; export default CustomerListPage;注意:
fetch('/api/customers/')能工作,是因为前面配置了package.json的proxy。如果未来要部署到生产环境,这个路径会变成绝对 URL,比如https://api.yourdomain.com/customers/,这时就需要在 React 里用环境变量管理 API 基础地址。
4. 生产环境部署:Nginx + Gunicorn + PostgreSQL 全流程
开发环境跑通了,下一步是让应用在 Ubuntu 18.04 上“活”起来。生产部署不是“复制粘贴”,而是一系列严谨的、有因果关系的步骤。我把它拆成四个环节:数据库准备、后端服务化、前端静态化、反向代理整合。漏掉任何一个,都会导致 502 Bad Gateway 或白屏。
4.1 PostgreSQL 数据库:比 SQLite 更可靠的选择
Ubuntu 18.04 的apt源里,PostgreSQL 是 10.22 版本,足够稳定。我们不用最新版,因为老版本的 bug 更少,文档更全。
# 安装 PostgreSQL 和客户端 apt-get install -y postgresql postgresql-contrib # 切换到 postgres 用户,创建数据库和用户 sudo -u postgres psql <<EOF CREATE DATABASE myproject; CREATE USER myprojectuser WITH PASSWORD 'strongpassword123'; ALTER ROLE myprojectuser SET client_encoding TO 'utf8'; ALTER ROLE myprojectuser SET default_transaction_isolation TO 'read committed'; ALTER ROLE myprojectuser SET timezone TO 'UTC'; GRANT ALL PRIVILEGES ON DATABASE myproject TO myprojectuser; \q EOF然后,修改 Django 的production.py配置:
# backend/settings/production.py from .base import * DEBUG = False ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com'] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'myproject