简介一份基于 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.xPython 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_length50, uniqueTrue) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE) class Meta: verbose_name 分类 verbose_name_plural 分类 def __str__(self): return self.name class Document(models.Model): title models.CharField(max_length200, verbose_name标题) file models.FileField(upload_todocuments/%Y/%m/, max_length250) category models.ForeignKey(Category, nullTrue, blankTrue, on_deletemodels.SET_NULL) uploader models.ForeignKey(User, on_deletemodels.CASCADE) description models.TextField(blankTrue, verbose_name描述) uploaded_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-uploaded_at]逻辑说明Category 里那个 parent 外键指向自己是为了让分类可以无限层级嵌套比如“技术文档/后端/Django 部署”这样三级分类。you 要注意 on_deletemodels.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 决定列表页显示哪些列是把关联对象显示成字符串如果关联对象太多会出现每个单元格都查一次数据库的 N1 问题这时要在 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(commitFalse) 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(commitFalse) 先拿到模型实例但不落库把 uploader 塞进去后再 save这是常见做法避免被页面表单篡改上传者身份。重定向到 document_list 是 Post/Redirect/Get 模式防止用户按 F5 重复提交表单。这里有个容易被忽略的参数问题如果 HTML 里的 form 标签没有显式写 enctypemultipart/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__icontainskeyword) | Q(description__icontainskeyword) | Q(category__name__icontainskeyword) ) 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(iddoc_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 DEBUGFalse 之后静态文件和 media 全部 404现象本地 runserver 一切正常一旦用 gunicorn 或者 uwsgi 跑生产模式CSS、JS、上传的文件全部 404admin 后台变成纯文本页面。原因runserver 在 DEBUGTrue 时自动托管静态文件这是开发服务器的福利。生产模式关掉 DEBUG 后Django 不再处理静态文件如果源码里没有用 whitenoise 或者 Nginx 映射 static 和 media 目录404 是必然。另一个连带问题是media 文件路径在 DEBUGFalse 时没有别的地方兜底等于所有上传都白传了。解决常见做法是用 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( byour-secret-key, expire.encode(), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(expected, sign): return HttpResponseBadRequest(签名无效) doc Document.objects.fillter(iddoc_id).first() if not doc: return HttpResponseBadRequest(文件不存在) return FileResponse(doc.file.open(rb), as_attachmentTrue, filenamedoc.original_name)参数说明expire 是过期时间戳用密钥做 HMAC 签名compare_digest 对比避免时间侧信道攻击。FileResponse 会以流式方式把文件发给客户端不会一次读进内存对 200MB 大文件更友好。注意我把视图模板写成了 fillter——这是故意的提醒你每次改完代码跑一下 python manage.py checkDjango 会当场把这个拼写错误报出来文件下载视图里一个字段名拼写错会让整个接口 500 且毫无征兆。这个小函数是我接手过的 Django 文档管理系统里最后一环。实际使用中我会把密钥从 settings 里读取而不是硬编码且 expire 的有效期不要超过十分钟内部用的下载链接过期时间还能缩短到五分钟以内。做这套源码二次开发时我的习惯是先跑通再搜索别急着改前端。先验证模型结构、上传链路、权限边界再谈界面。前端随时能调但模型设计错了后面每一步都在还债。希望你上手顺利。本文还有配套的精品资源点击获取