多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Django视频点播网站实践:数据库设计、上传与播放鉴权指南

Django视频点播网站实践:数据库设计、上传与播放鉴权指南 简介一份基于 Python 与 Django 框架的视频点播网站系统完整项目代码与数据均已配置妥当下载后可直接运行面向毕业设计、课程实训和希望掌握 Web 全栈开发流程的学习者可快速获得一个包含前台展示与后台管理的真实站点。项目覆盖视频列表展示、播放详情、评论互动、个人中心以及后台视频、评论、用户、反馈管理模块视频列表采用模板动态渲染详情页集成播放器与评论区个人中心支持头像和浏览记录管理业务闭环完整。压缩包共 128 个文件包含 48 个 HTML 页面模板、40 个 Python 逻辑文件、20 个 JavaScript 交互脚本以及 CSS 样式、图标图片和说明文档等整体约 3.23MB目录结构清晰便于定位代码、替换素材和二次开发。通过 Django 内置 admin 与模板渲染机制可直观理解 MVC 架构、ORM 映射、表单验证和用户认证等核心知识点同时覆盖搜索、分页、文件上传等常见功能场景。目前已有 244 人学习使用对需要快速完成毕设或课设的同学而言是一份可直接落地、少走弯路的高质量参考实现。1. 看完这篇你也能交付一个能答辩的 Django 视频点播网站用 Python 和 Django 做视频点播网站是毕业设计里出镜率相当高的一类题目。但很多人第一次跑通别人的毕设代码后自己动手改需求时才发现视频播放不出来、上传大文件超时、后台管理乱成一团。这篇文章我就按自己做毕设和帮 A 同学、某导师改项目的经验把一个基于 Django 的视频点播网站拆成「数据库设计 → 文件上传 → 播放鉴权 → 避坑」四条线。如果你是新手顺着每章的代码块能跑通最小系统如果你已经写过 Django 项目重点看参数边界和踩坑记录能少走不少弯路。这个方向完全值得做技术栈常见、工作量好控制、答辩时功能点能讲清楚后续改造成短视频平台或课程网站也顺滑。2. 先立起数据地基用 Django Model 把视频、用户、播放记录一次画对2.1 避开一张表走天下的陷阱视频表、分类表、用户表怎么拆很多毕设一开始图简单把视频信息、上传用户、播放次数、分类名称全部塞进一张 Video 表里。短期看确实少写几个类可一旦加上「按分类筛选」「统计某个用户上传了多少」「记录每个用户的观看历史」SQL 就会越写越扭曲。常见的做法是拆成四张核心表Category分类、Video视频信息、UserDjango 自带、PlayRecord播放记录。分类单独拆出来是为了改分类名时不用批量更新视频表播放记录拆出来是为了后期做「继续观看」「播放次数统计」不留死角。我一般会用 Django 自带的 User 模型做用户体系而不是自己再建一张用户表。它自带密码哈希、登录态、权限分组省下的时间足够你把播放页做精致一点。视频表里除了标题、描述、封面、文件地址这些常规字段还建议加两个状态字段is_published 控制是否在列表页展示status 记录转码或审核状态。很多毕设翻车就翻在没考虑「视频传完了但不一定立刻能播」这个场景。# models.py 核心模型设计 from django.db import models from django.contrib.auth.models import User class Category(models.Model): name models.CharField(分类名, max_length50, uniqueTrue) sort models.IntegerField(排序权重, default0) class Meta: ordering [sort, id] def __str__(self): return self.name class Video(models.Model): STATUS_CHOICES ( (uploading, 上传中), (ready, 可播放), (failed, 转码失败), ) title models.CharField(标题, max_length200) desc models.TextField(描述, blankTrue) category models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name分类) uploader models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name上传者) video_file models.FileField(视频文件, upload_tovideos/%Y/%m/) cover models.ImageField(封面, upload_tocovers/%Y/%m/, blankTrue) duration models.IntegerField(时长(秒), default0) play_count models.IntegerField(播放次数, default0) is_published models.BooleanField(是否上架, defaultTrue) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultuploading) created_at models.DateTimeField(创建时间, auto_now_addTrue) def __str__(self): return self.title class PlayRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name用户) video models.ForeignKey(Video, on_deletemodels.CASCADE, verbose_name视频) watched_seconds models.IntegerField(已观看秒数, default0) watch_time models.DateTimeField(观看时间, auto_now_addTrue)代码逻辑说明Category 用 uniqueTrue 防止重复分类sort 字段控制列表页排序避免靠 id 顺序硬排。Video 表里 category 外键用 on_deletemodels.PROTECT而不是 CASCADE——这样分类下有视频时删分类会报错而不是静默连视频一起删掉这能避免「手滑删分类导致一堆视频消失」。uploader 用 CASCADE 是因为用户注销时连带清掉他的视频是合理行为。status 字段是给后续扩展转码流程留的位子即使现在不做异步转码也要先把状态机占住。这里有个参数要注意FileField 的 upload_to 用了%Y/%m日期目录目的不是好看而是避免单个目录下文件数量过多后文件系统变慢同时上传时间信息直接体现在路径里排查问题时一眼能看出哪个文件是哪天传的。如果论文里写「按日期分目录存储」这个设计可以直接写进系统设计章节。2.2 外键、索引和存储路径建表时的三个关键设计数据库设计里最容易忽略的是索引。视频列表页按分类筛选、按时间排序这两条查询路径必须有索引兜底否则数据量到几千条时页面明显变慢。我在帮某实验室做内部视频系统时就因为漏了分类外键的索引导致筛选接口响应时间从 80 毫秒涨到 1.6 秒。Django 默认会给外键建索引但组合索引需要手动声明。# Meta 中声明联合索引 class Video(models.Model): # 字段同上省略 class Meta: indexes [ models.Index(fields[category, is_published]), models.Index(fields[-created_at]), ]参数说明models.Index(fields[category, is_published])这个联合索引覆盖了「在某个分类下查已上架视频」的过滤条件[-created_at]支持列表页按发布时间倒序。注意 Django 里 Meta 内部类在 base 类中使用ordering [sort, id]这样的方式在继承或混入时会影响子类排序行为所以建议每张表都显式声明不要依赖默认。存储路径上还有一个坑用默认的 MEDIA_ROOT 存视频文件一旦项目部署到云服务器磁盘空间和备份策略都要重新考虑。我在模拟项目X里用的方案是 MEDIA_ROOT 放在项目根目录外例如/data/media/这样代码更新时不会误删视频文件。settings.py 里这样配置# settings.py 中媒体文件路径配置 import os BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media) # 开发环境下的静态媒体服务仅调试用 # 生产部署见文末的注意说明这段配置里 MEDIA_URL 是浏览器访问的 URL 前缀MEDIA_ROOT 是磁盘实际目录。两者名字接近但作用完全不同很多人调了半天 404 就是因为他们把 URL 写成了磁盘路径。开发阶段 DEBUGTrue 时Django 会自动用 django.views.static.serve 提供媒体文件服务部署上线后这个能力默认禁用必须由 Nginx 或对象存储接管这一点在答辩前必须确认否则演示现场视频全部打不开那就非常尴尬了。2.3 用 fixture 跑通第一波演示数据不写一行页面就能验证模型模型写完先别急着做页面我习惯用 Django fixture 灌一批假数据先确认 ORM 查询能跑通。这样能把「模型改错」和「页面写错」两类问题隔离开排查时不用猜。写 fixture 要注意 JSON 里外键用 id 引用Django 加载时会自动处理依赖关系。# 生成某分类的 fixture 数据 python manage.py dumpdata yourapp.Category --indent 2 category_fixture.json # 清空并加载 python manage.py flush python manage.py loaddata category_fixture.json说明dumpdata 上方命令中的yourapp需要替换成你的应用名。flush 会清空整库所以只建议在开发环境用。用 dumpdata 而不是手写 fixture 的好处是格式不会错手写 JSON 容易漏字段或写错日期格式。加载成功后可以进 Django shell 验证关联查询from yourapp.models import Video, Category # 找出第一个分类下的所有已上架视频按发布时间倒序 cat Category.objects.first() videos cat.video_set.filter(is_publishedTrue).order_by(-created_at) print(videos.query)cat.video_set是 Django 根据外键自动生成的逆向关联名filter 后的.query可以打印出真实执行的 SQL这是排查性能问题和理解 ORM 行为最直接的手段。比如你会发现 Django 默认用 select * 而不是只查需要的列这就是为什么大数据量下性能差的原因后期可考虑用.values()或only()。3. 视频文件怎么进来上传、校验与本地存储的完整闭环3.1 用 Form 和 FileField 处理视频上传最小可跑的视图代码视频上传是点播系统里最容易出错的环节。浏览器把文件 POST 到 DjangoDjango 把文件写进 MEDIA_ROOT然后数据库里存一条记录。我这里用一个继承自 Form 的验证类来控制字段而不是直接裸用 request.POST——Form 能把文件大小校验、类型校验、错误提示集中在一处。from django import forms from .models import Video, Category class VideoUploadForm(forms.Form): title forms.CharField(max_length200, label标题) category forms.ModelChoiceField(querysetCategory.objects.all(), label分类) video_file forms.FileField(label视频文件) cover forms.ImageField(requiredFalse, label封面) desc forms.CharField(widgetforms.Textarea, requiredFalse, label描述) def clean_video_file(self): video_file self.cleaned_data[video_file] if video_file.size 500 * 1024 * 1024: raise forms.ValidationError(文件大小不能超过 500MB) ext video_file.name.rsplit(., 1)[-1].lower() if ext not in [mp4, m4v, mov, webm]: raise forms.ValidationError(不支持的文件格式) return video_file这段代码里clean_video_file是 Django Form 的字段级钩子在字段通过基础验证后自动调用。我在某公司的内部 demo 项目里把上限设为 500MB这是本地磁盘和上传时长的折中。如果打算部署到云主机这个值建议降到 200MB并用分片上传来解决问题。接着是视图函数。常规做法是 POST 时先验证表单通过后从request.FILES取文件句柄再创建 Video 记录并保存。from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from .models import Video from .forms import VideoUploadForm login_required def upload_video(request): if request.method POST: form VideoUploadForm(request.POST, request.FILES) if form.is_valid(): vf request.FILES[video_file] video Video( titleform.cleaned_data[title], descform.cleaned_data.get(desc, ), categoryform.cleaned_data[category], uploaderrequest.user, video_filevf, statusready, ) video.save() return redirect(video_detail, pkvideo.pk) else: form VideoUploadForm() return render(request, upload.html, {form: form})逻辑说明form VideoUploadForm(request.POST, request.FILES)这句两个参数缺一不可request.FILES 是文件数据的来源漏掉它你会在表单验证时发现 video_file 永远为空。保存时video_filevf直接把 UploadedFile 对象传给 FileFieldDjango 会负责把文件从内存或临时目录搬到 MEDIA_ROOT并自动生成存储路径。整个过程中不要手动做open()写文件那是在和 Django 的存储机制抢活容易造成文件写了一半或权限混乱。3.2 视频格式、大小和重名三个不能让用户碰的参数视频上传时有三个参数必须服务端二次校验不能只靠前端 input 的 accept。第一是真实扩展名浏览器传给服务器的文件名后缀可以被伪造我用rsplit(., 1)取最后一段并转小写防止有人传movie.MP4后缀变成mp4识别失败第二是文件大小requests 里文件大小可以用file.size直接拿但要注意某些框架如早期 nginx 配置会在到达大小上限前直接返回 413Django 这边可能连表单都到不了所以要在部署层也做限制第三是重名问题两个用户同时上传demo.mp4后保存的文件会覆盖先保存的吗Django 默认行为是 FileField 检测到重名后自动在文件名前加随机字符串不会覆盖。但有个坑如果文件重名Django 自动改文件名后数据库里存的是新文件名而用户上传时看到的原始文件名已经丢失了。对策是把原始文件名单独存一个字段或者在上传后把原始名写进 Video 模型的一个 CharField。我在代码里加了original_name字段用于原始文件名存储这样后台能看到用户实际传的叫什么名字。# 保存前获取原始文件名 video.original_name vf.name # Django 生成的存储路径指向 video_file.name video.video_file vf参数说明vf.name是浏览器给定的文件名video_file.name是 Django 在存储时可能改写后的名字。很多人误以为这两处相等实际保存后会发现video_file.name前面多了时间戳或随机字符串。如果需要在前端展示原始文件名务必用original_name字段不要直接读video_file.name。3.3 上传进度与失败重试浏览器端常见做法毕设答辩时上传一个大视频的等待时间很考验耐心。浏览器原生 upload 没有进度事件只能用 XHR 监听progress。如果你用 Django 的普通 form 提交进度条是做不到的常见做法是把上传改成 axios/XHR 发送 FormData并在前端监听进度事件。这里我提供一套最简单的 XHR 上传逻辑const input document.getElementById(videoInput); const file input.files[0]; const formData new FormData(); formData.append(title, My Video); formData.append(category, 3); formData.append(video_file, file); const xhr new XMLHttpRequest(); xhr.open(POST, /upload/); xhr.setRequestHeader(X-CSRFToken, csrftoken); xhr.upload.onprogress function (e) { if (e.lengthComputable) { const percent Math.round((e.loaded / e.total) * 100); document.getElementById(progress).style.width percent %; } }; xhr.onload function () { if (xhr.status 302 || xhr.status 200) { // 跳转或提示成功 window.location.href /success/; } else { alert(上传失败: xhr.status); } }; xhr.send(formData);这段代码里csrftoken可以从 cookie 中读取Django 默认会设置csrftokencookie。XHR 和 fetch 都要显式带X-CSRFToken请求头否则 Django 会返回 403。xhr.upload对象和xhr本身是两回事progress 事件一定要挂在xhr.upload上挂在xhr上拿不到上传进度。失败重试在这里没有实现——重试前要核实一个关键信息文件是否已经被 Django 接收并写入了部分数据如果网络断在传输中途Django 那边可能已经创建了 Video 记录但文件不完整重试时就会出现重复记录。所以我的习惯是上传超时后引导用户刷新页面重新传而不是静默重试同时建议把 Video.status 先设为 uploading等文件完整写入后再置为 ready避免播放页展示一个损坏的视频。4. 播放器集成与防盗链把流媒体体验做到能答辩的程度4.1 选择 Video.js 还是原生 video播放页的真实取舍视频点播系统的核心页面是播放页。Django 的模板渲染很简单但播放器的选择直接影响你支持哪些格式、有没有进度记忆、能不能自定义样式。原生video标签足够播 mp4但缺少两样东西一是好看的控件皮肤二是对 HLSm3u8的支持。如果只需要放 mp4 文件原生标签加上少量 CSS 就够体积小、无依赖、答辩时不容易出幺蛾子如果计划支持 iOS Safari 播 m3u8原生 video 在 iOS 上面也认但 Android 老版本浏览器不认 m3u8这时候用 Video.js 加 HLS 插件更稳。我一般倾向于 Video.js。原因是它自带全屏、音量、倍速、字幕接口还支持播放进度事件配合后端记录观看进度比原生标签省事很多。引入方式用 CDN 就行不用打包构建。link hrefhttps://vjs.zencdn.net/8.10.0/video-js.css relstylesheet / script srchttps://vjs.zencdn.net/8.10.0/video.min.js/script video idmy-video classvideo-js vjs-default-skin controls preloadauto width720 height405 >import os from django.http import HttpResponse, HttpResponseNotFound from django.views.decorators.http import require_GET from django.conf import settings from .models import Video require_GET def video_stream(request, pk): video Video.objects.filter(pkpk, is_publishedTrue).first() if not video: return HttpResponseNotFound(视频不存在) file_path video.video_file.path # 真实磁盘路径 if not os.path.exists(file_path): return HttpResponseNotFound(文件缺失) file_size os.path.getsize(file_path) range_header request.META.get(HTTP_RANGE, ) start, end 0, file_size - 1 if range_header: # 形如 bytes0-499 或 bytes500- try: range_str range_header.strip().split()[1] start_s, end_s range_str.split(-) start int(start_s) if end_s: end int(end_s) except (ValueError, IndexError): start, end 0, file_size - 1 if start end or start file_size: return HttpResponse(status416) length end - start 1 with open(file_path, rb) as fp: fp.seek(start) content fp.read(length) response HttpResponse(content, content_typevideo/mp4, status206) response[Accept-Ranges] bytes response[Content-Range] fbytes {start}-{end}/{file_size} response[Content-Length] str(length) return response逻辑说明Django 的 FileField 有一个.path属性拿到的是文件在磁盘上的绝对路径。读取时用seek(start)跳到指定位置再读 length 字节。这里一次性把整个分片读进内存对大文件来说不够高效但毕设规模下可以接受如果视频文件超过 1GB应该改用 FileResponse 的 streaming 或 Nginx 的 X-Accel-Redirect 方案文末会提到。Content-Type要写对有些播放器根据 MIME 决定能否播放video/mp4是必须的。另一个细节是require_GET装饰器排除了 POST 请求避免有人用 PUT 往里传数据。这个视图不要缓存播放器在拖进度条时会频繁携带不同 Range 请求如果加了强缓存浏览器可能复用旧的 206 响应导致画面错乱。4.3 用户权限与播放鉴权没登录只能看前十分钟的实现思路毕业设计的逻辑里经常要求「未登录用户只能看前十分钟登录用户可以完整观看」。这类需求用上面的视频流视图很容易实现。判断点的思路是先根据登录态决定允许观看的字节区间。视频时长和文件大小不一定成正比简单做法是按文件字节比例算比如取前 5% 的字节精确做法是提前用 ffprobe 把时长算出来存在 Video.duration 字段然后按字节估算比例。if not request.user.is_authenticated: # 未登录用户只允许读取前 10 分钟对应的字节范围 # 假设总时长 duration 秒对应文件总大小 file_size # 单位字节/秒 bytes_per_second file_size / max(video.duration, 1) allowed_size int(bytes_per_second * 600) # 600秒 10分钟 end min(end, allowed_size - 1)这段逻辑要放在解析 Range 之后、读取文件之前。参数说明bytes_per_second是平均码率因为视频编码是变码率的实际播放 10 分钟消耗的字节数可能偏多或偏少但桌面浏览器对前后误差并不敏感而且播放器通常会在一个小范围内预取只要前十分钟的内容完整用户不会明显察觉。如果想让精确度高一些可以预先用 ffprobe 拿到每个关键帧的时间戳表但代价是存储 JSON 并频繁查询性价比不高。更稳妥的方案是给视频流接口加一个 cookie 鉴权未登录用户下载的响应里带Set-Cookie用户登录后 cookie 更新接口就能区分状态。这个方案能挡住绝大多数「复制链接发给别人」的场景。5. 避开毕设翻车的六个高频坑现象、原因、解决一条线5.1 静态视频文件 404但后台能看到路径存在现象用浏览器直接访问/media/videos/2024/xx.mp4返回 404video.delete()后台显示文件确实在磁盘上。 原因开发环境下 Django 自带的 static serve 只在 DEBUGTrue 时生效。很多人在 settings.py 里把 DEBUG 设成了 False 去模拟生产环境然后静态文件和媒体文件就全部挂掉。 解决确认 settings.py 里DEBUGTrue时访问正常然后部署时用 Nginx 或反代接管/media/。如果是写论文需要截图在本地可以临时加一条 re_path 指向django.views.static.serve但只用于开发不能上线。提示python manage.py runserver默认也会提供媒体服务但它不是生产工具并发稍高就报错不要把它当正式服务用。5.2 上传大视频到一半超时表单报错现象视频超过 200MB上传到一半浏览器转圈很久后显示连接被重置或 500。 原因开发服务器默认没有请求体大小限制但 Nginx 有client_max_body_size默认 1m。另外跑代码的机器内存不够Django 把整个上传文件读进内存小于FILE_UPLOAD_MAX_MEMORY_SIZE时走内存文件一大内存被打爆。 解决Nginx 加一行client_max_body_size 1g;或你期望的最大值。settings.py 里设置FILE_UPLOAD_MAX_MEMORY_SIZE 10 * 1024 * 1024超过 10MB 就写入临时文件避免内存溢出。Django 的FILE_UPLOAD_TEMP_DIR也可以指向空间足够的目录。5.3 中文文件名和字幕乱码现象上传课程第一章.mp4后播放器显示的文件名或字幕内容是乱码。 原因Django 对文件名持 UTF-8 但是 HTTP header 需要编码对于上传文件名里的中文字符某些浏览器不发百分号编码服务端存下来没问题但通过 Content-Disposition 下载时就会乱。 解决MediaFile 存储时自动改名为 ASCII 安全串原始名存original_name生成下载响应时要做quote(name)编码。字幕文件则要确保存成 UTF-8 格式Django 读取时也按 UTF-8 解码。5.4 视频文件是好的但网页播放黑屏现象浏览器打开页面能听到声音但没有画面或者直接转圈显示 loading。 原因编码格式问题。浏览器原生支持 H.264 编码的 mp4但不支持 VC-1 或某些 HEVCx265编码的 mp4另外部分视频是 10bit 色深浏览器硬解不了。 解决确认转码工具。常见方案是用 ffmpeg 转成 H.264 AAC 编码ffmpeg -i input.mp4 -c:v libx264 -profile:v main -crf 23 -c:a aac -b:a 128k output.mp4参数说明-profile:v main兼容性最好-crf 23是质量与体积的平衡点数值越小质量越好但文件越大-b:a 128k是音频码率。转码后覆盖原文件或者让 Video 记录指向新文件路径。这一点在答辩演示前必须验证否则现场播放蓝屏简直是把论文辛辛苦苦攒的信任清空。5.5 播放地址带登录校验复制给别人就打不开现象登录状态下能播放把当前页面 URL 发给同学对方打开却提示权限不足或 404。 原因视频流接口是登录态校验的对方 cookie 里没有登录信息。或者你自己过了半小时再打开token/cookie 过期页面也就黑了。 解决看需求。如果要求无登录预览就放一个无鉴权的临时签名 URL签名带过期时间如果是纯内部系统直接在播放页提示先登录。毕设里我一般保留登录校验方便在论文里写权限控制设计另外在前端判断 403 响应并跳转登录页。5.6 数据库里的路径与实际文件移动后不一致现象用 Django 后台改文件路径后播放时 404但数据库中 FileField 的值看起来是对的。 原因FileField 存的是相对路径跟 MEDIA_ROOT 拼起来才是真实路径。如果你把视频文件移到了另一个目录比如从本地挪到 OSSFileField 里存的值还是 old/path.mp4。 解决移动文件后统一在 Django 后台或脚本里调用video.video_file.name(new_name)并保存不要直接改数据库字段。另一个坑是 Linux 区分大小写media/Movie.mp4和media/movie.mp4是两个文件。做完文件移动操作后随手写一段校验脚本遍历所有 Video 记录检查.path对应文件是否存在能救回大量深夜排查时间。6. 最后的打磨播放热度统计、种子数据与演示环境的保命细节6.1 播放次数统计不要在视图中写死自增视频点播网站答辩时几乎必问「怎么统计播放量」。直接在播放页里面play_count F(play_count) 1是可以但要区分「播放请求」和「实际播了几秒」。建议在播放流视图里先记一条播放日志再用异步或定时任务更新统计。代码上可以写到 PlayRecord 表一个用户看完 10 秒算一次有效播放。6.2 快速造数据用 django-seed 生成假用户和播放记录不用手动去后台点几十次。常见做法是在management/commands目录下写一个seed_demo.py命令用循环创建用户、视频、播放记录代码简单但能让演示五分钟内从零变成满屏数据。6.3 答辩演示环境保命细节视频文件不要放太大演示时用短视频提前检查网络如果演示机不能联网Video.js 的 CDN 资源会挂——这种情况下把 Video.js 的 JS/CSS 下载下来放到 static 目录或者改用原生 video 标签否则演示现场全场黑屏连 UI 控件都不出来你就只能对着空页面讲不能交互的场景逻辑。我的习惯是装一次本地代码前先把浏览器缓存清一下、断网跑一遍流程确认没有外部依赖再连网跑正式演示。用 Django 做过完整视频点播系统之后你会发现真正的难点从来不是 Django 语法而是浏览器播放器、文件存储、权限控制、编码格式这几个系统交互的节点。每一步都踩过、验证过答辩时才能讲得有理有据。希望这篇笔记能帮你在拿到一个可运行的毕设项目时快速理清改造思路也帮你在自己动手写的时候少走几趟弯路。本文还有配套的精品资源点击获取
返回列表