
简介本资源为基于Django框架与MySQL数据库的图片推荐系统毕业设计文档面向计算机科学专业的大学生、研究生及Web开发初学者与研究人员用于解决传统图片推荐管理模式中效率低、数据共享不便等问题可作为课程设计、毕业设计参考或Django Web开发学习的实战范例。压缩包内共1个docx文件约3.75MB内容完整呈现了系统的需求分析、总体设计、模块划分、数据库设计与测试维护等章节涵盖用户管理、图片信息管理与信息共享等核心功能并包含中英文摘要与研究背景。文档结构规范技术路线清晰涉及Python语言、Django框架、MySQL数据库及B/S架构等知识点还提及Hadoop、Scrapy等相关技术介绍。读者可借此了解从需求调研到系统实现与测试的完整开发流程掌握Web应用分层设计与数据库表结构设计思路并对照功能模块梳理自身的开发方案。目前已有52人学习浏览适合希望系统学习Django项目落地与撰写设计文档的读者参考借鉴。1. 一套图片推荐系统难的不是推荐算法而是数据链路很多人拿到「基于 Django 和 MySQL 的图片推荐系统」这类毕设题目第一反应是去找协同过滤代码觉得推荐算法才是核心。实际拆过几个类似项目后会发现真正拖垮进度的是另一条链路图片文件怎么存、元数据怎么落库、推荐结果怎么在 B/S 架构下稳定返回给浏览器。图片这类二进制资源一旦和业务表耦合Django 的 ORM 查询会迅速变慢MySQL 单表几万行就开始出现翻页卡顿。这套系统的定位很清晰管理员侧管理用户、地区、图片信息与新闻资讯用户侧浏览、检索、评论、收藏推荐模块基于图片标签和浏览行为生成候选列表。技术栈是 Python Django MySQLB/S 架构浏览器即客户端。适合正在做同类毕设、或者想用 Django 搭一个带多媒体资源管理的 Web 应用的人。下面按数据模型、ORM 查询、推荐候选生成、部署与排错逐层拆开代码都能直接跑。2. MySQL 表结构与 Django ORM 模型映射2.1 图片资源为什么必须拆成两张表要把图片管理做扎实第一件事是分清「文件本体」和「图片信息」。文件本体放在文件系统或对象存储数据库只存路径、宽高、格式、大小这些元数据。如果直接把图片二进制塞进 MySQL 的 BLOB 字段单张 2MB 的图一万张就是 20GB备份、迁移、主从同步全部受影响而且 Django 每次查询把整条记录读进内存内存直接被打爆。按原料里的 E/R 图图片信息实体包含标题、图片、尺寸、格式、版权来源、大小用户实体包含用户名、密码、姓名、性别、年龄、手机、头像、地区。这里我用两张表承载picture_meta存元数据picture_file存物理路径和哈希。拆开之后列表页查询只扫picture_meta需要原图时再按外键取路径I/O 明显更轻。字段类型说明索引idBIGINT主键自增PRIMARYtitleVARCHAR(200)图片标题idx_titlefile_pathVARCHAR(500)相对路径无file_hashCHAR(64)SHA-256去重uk_hashwidth / heightINT像素尺寸无file_sizeBIGINT字节数无formatVARCHAR(16)jpg/png/webp无copyright_sourceVARCHAR(200)版权来源无tagsVARCHAR(500)逗号分隔标签idx_tags(前缀)upload_user_idBIGINT外键上传者idx_useraddtimeTIMESTAMP默认 CURRENT_TIMESTAMP无file_hash上建唯一索引是为了去重同一张图重复上传时先算哈希查一次命中就直接复用已有记录不占额外磁盘。这在图片推荐场景里很关键因为爬虫或批量导入经常带来重复图。2.2 Django 模型定义与迁移把上面的表结构翻译成 Django 模型注意db_table显式指定表名避免 Django 默认的app_model命名跟 DBA 的规范打架。# models.py from django.db import models class PictureMeta(models.Model): title models.CharField(max_length200, db_indexTrue) file_path models.CharField(max_length500) file_hash models.CharField(max_length64, uniqueTrue) width models.IntegerField(default0) height models.IntegerField(default0) file_size models.BigIntegerField(default0) format models.CharField(max_length16, defaultjpg) copyright_source models.CharField(max_length200, blankTrue) tags models.CharField(max_length500, blankTrue) upload_user models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue) addtime models.DateTimeField(auto_now_addTrue) class Meta: db_table picture_meta indexes [models.Index(fields[tags])] def __str__(self): return self.titleon_deletemodels.SET_NULL是有意为之用户注销时图片不该跟着删保留内容资产只把上传者置空。auto_now_addTrue对应 MySQL 的CURRENT_TIMESTAMP默认值插入时自动填。迁移命令两条python manage.py makemigrations python manage.py migratemakemigrations生成迁移文件migrate才会真正在 MySQL 里执行 DDL。注意一个高频坑Django 4.2 以上遇到 MySQL 8.0.24 会报NotSupportedError: MySQL 8.4 or later is required这通常是数据库版本检测或驱动版本不匹配引起的升级mysqlclient到最新版一般能解决。2.3 标签检索的查询方案与性能取舍标签用逗号存在一个字段里好处是写入简单坏处是没法直接建普通索引做精确匹配。常见做法是加前缀索引配合LIKE但%关键词%这种写法会全表扫描。我一般会按下面这样分层处理# 推荐候选按标签命中数量排序 from django.db.models import Q, Count def recommend_by_tags(tag_list, limit20): query Q() for t in tag_list: query | Q(tags__containst) return (PictureMeta.objects.filter(query) .only(id, title, file_path, tags) .order_by(-addtime)[:limit])Q对象用|累加等价于 SQL 里的OR。only()限定只取四个字段避免把copyright_source这类长文本拉出来列表页响应能快不少。标签字段本身不适合频繁LIKE量大时应该拆出picture_tag关联表用多对多关系建联合索引。3. 图片上传、去重与 MySQL 写入的完整链路3.1 上传视图里先算哈希再落库上传是图片系统最容易出问题的地方大文件、并发重名、格式伪装。我的处理顺序是「校验格式 → 算哈希 → 查重 → 落盘 → 写库」任何一步失败都不留脏数据。# views.py import hashlib, os from django.conf import settings from django.http import JsonResponse from .models import PictureMeta ALLOWED {image/jpeg: jpg, image/png: png, image/webp: webp} def upload_picture(request): f request.FILES.get(file) if not f or f.content_type not in ALLOWED: return JsonResponse({code: 400, msg: 格式不支持}, status400) digest hashlib.sha256(f.read()).hexdigest() f.seek(0) # 读完指针回退否则保存的是空文件 exists PictureMeta.objects.filter(file_hashdigest).first() if exists: return JsonResponse({code: 200, msg: 已存在, id: exists.id}) ext ALLOWED[f.content_type] name f{digest[:16]}.{ext} save_path os.path.join(settings.MEDIA_ROOT, name) with open(save_path, wb) as dst: for chunk in f.chunks(): dst.write(chunk) meta PictureMeta.objects.create( titlerequest.POST.get(title, name), file_pathfmedia/{name}, file_hashdigest, file_sizef.size, formatext, upload_userrequest.user if request.user.is_authenticated else None, ) return JsonResponse({code: 200, id: meta.id})关键点有三个。f.read()之后必须f.seek(0)否则后续chunks()读到的是文件末尾落盘得到一个 0 字节文件这种 bug 排查起来很费时间。文件名用哈希前缀而不是原始文件名避免路径穿越和中文名在不同系统上的编码差异。查重放在落盘之前命中直接返回已有 id磁盘和数据库都不动。3.2 图片尺寸的获取与元数据补齐width和height不填的话前端布局会抖。用 Pillow 在落盘后补一次from PIL import Image def fill_dimensions(meta): with Image.open(meta.file_path) as im: meta.width, meta.height im.size meta.save(update_fields[width, height])update_fields只生成更新两列的 SQL不会触发全表字段回写这是 ORM 层最常见的性能优化点之一。注意 Pillow 在遇到损坏文件时会抛UnidentifiedImageError视图里要 try 住不能让整个请求 500。3.3 批量导入场景下的事务与批处理爬虫或后台批量导入几百张图时逐条create太慢。用transaction.atomic包住配合bulk_createfrom django.db import transaction transaction.atomic def batch_import(items): objs [PictureMeta(**it) for it in items] PictureMeta.objects.bulk_create(objs, batch_size500, ignore_conflictsTrue)batch_size500控制单条 INSERT 语句的行数太大容易撞max_allowed_packet。ignore_conflictsTrue依赖唯一索引重复哈希直接跳过。整个函数在原子事务里中途报错全部回滚。4. 推荐候选生成与 B/S 架构下的接口实现4.1 基于行为日志的简单推荐打分本科毕设级别不需要上深度学习用「标签权重 浏览热度 时间衰减」就能给出说得过去的结果。先记录行为再用 Python 聚合打分。行为权重数据来源浏览1view_log收藏3favorite 表评论4comment 表下载5download_logfrom datetime import timedelta from django.utils import timezone from django.db.models import Sum, F WEIGHT {view: 1, favorite: 3, comment: 4, download: 5} def score_pictures(user, limit20): since timezone.now() - timedelta(days30) rows (BehaviorLog.objects.filter(useruser, created__gtesince) .values(picture_id) .annotate(scoreSum(weight))) ranked sorted(rows, keylambda r: r[score], reverseTrue)[:limit] ids [r[picture_id] for r in ranked] return PictureMeta.objects.filter(id__inids).order_by(-addtime)values().annotate()走的是 GROUP BY把聚合下推到 MySQL 执行比在 Python 里循环快得多。created__gte做 30 天时间窗避免几个月前的一次收藏一直霸占推荐位。4.2 分页查询与 N1 问题规避推荐列表返回给前端时分页必须用游标式或limit offset。Django 的Paginator底层是LIMIT n OFFSET m页码越深越慢因为 MySQL 要扫描并丢弃前 m 行。数据量上万后改成基于 id 的游标def page_by_cursor(last_id, size20): qs PictureMeta.objects.filter(id__gtlast_id).order_by(id)[:size] items list(qs.values(id, title, file_path)) next_id items[-1][id] if items else None return {items: items, next: next_id}另外序列化图片信息时如果每条都要取上传者姓名会触发 N1。用select_related(upload_user)一次 JOIN 取回qs PictureMeta.objects.select_related(upload_user).filter(id__inids)select_related走 INNER JOIN适合外键一对一/多对一一对多用prefetch_related走两次查询在内存里拼装。用错会先炸内存再炸日志。4.3 Django 路由与接口组织B/S 架构下前端只需要拿到 JSON。用 Django 的路径配置把资源接口挂上去# urls.py from django.urls import path from . import views urlpatterns [ path(api/picture/upload/, views.upload_picture), path(api/picture/list/, views.picture_list), path(api/recommend/, views.recommend_view), ]picture_list里接page和size参数recommend_view里先取当前用户行为标签再调recommend_by_tags。整个链路是「浏览器 → Django 视图 → ORM → MySQL → JSON → 渲染」B/S 架构的好处是升级只改服务端客户端零维护。5. 部署后的排错与两个实用技巧5.1 图片 404 与 MySQL 连接耗尽的排查顺序上线后最常见的是图片 404。按顺序查settings.MEDIA_URL和MEDIA_ROOT是否配置、Nginx 有没有把/media/指到静态目录、DEBUGFalse时 Django 不会自动托管媒体文件必须交给 Nginx 或 CDN。这几个点里第二个最常被漏。数据库侧报Too many connections通常是视图里开了长事务没提交或者连接池没配。Django 每个请求默认复用连接但CONN_MAX_AGE设为 0 时每次请求都新建连接高并发下很快打满max_connections。常见做法是设CONN_MAX_AGE60并给 MySQL 的wait_timeout留出更大值。5.2 用 EXPLAIN 定位慢查询别靠猜。把 ORM 生成的 SQL 打印出来丢进 MySQL 客户端跑EXPLAINpython manage.py shell -c from django.db import connection from app.models import PictureMeta list(PictureMeta.objects.filter(tags__contains风景)[:10]) print(connection.queries[-1][sql]) 拿到 SQL 后执行EXPLAIN SELECT ...重点看type列。出现ALL说明全表扫描rows列的数字就是预计扫描行数。标签检索慢就加关联表、标题检索慢就确认idx_title是否被用到。如果key列是 NULL说明索引没生效大概率是字段类型不匹配比如字符串列拿整型去比。5.3 图片格式统一转换成 WebP推荐列表展示图统一转 WebP体积比 JPEG 小 25% 到 35%浏览器兼容性现在也没问题。批量转换不影响原图只生成缩略图from PIL import Image def to_webp(src, dst, quality82): with Image.open(src) as im: im.convert(RGB).save(dst, WEBP, qualityquality)quality82是我常用的平衡点再往上体积涨得比画质快。缩略图单独存一张表或一个目录列表接口返回缩略图路径详情页再取原图。这样既省带宽也避免列表页把大图全部加载进浏览器。列表接口里把缩略图路径和原图路径分开返回前端img标签用缩略图点开详情再换原图滚动列表的手感会明显不一样。本文还有配套的精品资源点击获取