目录
[1. 我们面临的三个核心问题](#1. 我们面临的三个核心问题)
[2. 场景实战:合同列表展示](#2. 场景实战:合同列表展示)
[2.1 传统 Django 模版系统实现](#2.1 传统 Django 模版系统实现)
[2.2 解耦后的 Django + DRF + 前端分离实现](#2.2 解耦后的 Django + DRF + 前端分离实现)
[3. 进阶技术实践](#3. 进阶技术实践)
[3.1 认证升级:集成 JWT 与自定义响应](#3.1 认证升级:集成 JWT 与自定义响应)
[3.2 复杂业务逻辑处理:APIView 的灵活性](#3.2 复杂业务逻辑处理:APIView 的灵活性)
[4. 遇到的挑战与解决方案](#4. 遇到的挑战与解决方案)
[5. 总结](#5. 总结)
在智能合同审查系统的开发演进过程中,我们经历了一次重要的架构转型:从传统的 Django 模版系统(MVT)迁移到了前后端分离的 API 驱动架构(Django REST Framework + Vue 3)。
这一转变并非盲目跟风,而是为了解决我们在初期开发中遇到的三个核心痛点。本文将结合具体的代码示例,详细对比这两种实现方式,并阐述重构带来的收益。
1. 我们面临的三个核心问题
在项目初期,我们使用了 Django 原生的模版系统。随着业务复杂度的增加,以下问题日益凸显:
耦合度高:前端页面逻辑(HTML/CSS/JS)与后端业务逻辑(Python View)紧密缠绕。修改一个按钮的样式,可能需要后端开发人员介入修改模版文件;后端修改数据结构,又极易打破前端的渲染逻辑。
交互体验差:传统的 Web 应用是多页应用(MPA)。用户每次点击"下一页"、提交表单或进行筛选,浏览器都需要向服务器请求完整的 HTML 页面并重新加载。这种"白屏-加载-渲染"的循环无法提供类似原生应用的流畅体验。
接口复用难:模版系统直接返回渲染好的 HTML 字符串。如果未来我们需要开发移动端 App 或微信小程序,现有的视图逻辑完全无法复用,必须重新开发一套返回 JSON 数据的 API。
2. 场景实战:合同列表展示
为了更直观地说明区别,我们以"获取并展示用户合同列表"这一高频场景为例,分别展示两种架构下的代码实现
2.1 传统 Django 模版系统实现
在传统模式下,后端 View 负责查询数据并将其"填"入 HTML 模版中,浏览器接收到的是最终的 HTML 页面
后端视图 (Views.py)
python
# 传统 Django View
from django.shortcuts import render
from .models import Contract
def contract_list(request):
# 1. 业务逻辑:查询当前用户的未删除合同
contracts = Contract.objects.filter(
uploader=request.user,
is_deleted=False
).order_by('-created_at')
context = {
'contracts': contracts,
'username': request.user.username
}
# 2. 渲染:将数据与 HTML 模版混合,返回完整的 HTML 页面
return render(request, 'contract/contract_list.html', context)
前端模版 (contract_list.html)
python
<!-- Django Template Language (DTL) -->
{% extends "base.html" %}
{% block content %}
<div class="contract-container">
<h2>{{ username }} 的合同列表</h2>
<table>
<thead>
<tr>
<th>合同编号</th>
<th>合同名称</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<!-- 逻辑与视图耦合:在 HTML 中写循环 -->
{% for contract in contracts %}
<tr>
<td>{{ contract.contract_no }}</td>
<td>{{ contract.name }}</td>
<td>{{ contract.get_status_display }}</td>
<td>
<!-- 每次点击都会触发页面完全刷新 -->
<a href="/contract/{{ contract.id }}/">查看</a>
</td>
</tr>
{% empty %}
<tr><td colspan="4">暂无合同</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
局限性分析:
- 强耦合 :前端开发人员必须懂 Django 模版({% for contract in contracts %}、{{ }})语法,无法独立使用 Vue/React 等现代工具。
- 体验卡顿 :用户想要按"状态"筛选合同,必须点击按钮 -> 浏览器跳转 -> 服务器查询 -> 返回新 HTML-> 浏览器重绘。整个过程用户会感觉到明显的页面刷新
- 接口复用难 :如果明天要做 App,我们不能直接返回 HTML 给 App。所以不得不再写一套 API(通常是 JSON 格式),这就意味着同样的数据逻辑(获取文章列表)要写两遍,或者做复杂的判断。
2.2 解耦后的 Django + DRF + 前端分离实现
重构后,
后端 仅充当数据提供者(Data Provider),通过 RESTful API 返回 JSON数据;
前端作为独立应用(Vue 3),负责页面渲染和交互
第一步:定义序列化器 (Serializers.py)
后端首先定义"如何将数据库对象转换为 JSON 格式"
python
# backend/contract/serializers.py
from rest_framework import serializers
from .models import Contract
class ContractListSerializer(serializers.ModelSerializer):
# 自动处理关联字段,例如获取上传者的用户名
uploader_name = serializers.CharField(source='uploader.username', read_only=True)
# 自动获取 choices 字段的显示文本(如 "reviewing" -> "审查中")
status_display = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = Contract
# 明确指定返回给前端的字段,按需获取
fields = ['id', 'name', 'contract_no', 'status', 'status_display', 'uploader_name', 'updated_at']
第二步:编写 API 视图 (Views.py)
视图不再关注 HTML,只关注数据处理和权限
python
# backend/contract/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions
from .models import Contract
from .serializers import ContractListSerializer
class ContractView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
"""
获取合同列表 API
返回格式:JSON
"""
# 1. 业务逻辑
queryset = Contract.objects.filter(
uploader=request.user,
is_deleted=False
).order_by('-created_at')
# 2. 序列化:将 QuerySet 转换为 JSON 兼容的字典/列表
serializer = ContractListSerializer(queryset, many=True)
# 3. 响应:返回纯数据
return Response({
'code': 200,
'msg': '获取成功',
'data': serializer.data
})
第三步:前端异步调用 (Vue 3)
前端完全独立,通过 AJAX (Axios) 请求数据,并在本地动态渲染
python
// frontend/src/views/contract/ContractHome.vue (Script部分)
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import request from '@/utils/http-axios'
// 定义纯粹的数据接口,不关心后端实现
interface ContractItem {
id: number
name: string
contract_no: string
status_display: string
}
const contractList = ref<ContractItem[]>([])
const loading = ref(false)
// 异步获取数据,页面无需刷新
const fetchContracts = async () => {
loading.value = true
try {
const res = await request.get('/api/contracts/')
if (res.data.code === 200) {
contractList.value = res.data.data
}
} finally {
loading.value = false
}
}
onMounted(() => {
fetchContracts()
})
</script>
// Template 部分 (省略样式)
<template>
<div v-loading="loading">
<div v-for="item in contractList" :key="item.id" class="contract-card">
<h3>{{ item.name }}</h3>
<p>状态: {{ item.status_display }}</p>
</div>
</div>
</template>
重构后的收益:
- 彻底解耦:后端开发人员只关注 API 文档和数据正确性;前端开发人员可以使用 Vue 生态的所有组件库(如 Element Plus),开发效率大幅提升
- 极致体验:页面加载后,切换筛选条件或翻页时,JavaScript 只需请求微小的 JSON 数据并局部更新 DOM,用户几乎感觉不到延迟,实现了"单页应用 (SPA)"的丝滑体验
- 一次开发,多端复用 :
/api/contracts/接口返回的是标准的 JSON 数据
- Web端:Vue 解析 JSON 渲染表格
- 移动端:iOS/Android App 解析同样的 JSON 渲染原生列表
- 第三方集成:合作伙伴可以通过该 API 获取合同状态
- 无需为新客户端重写任何后端代码
3. 进阶技术实践
除了基础的 CRUD 接口,DRF 在处理复杂业务场景时也展现出了强大的灵活性
3.1 认证升级:集成 JWT 与自定义响应
为了适应前后端分离的无状态特性,我们放弃了传统的 Session 认证,改用 JSON Web Token (JWT)。默认的 JWT 接口只返回 access 和 refresh token,但在实际业务中,我们希望在登录成功后直接返回用户的基本信息(如角色、头像),以减少前端的二次请求
我们可以通过继承 TokenObtainPairSerializer 来实现自定义响应:
python
# backend/user/serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
data = super().validate(attrs) # 获取默认的 token 结果
# 注入额外的用户信息
data['user'] = {
'username': self.user.username,
'age': self.user.age,
'role': self.user.role,
'role_display': self.user.get_role_display()
}
return data
3.2 复杂业务逻辑处理:APIView 的灵活性
虽然 DRF 提供了 ModelViewSet 来快速生成标准接口,但对于"合同上传"这种涉及多步操作的复杂业务,我们更倾向于使用 APIView 来获得完全的控制权
例如,一个上传请求需要同时完成:文件存入 OSS -> 获取签名 URL -> OCR 解析 -> 存入数据库 -> 创建版本记录
python
# backend/contract/views.py
class ContractView(APIView):
parser_classes = [MultiPartParser, FormParser] # 支持文件上传
def post(self, request):
file_obj = request.FILES.get('file')
# 1. 上传至阿里云 OSS
oss_object_name = upload_contract(file_obj)
mock_file_url = get_signed_url(oss_object_name)
# 2. 调用 OCR 解析合同文本
contract_text = parse_contract_from_url(mock_file_url)
# 3. 序列化验证与数据保存
data = request.data.copy()
serializer = ContractSerializer(data=data)
if serializer.is_valid():
contract = serializer.save(uploader=request.user)
# 4. 手动创建初始版本记录(原子性操作的一部分)
ContractVersion.objects.create(
contract=contract,
version=1,
file_url=mock_file_url,
# ...
)
return Response({'code': 200, 'msg': '上传成功'})
4. 遇到的挑战与解决方案
在重构过程中,我们也遇到了一些典型的"分离焦虑",以下是我们的解决方案:
挑战一:跨域资源共享 (CORS)
前后端分离后,前端(如 localhost:5173)访问后端(localhost:8000)会遇到浏览器的同源策略限制
解决方案 :
安装
django-cors-headers中间件,并在settings.py中配置CORS_ALLOWED_ORIGINS,明确允许前端开发服务器的域名访问 API
挑战二:文件上传与静态资源管理
传统的 Django 静态文件管理(MEDIA_ROOT)在分离架构下不再适用,特别是涉及用户上传的敏感合同文件,直接暴露在文件系统中存在安全隐患且难以扩展
解决方案 :
引入阿里云 OSS 对象存储。后端不再存储物理文件,只负责生成上传凭证或接收文件流转发至 OSS,数据库仅存储文件的 Key 或 URL。前端通过后端返回的临时签名 URL 访问文件,既保证了安全性(链接有时效性),又减轻了应用服务器的带宽压力
5. 总结
通过引入 Django REST Framework,我们不仅解决了"耦合度高、体验差、复用难"这三大顽疾,更为系统的未来扩展打下了坚实基础。现在,我们的智能合同审查系统拥有了清晰的边界:后端是稳定高效的数据与逻辑中心,前端则是灵活多变的交互界面。