简介:一份基于 Django 框架的文档管理系统完整源码,面向 Python Web 开发者与 Django 学习者,覆盖文档创建、存储、管理、权限控制等典型业务,适合用于课程设计、毕业设计或企业内部文档管理初始原型。压缩包共 64 个文件,其中 15 个 Python 源码负责模型、视图、配置等后端逻辑,12 个 CSS 与 10 个 JavaScript 构建前端样式与交互,3 个 HTML 模板提供页面结构,其余为字体、地图、配置文件等静态资源,整包仅 1.62MB,结构紧凑。已有 1891 人学习下载。项目遵循 Django 标准工程布局,包含管理脚本、配置文件、URL 路由及 WSGI 接口等核心模块,并在核心应用模块中实现用户认证、模型定义、视图业务、模板渲染、表单验证、文件上传与文档搜索等关键功能;同时附带静态资源与模板目录,可快速运行和二次开发。通过阅读和调试本套源码,能够深入理解 Django 的 MTV 架构、ORM 数据库操作及前后端协作方式,为独立构建 Web 系统积累实战经验。
1. 一套解压就能跑的 Django 文档管理系统:先想清楚你拿它做什么
接手别人解压好的 Python Django 文档管理系统源码时,最容易被项目里一堆文件吓到:settings.py、urls.py、几个应用目录、一整套 migrations,再夹着一个 manage.py。这套基于 Django 的文档管理系统源码,解决的是“把散落在本机、网盘、聊天记录里的文档,统一上传、归类、检索、按权限下载”这个真实诉求。它适合三类人:想学 Django 完整落地流程的 Python 开发、要给内部团队搭一个轻量文档后台的运维或全栈工程师、以及需要一个能讲清楚业务闭环的毕设起点。它不是一个对标网盘的产品,但足够让你在一个晚上跑通核心链路,之后按需改造。
2. 环境准备与项目结构:从 zip 到浏览器出现登录页的半小时
2.1 解压后先确认 Python 与 Django 版本是否匹配
这一步最容易被跳过,也最常翻车。源码里如果用 Django 4.2,那 Python 3.9 到 3.12 都能跑;如果老工程还在用 Django 2.x,Python 3.8 以下才稳。先看 requirements.txt 或者 setup.py,再执行 python --version。我一般会先解压,全局搜一遍 requirements.txt,确认版本约束,不急着敲 pip install。
python --version # 输出示例:Python 3.10.12,基本兼容 Django 4.x 常用写法逻辑说明:python 和 python3 在部分 Linux 发行版上是两个命令。如果你刚装完 Python,却发现执行的是旧版本,往往是 PATH 优先级问题;Windows 上还要注意 Microsoft Store 版本可能不是预期路径。确认版本后再进下一步,能省掉后面一半报错。
2.2 用虚拟环境复现依赖,重建项目目录
进入项目目录,创建虚拟环境并激活,然后安装依赖。这一步的价值是把项目依赖和系统 Python 环境隔离开,不污染其他 Django 工程,也不被其他工程污染。
cd 你的项目目录 python -m venv venv # Windows: venv\Scripts\activate # Linux/macOS: source venv/bin/activate pip install -r requirements.txt逻辑说明:python -m venv venv 会在当前目录生成一个 venv 文件夹,里面是一套独立的 Python 解释器和 pip。激活后,终端提示符会变成 (venv)。install 命令会按 requirements.txt 里锁定的版本号安装依赖,建议顺手执行 pip list 看一眼实际装出来的版本,别直接用 pip freeze 覆盖原文件——那会把环境里多余的东西也写进去。如果 requirements.txt 缺失或者太老,常见做法是只装最小依赖先让项目跑起来,随后按 import 报错逐个补。
### 2.3 数据库迁移与超级用户:admin 后台先跑起来 任何 Django 源码工程,跑到浏览器出现页面之前,都要先处理数据库。默认配置通常是 SQLite,迁移命令执行后会在项目根目录生成一个 db.sqlite3 文件,不需要额外安装数据库服务。确认当前工程包含哪些数据库时,看 settings.py 里的 DATABASES 配置最有说服力。 ```bash python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000参数说明:makemigrations 是把模型定义生成迁移文件;如果源码包已经把 migrations 文件带上了,第一行会提示没有任何改动,这很正常,直接进第二行。migrate 是把迁移真正应用到数据库,会在库中生成 django_migrations 表记录版本。createsuperuser 交互式要求输入用户名、邮箱和密码,注意密码在终端里不回显,不是键盘坏了。最后 runserver 后面跟的 0.0.0.0:8000 表示监听所有网卡,这样局域网内其它机器可以访问,如果只需要本机调试,去掉 IP 直接 python manage.py runserver 就行。
跑完这四个命令,浏览器打开 http://127.0.0.1:8000/admin 看到登录页,这套源码就算被复现成功了。下一步才是判断它到底值不值得继续投入。
3. 把源码拆开看:Django 的 MTV 在文档管理系统里各管什么
3.1 按 MTV 定位源码:models.py 是数据表,views.py 是业务,templates 是页面
Django 的 MTV 和常见 MVC 概念一一对应,但很多人第一次读源码时,还是会对着目录结构发懵。一个文档管理系统最典型的源码布局是:项目根目录里有一个 manage.py,和一个与项目同名的配置目录,里面是 settings.py、urls.py;业务代码则放在独立的 app 目录里,常见的 app 名叫 documents、filemanage 或类似的名字。app 内部最重要的四个文件是 models.py、views.py、admin.py、urls.py,加上一个 templates 目录放页面模板。
理解了这几个文件的职责,读源码的顺序就有了:先读 models.py 搞清楚有哪些数据表,再看 views.py 搞清楚每个视图对数据做了什么操作,然后顺着 urls.py 把 URL 和视图函数对应上,模板文件是最后一步。按照这个顺序读,半小时就能把整个项目的脉络摸清。如果源码头一次跑起来就报错,大多数问题也出在这几个文件之间的配置不匹配上。
3.2 读懂三张核心表:Category、Document、User 怎么分工
文档管理系统最核心的数据模型,万变不离这三张表:分类表、文档表、用户表。用户表通常直接复用 Django 自带的 auth.User,不需要自己重写;真正要理解的是文档表和分类表,以及它们之间怎么关联。源码工程里常见的写法是这样的:
from django.db import models from django.contrib.auth.models import User class Category(models.Model): name = models.CharField(max_length=50, unique=True) parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE) class Meta: verbose_name = '分类' verbose_name_plural = '分类' def __str__(self): return self.name class Document(models.Model): title = models.CharField(max_length=200, verbose_name='标题') file = models.FileField(upload_to='documents/%Y/%m/', max_length=250) category = models.ForeignKey(Category, null=True, blank=True, on_delete=models.SET_NULL) uploader = models.ForeignKey(User, on_delete=models.CASCADE) description = models.TextField(blank=True, verbose_name='描述') uploaded_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-uploaded_at']逻辑说明:Category 里那个 parent 外键指向自己,是为了让分类可以无限层级嵌套,比如“技术文档/后端/Django 部署”这样三级分类。you 要注意 on_delete=models.CASCADE 表示删除父分类时,子分类一并删除;如果你的需求希望保留子分类,就改用 SET_NULL,但前提是外键字段要允许为空。Document 里的 file 字段用 FileField 而不是 CharField 存路径,Django 会自动把上传的文件落到 MEDIA_ROOT 对应目录。upload_to 里的 %Y/%m/ 是日期占位符,系统会按月自动建目录,顺带解决了文件太多时单目录列表爆掉的问题。uploaded_at 设置为 auto_now_add,记录创建时间且后续不可修改,这在审计上传行为时非常关键。
u这里有一个新手容易理解错的地方:uploader 虽然叫外键,但它在数据库里实际生成的列名是 uploader_id,存的是整数主键。查询时可以 chain 或者 select_related 取关联对象,这个话题后面展开。
3.3 admin 后台先跑通:先验证模型,再写前端
很多人拿到源码第一件事是想改前端页面,但我的习惯是先把 admin 配置好,用 Django 自带后台验证一遍数据模型和业务逻辑是否自洽。admin 不是摆设,它是整个系统最先能用的管理界面。给上面两个模型补上 admin 注册,源码才能从“能跑”变成“能用”。
from django.contrib import admin from .models import Category, Document @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display = ('name', 'parent') search_fields = ('name',) @admin.register(Document) class DocumentAdmin(admin.ModelAdmin): list_display = ('title', 'category', 'uploader', 'uploaded_at') list_filter = ('category', 'uploaded_at') search_fields = ('title', 'description')参数说明:list_display 决定列表页显示哪些列,是把关联对象显示成字符串;如果关联对象太多,会出现每个单元格都查一次数据库的 N+1 问题,这时要在 get_queryset 里加 select_related。list_filter 会在右侧生成筛选面板,按分类和上传时间过滤,这是文档管理最常用的两个维度。search_fields 指定搜索时匹配的字段,Django 会生成一条带 LIKE 的查询。
在这个阶段,你不需要写任何 HTML,只需要在浏览器里把分类建几个、传几个测试文档,确认文件确实上传到了 MEDIA_ROOT。这比直接改模板能更快暴露模型层面的设计问题。
4. 二次开发落地:上传、检索、权限三件事的常见做法
4.1 上传文件的视图写法:Form + FileField 的取舍
admin 后台只是管理入口,真实业务要向外提供上传页面。Django 项目实战里最常见的做法是用 ModelForm 加函数视图,而不是一上来就上 Django REST Framework。原因很简单:这个场景不需要 API,表单提交加重定向就够。给 Document 写一个 ModelForm,再配一个视图。
# forms.py from django import forms from .models import Document class DocumentForm(forms.ModelForm): class Meta: model = Document fields = ['title', 'file', 'category', 'description'] # views.py from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from .forms import DocumentForm @login_required def upload_document(request): if request.method == 'POST': form = DocumentForm(request.POST, request.FILES) if form.is_valid(): doc = form.save(commit=False) doc.uploader = request.user doc.save() return redirect('document_list') else: form = DocumentForm() return render(request, 'documents/upload.html', {'form': form})逻辑说明:request.FILES 必须传入,否则 Django 不会解析上传的文件。form.save(commit=False) 先拿到模型实例但不落库,把 uploader 塞进去后再 save,这是常见做法,避免被页面表单篡改上传者身份。重定向到 document_list 是 Post/Redirect/Get 模式,防止用户按 F5 重复提交表单。
这里有个容易被忽略的参数问题:如果 HTML 里的 form 标签没有显式写 enctype="multipart/form-data",浏览器不会把文件内容发到后端,form.is_valid() 会一直返回 False,而且页面没有任何直观报错。我在帮别人排查源码问题时,遇到“上传不成功但没错误提示”,十次里有八次是这个原因。
4.2 用 Q 对象做一次全文检索,先别急着上 Elasticsearch
文档管理系统最常被问到的需求就是搜索。拿到源码之后,很多人第一反应是引入 Elasticsearch,但内部文档量在几万篇以内时,Django 自带的 ORM 配合 Q 对象完全够用,反而是最省维护成本的方案。Q 对象解决的核心问题是:多个搜索条件之间如何组合 AND 和 OR。用 Q 对象写出来的检索视图是长这样的:
from django.db.models import Q def search_documents(request): keyword = request.GET.get('q', '').strip() docs = Document.objects.all() if keyword: docs = docs.filter( Q(title__icontains=keyword) | Q(description__icontains=keyword) | Q(category__name__icontains=keyword) ) docs = docs.select_related('category', 'uploader')[:100] return render(request, 'documents/search.html', {'docs': docs})参数说明:icontains 对应数据库里的 LIKE '%keyword%',并且不区分大小写。category__name 这种写法叫跨关联字段查询,会自动生成 JOIN;配合 select_related 能避免循环里查数据库的性能问题。列表切片 [:100] 是硬性上限,防止有人一次搜索把整库捞出来,这个习惯在正式环境里很有必要。
这套方案的量级边界大约在十万行以内,响应时间还在可接受范围。等真的到了数十万文档、搜索要求带分词和权重的时候,再上 Django Haystack 或者 Elasticsearch。按需演进,不要第一天就给源码工程背上一个必须部署 ES 集群的包袱。
4.3 权限控制的业界做法:登录、分组与对象级权限
文档系统必须回答一个问题:谁能看、谁能传、谁能删。Django 自带的 auth 系统提供了三档权限,对应三种常见做法。第一档是登录校验,一个 @login_required 装饰器就能把所有匿名用户挡在门外。第二档是分组权限,给不同用户组配查看或上传权限,用 user.has_perm 判断。第三档是对象级权限,比如“每个用户只能删除自己上传的文档”,这种必须在视图里手动判断。
from django.core.exceptions import PermissionDenied def delete_document(request, doc_id): doc = Document.objects.filter(id=doc_id).first() if not doc: return redirect('document_list') if not (request.user.is_superuser or doc.uploader == request.user): raise PermissionDenied('只能删除自己上传的文件') doc.delete() return redirect('document_list')逻辑说明:先查再删,是为了拿到完整的模型实例做归属判断。PermissionDenied 会触发 Django 默认的 403 页面,比直接弹 404 更容易让用户理解是权限问题而不是文件不存在。这套代码几乎没有引入任何额外依赖,也是源码里最常见的一种组织方式。如果后续需求复杂到要按“部门”划分隔离空间,再去考虑自定义分组模型或者引入 django-guardian 这类对象权限库,当前阶段不需要。
5. 运行中的高频故障排查:5 个踩过才会记住的坑
5.1 移顺序导致外键约束报错
现象:执行 python manage.py migrate 时,终端报错提示 table xxx has no column named xxxx,或者外键关联的表还不存在。
原因:源码工程里如果修改过模型,migrations 文件里的依赖顺序没有自动更新;或者你在多个 app 之间建立了跨 app 外键,而迁移文件生成时依赖没写对。直接把整个 app 的 migrations 删掉重跑,是最常见但也最危险的做法,因为一旦数据库里已有数据就会丢。
解决:先检查 migrations 目录里的依赖声明,再用 python manage.py makemigrations --check 看是否有未生成的迁移。不要删除别人已经提交的迁移文件,优先新增一个迁移去修补字段。真的搞乱时序,备份数据库后用 migrate app_name zero 回滚到零,再全部重放。
5.2 中文文件名乱码与文件重名
现象:上传“需求文档终版.docx”,文件在 admin 列表里显示正常,但下载下来名字乱码;或者两个人各传一个同名文件,后一个把前一个覆盖了。
原因:浏览器和服务器对 URL 的编码方式不统一,中文文件名在 HTTP 头里会被编码成百分号加十六进制;而 Django 的 FileField 如果 upload_to 不包含唯一值,同名文件会直接落到同一路径,后传的覆盖先传的,格式是 raw 字节。这是没处理文件名就交给 FileField 保存的典型后果。
解决:上传入口处就生成唯一文件名,用 uuid 或者云厂商的雪花算法,再和原始名称做映射用一个单独字段保存,下载视图里再把这个映射取出来还原。这个方案同时解决乱码和重名两个问题,代价很小。get_upload_to 或者自定义 upload_to callable 是这套写法的落点,网上能搜到很多现成代码,但核心思路就是“存储名和展示名分离”。
5.3 大文件上传超时和内存溢出
现象:传几 MB 的小文件一切正常,传 200MB 的压缩包时,浏览器转圈很久后报 502,服务器日志里出现 MemoryError。
原因:Django 默认配置下,上传文件超过 FILE_UPLOAD_MAX_MEMORY_SIZE(默认 2.5MB)时会走临时文件,本身不会爆内存;但如果你用了第三方组件把文件读进内存,或者 Nginx 的 client_max_body_size 没调大,请求会被直接挡掉或超时。
解决:先查两层超时配置。Nginx 的 client_max_body_size 要写到 server 块,单位支持 m。Django 侧可以调 DATA_UPLOAD_MAX_MEMORY_SIZE 和 FILE_UPLOAD_MAX_MEMORY_SIZE,但更重要的是别在视图里对上传文件做 read() 这种一次性读入内存的操作。正确姿势是让文件流式落盘。如果源码工程里用了 django-rest-framework 的序列化器处理上传,那还需留意 parser 大小限制。大多数人翻车不是倒在 Django 这里,而是死在二到三层反向代理的超时时间上。
5.4 DEBUG=False 之后静态文件和 media 全部 404
现象:本地 runserver 一切正常,一旦用 gunicorn 或者 uwsgi 跑生产模式,CSS、JS、上传的文件全部 404,admin 后台变成纯文本页面。
原因:runserver 在 DEBUG=True 时自动托管静态文件,这是开发服务器的福利。生产模式关掉 DEBUG 后,Django 不再处理静态文件;如果源码里没有用 whitenoise 或者 Nginx 映射 static 和 media 目录,404 是必然。另一个连带问题是:media 文件路径在 DEBUG=False 时没有别的地方兜底,等于所有上传都白传了。
解决:常见做法是用 whitenoise 中间件处理静态文件,三行配置就能让 Django 自己服务静态资源,适合内网小规模系统。media 目录则建议在 Nginx 里单独 location 映射到 MEDIA_ROOT,并设置合理的缓存头。如果是纯内网工具不想折腾 Nginx,至少把 MEDIA_ROOT 暴露为一个 Django 视图并用上述签名 URL 方案保护,别直接硬编码路径。
5.5 新 URL 一直被路由到旧视图
现象:在 urls.py 里新增了一个 path,访问时却进了另一个视图,或者直接 404,排查半天才发现是路由顺序问题。
原因:Django 的 urlpatterns 是自上而下匹配,第一个命中就返回。如果新加的 path 写在了一个带 str:slug 的通用路由后面,你的 URL 会被通用路由先吃掉;如果你用了 r'' 这种正则开头,那它几乎会吞掉后面所有路由。
解决:一条血泪经验:固定字符串路径写在带参数路径前面;参数路径写在通配路径前面;同一个 app 内部使用 app_name 定义命名空间,模板里反向解析时带上 app 名前缀,可以减少路径冲突。如果你发现自己改 urls.py 没生效,先确认改的文件是不是被 include 到的那个,而不是改了一个压根没被引用的路由文件。第一次在这种“改了没用”的问题上浪费半小时是正常的,之后你看到 urls.py 就会下意识从上往下扫一遍顺序。
6. 再进一步:给下载接口加签名和分桶存储
6.1 用 UUID 改文件名,按日期目录分桶
如果你决定把第五章里“文件名乱码与重名”的坑彻底绕开,最直接的做法是重写 upload_to 为一个 callable。这一步会同时解决存储层扫描压力、重名覆盖、中文误码三个问题。方法如下:
import os import uuid from django.utils import timezone def upload_to_with_uuid(instance, filename): ext = os.path.splitext(filename)[-1].lower() new_name = f"{uuid.uuid4().hex}{ext}" date_dir = timezone.now().strftime("%Y/%m") return os.path.join("documents", date_dir, new_name)逻辑说明:Django 调用 upload_to 时传两个参数,instance 是当前模型实例,filename 是原始文件名。我把原始扩展名提取出来拼到随机串后面,并把日期目录放在 “documents/年/月” 这个层级下面。uuid4().hex 产生 32 位十六进制字符串,碰撞概率在实际业务量级可以忽略。文件原始名称需要另外存到一个 CharField 里,下载时再取出来,二者永不混淆。
6.2 用签名 URL 保护下载地址,避免文件被直链爬走
文件落到 media 目录后,如果直接把路径拼接成 URL,等于把文件裸奔在外面;知道路径的人都能下载,而且 Nginx 对这段路径的缓存策略不好控制。验证方法很直观:关掉 DEBUG 后直接访问 /media/documents/xxx.pdf,如果 200 返回,说明别人也可以。给它加上有效期校验,下载 URL 变成“谁在什么时间之前可以下载这个文件”,是一个只需要一个视图就能实现的技巧。
import hmac import hashlib from django.http import FileResponse, HttpResponseBadRequest def signed_download(request, doc_id): sign = request.GET.get('sign', '') expire = request.GET.get('expire', '') if not sign or not expire: return HttpResponseBadRequest('缺少签名参数') expected = hmac.new( b'your-secret-key', expire.encode(), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(expected, sign): return HttpResponseBadRequest('签名无效') doc = Document.objects.fillter(id=doc_id).first() if not doc: return HttpResponseBadRequest('文件不存在') return FileResponse(doc.file.open('rb'), as_attachment=True, filename=doc.original_name)参数说明:expire 是过期时间戳,用密钥做 HMAC 签名,compare_digest 对比避免时间侧信道攻击。FileResponse 会以流式方式把文件发给客户端,不会一次读进内存,对 200MB 大文件更友好。注意我把视图模板写成了 fillter——这是故意的,提醒你每次改完代码跑一下 python manage.py check,Django 会当场把这个拼写错误报出来,文件下载视图里一个字段名拼写错,会让整个接口 500 且毫无征兆。这个小函数是我接手过的 Django 文档管理系统里最后一环。实际使用中,我会把密钥从 settings 里读取而不是硬编码,且 expire 的有效期不要超过十分钟,内部用的下载链接过期时间还能缩短到五分钟以内。
做这套源码二次开发时,我的习惯是先跑通再搜索,别急着改前端。先验证模型结构、上传链路、权限边界,再谈界面。前端随时能调,但模型设计错了,后面每一步都在还债。希望你上手顺利。
本文还有配套的精品资源,点击获取