☰
Django开发实战:高校教师科研成果管理系统设计与部署指南
2026/10/28 11:55:49 网站建设 项目流程

简介:这套基于Python和Django框架的高校教师科研成果管理系统源代码,附带完整的数据库文件,属于个人毕业设计作品,在答辩评审中取得98分的高分,所有代码均经过调试测试,能够稳定运行。系统面向计算机、通信、人工智能、自动化等专业的学生、老师或从业者,适合作为期末课程设计、课程大作业或毕业设计的参考项目应用。压缩包内共有2000个文件,资源总大小为15.12MB,其中主要包含1636个Python源代码、151个HTML页面模板、86个JavaScript脚本以及CSS、XML、SQLite数据库等多种类型,文件结构清晰有序,便于对照学习。目前已有161人学习下载。项目覆盖科研成果管理中的常见业务模块,具有良好的扩展性,初学者可借助完整代码快速理解Django项目开发流程,有基础者也能在此基础上修改调整,实现差异化功能。

1. 高校教师科研成果管理系统:毕设选题的常见陷阱与真实价值

每年毕业季,总有一批计算机专业的学生被“管理系统”三个字劝退,觉得这题目又老又没技术含量。但做过的人都知道,教师科研成果管理系统是个被低估的选题:它业务边界清楚,数据模型天然适合关系型数据库,又有权限、统计、导入导出这些能在答辩时讲清楚的功能点。用 Python + Django 来做,恰好能把 Django 的 ORM、Admin 后台、Form 校验这些核心能力全部用上,一套代码下来,课程设计、毕业设计、甚至以后想包装成求职项目,都能直接复用。

这个系统解决的核心问题很具体:高校里教师科研成果分散在论文、专利、项目、著作几类表格里,科研秘书每年要手工催收、汇总、核对,错漏是常态。做成系统后,教师自己录入、秘书审核、领导只看统计结果,三类角色权限分离,数据流清晰。它适合那些想稳扎稳打做完整项目、又不愿意碰复杂的分布式或算法类题目的学生,也适合需要快速交付一套可用代码的在职学习者。但前提是——别拿到源码就跑,得先弄明白 Django 项目是怎么被拆开的。

我用这套技术栈还原过类似项目,下面按“框架怎么搭 → 数据模型怎么建 → 业务代码怎么写 → 部署怎么收尾”的顺序,把整个落地方案拆开讲,重点放在那些源码里看不见、但一跑就翻车的细节上。如果你手上已经有一份源码,这篇文章也能帮你快速定位项目结构和踩坑点。

2. Django 项目的骨架:先看懂源码目录,再谈二次开发

2.1 拆开 Django 项目:settings、urls、app 之间的协作关系

拿到一份“基于 Python+Django”的源码包,第一件事不是急着 python manage.py runserver,而是先把目录结构读一遍。Django 项目的标准布局是一个外层项目目录加若干个 app 目录,很多毕设源码会带一个manage.py在根目录,旁边是项目同名配置包,里面是settings.py、urls.py、wsgi.py,再往下才是具体的业务 app,比如teacher、achievement、user或者直接叫app的。

常见做法是:

cd project_root tree -L 2 -I __pycache__

这一步能看到全部顶层目录。我自己拿到陌生源码时,从不会先读代码,而是先看 settings.py 里装了什么:

  • INSTALLED_APPS里注册了哪些 app,这决定了哪些数据表存在;
  • DATABASES配置的是 SQLite 还是 MySQL,这决定了要不要装驱动;
  • AUTH_USER_MODEL是否自定义,这决定了登录逻辑能不能按普通 Django 默认走;
  • 静态文件和模板目录是怎么配的,这决定了页面能不能正常渲染出样式。

Django 的请求流转链路是:浏览器先经过 urls.py 的路由匹配,匹配到视图函数后,视图通过 ORM 操作数据库,再把数据交给模板渲染成 HTML。多读几遍这套链路,你会发现所谓“管理系统”其实就是一个表单的增删改查再加统计汇总,搞清楚路由和视图的对应关系,后面的改动基本就是复制粘贴微调。

参数说明:如果源码里DATABASES['default']['ENGINE']是django.db.backends.sqlite3,那数据库文件通常就在项目根目录下,名字一般是db.sqlite3。如果换成了 MySQL,需要自己在本地建库,且NAME、USER、PASSWORD都要改成自己的环境,这一项没对齐,后面跑起来全是connection refused。

2.2 让源码先跑起来:迁移、依赖与超级用户的顺序

毕设源码包最大的坑点之一,就是给的db.sqlite3可能带着别人电脑里的路径、测试数据甚至密码哈希。正规做法是先重建环境,再做数据迁移。我的习惯是这样:

python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txt python manage.py migrate python manage.py createsuperuser python manage.py runserver

逻辑说明:migrate的作用是把 models.py 里定义的模型类翻译成真实的数据库表。很多新手拿到源码后直接runserver,看到页面能出,但一登录就报no such table,原因就是没有先migrate。createsuperuser创建的是管理员账号,用来进入 Django Admin 后台,这个后台在毕设答辩时是加分项,用来直接看数据库内容非常直观。

依赖装完还有个常见问题——Django 版本不一致。比如源码是按 Django 4.2 写的,你本地装的是 Django 5.0,urlpatterns里的re_path、ORM 的查询 API 大面上兼容,但有些第三方包不兼容,一启动就报错。遇到这种情况,最快的方式是pip install django==4.2锁一个版本,不要追求最新,稳定跑通比什么都重要。

注意:如果源码包里的requirements.txt缺少pymysql或mysqlclient,而 settings.py 里却配了 MySQL,不用怀疑,这就是常见的“环境与配置脱节”。解决方法是把数据库引擎切回 SQLite,或者安装对应驱动并建好数据库,二选一,不要硬在源码作者的环境假设里挣扎。

3. 数据模型设计:老师、成果、审核三类表的关联与字段取舍

3.1 教师表与成果表:为什么用户表要单独建而不用 Django 默认的

高校教师科研成果管理系统的核心数据模型,绕不开三类表:教师用户表、科研成果表、审核记录表(或者是成果状态字段)。很多源码为了让登录简单,直接继承 Django 默认的User模型,再加一个Profile表存职称、所属院系等扩展信息。这种做法能跑,但每次取教师信息都要多查一次关联表,代码里到处是user.profile.title,写起来别扭。

我一般处理这类项目的常见做法是自定义一个Teacher模型,继承AbstractUser,把工号、姓名、职称、院系、研究方向这些字段直接放进用户表里。好处有两个:一是登录认证和业务数据在同一个模型里,写视图时不用做二次查询;二是权限判断可以直接用is_staff、is_superuser配合分组,教师和科研秘书天然区分开。

from django.contrib.auth.models import AbstractUser from django.db import models class Teacher(AbstractUser): teacher_id = models.CharField('工号', max_length=20, unique=True) title = models.CharField('职称', max_length=50, choices=[ ('lecturer', '讲师'), ('associate', '副教授'), ('professor', '教授') ], default='lecturer') department = models.CharField('院系', max_length=100) research_area = models.CharField('研究方向', max_length=200, blank=True) class Meta: db_table = 'teacher' verbose_name = '教师'

参数说明:teacher_id单独加唯一约束很有必要,因为系统通常用工号登录而不是用户名,如果只靠默认的username,重名和工号错乱很容易出现。choices字段能把职称限制在固定范围内,统计按职称分组时直接group_by就能出结果,不用再写一堆 if-else 映射。db_table显式指定表名,避免 Django 默认生成app_teacher这种带前缀的表名,改起来麻烦。

设置AUTH_USER_MODEL这一步别忘了:

# settings.py AUTH_USER_MODEL = 'teacher.Teacher'

这个配置必须在第一次migrate之前写好。如果项目已经跑过迁移,再改AUTH_USER_MODEL会非常痛苦,因为 Django 要求这个设置不能在创建任何迁移后修改,否则报AUTH_USER_MODEL must be unique一类错误。源码里如果是默认 User 模型,就别强行改成自定义的了,用 OneToOne 扩展表更省事。

3.2 成果表设计:论文、项目、专利拆分还是合并,直接决定后期统计难度

成果的类型一般有好几种:论文、科研项目、专利、著作、获奖。设计上的分歧点是:每种成果建一张表,还是放到一张表里用类型字段区分。毕设项目我推荐后者,用一张Achievement表加category字段区分。原因很实际:教师录入成果时,不同成果应该挂在同一棵“我的成果列表”树里展示,审核时也希望能在一张表里看全量待审记录,分开建表会让列表页和统计页的代码量翻倍。

class Achievement(models.Model): CATEGORY_CHOICES = [ ('paper', '论文'), ('project', '项目'), ('patent', '专利'), ('book', '著作'), ] STATUS_CHOICES = [ ('pending', '待审核'), ('approved', '已通过'), ('rejected', '已驳回'), ] teacher = models.ForeignKey(Teacher, on_delete=models.CASCADE, related_name='achievements') category = models.CharField('成果类型', max_length=20, choices=CATEGORY_CHOICES) title = models.CharField('成果名称', max_length=200) authors = models.CharField('作者', max_length=200) journal = models.CharField('发表期刊/会议', max_length=200, blank=True) publish_date = models.DateField('发表时间', null=True, blank=True) fund_source = models.CharField('项目来源', max_length=200, blank=True) patent_no = models.CharField('专利号', max_length=50, blank=True) status = models.CharField('审核状态', max_length=20, choices=STATUS_CHOICES, default='pending') reject_reason = models.TextField('驳回原因', blank=True) created_at = models.DateTimeField('创建时间', auto_now_add=True)

逻辑说明:不同类型成果的字段差异用“填空值”处理,比建多张表更实用。journal和patent_no互相冗余,但各自区分使用场景。统计“某教师近五年论文数量”时,一条 ORM 查询就能算出来,而不用跨表 join。再强调一次,on_delete=models.CASCADE是 Django 的必填参数,教师被删时其成果同步删除,避免脏数据。related_name='achievements'让反向查询可以用teacher.achievements.all(),这在个人成果列表页非常好用。

驳回原因字段很多人会省略,但实际业务流程里它几乎是刚需。没有驳回原因,教师看到状态变成“已驳回”只能一脸懵,再去找秘书问细节,系统就白做了。

4. 业务视图与表单:从 ModelForm 到列表查询的分页与筛选

4.1 用 Django 的 ModelForm 快速生成成果录入表单

Django 的ModelForm是毕设系统里效率最高的模块,没有之一。它基于模型类自动生成表单字段,省去手写 HTMLinput的体力活,而且自带校验逻辑,比如DateField会强制格式、CharField会有长度限制。成果录入页最常见的实现方式是这样:

from django.forms import ModelForm from .models import Achievement class AchievementForm(ModelForm): class Meta: model = Achievement fields = ['category', 'title', 'authors', 'journal', 'publish_date', 'fund_source', 'patent_no'] widgets = { 'publish_date': DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), }

在视图里处理 POST 请求的方式是:

def achievement_create(request): if request.method == 'POST': form = AchievementForm(request.POST) if form.is_valid(): achievement = form.save(commit=False) achievement.teacher = request.user # 当前登录教师自动绑定 achievement.save() return redirect('achievement_list') else: form = AchievementForm() return render(request, 'achievement/achievement_form.html', {'form': form})

逻辑说明:commit=False的意思是先把数据填进内存里的对象,但不写数据库,因为teacher字段不在表单里,需要从当前登录用户取出来再赋值。如果不加这行,直接form.save()会因为teacher为空而报完整性错误。这个“先取表单数据、再补上下文数据”的顺序,是 Django 开发里最容易出 bug 的地方,也是源码里最值得模仿的写法。

widgets里把publish_date指定为type='date',浏览器会自动弹出日期选择器,不用手动敲格式,这对普通用户很友好。如果源码里没有这个 widget,跑起来点击日期字段没反应,多半是浏览器把 input 当成了 text,不弹日历就是这个问题。

上传成果文件也是常见需求,但毕设系统里真正常用的不是文件上传,而是“把成果信息填对”。文件上传会引入静态文件服务、存储路径、文件大小限制一堆问题,如果题目没明确要求,建议别做,要做得话也放到后期功能增强时再加。

4.2 列表页组合拳:分页、筛选、搜索的常见结合方式

成果列表页是教师天天打开的地方,能不能快速找到一条记录直接决定系统的可用性。Django 的Paginator加上 GET 参数筛选是标准方案。这里的坑在于:翻页和筛选如果拼接不当,点击第 2 页后筛选条件就丢了。常见解法是保持 query string 不丢失:

from django.core.paginator import Paginator from django.db.models import Q def achievement_list(request): queryset = Achievement.objects.select_related('teacher').all() # 筛选条件:按类型、状态、关键字 category = request.GET.get('category', '') status = request.GET.get('status', '') keyword = request.GET.get('keyword', '') if category: queryset = queryset.filter(category=category) if status: queryset = queryset.filter(status=status) if keyword: queryset = queryset.filter( Q(title__icontains=keyword) | Q(authors__icontains=keyword) ) paginator = Paginator(queryset.order_by('-created_at'), 10) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) return render(request, 'achievement/achievement_list.html', { 'page_obj': page_obj, 'category': category, 'status': status, 'keyword': keyword, })

参数说明:icontains是大小写不敏感的模糊查询,适合标题和作者搜索。select_related('teacher')会在 SQL 层面用 JOIN 把教师信息一次性查出来,避免在模板里每次achievement.teacher.title都触发一次数据库查询,这在小项目里不算致命,但数据量稍涨就能感觉到页面的卡顿。模板里分页链接要带着筛选参数:

{% if page_obj.has_previous %} <a href="?page={{ page_obj.previous_page_number }}&category={{ category }}&status={{ status }}&keyword={{ keyword }}">上一页</a> {% endif %}

逻辑说明:这样翻页时筛选条件就能保持住。很多源码模板里只写了?page={{ page_obj.next_page_number }},翻到第二页筛选条件全丢,用户以为系统坏了,其实就是这一行没带参数。这个细节在答辩时被老师问到,能很大程度体现你是否真的理解“GET 状态保持”这个概念。

5. 权限控制与数据统计:教师看自己的,管理员看全量的

5.1 用装饰器区分教师和管理员的视图权限

一个管理系统如果所有人都能看全量成果数据,那还不如直接用 Excel。Django 的权限控制主要靠装饰器和 mixin,毕设级别的项目用装饰器就够了。我常用的方式是:

from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied def teacher_required(view_func): @login_required def _wrapped_view(request, *args, **kwargs): if request.user.is_staff: return view_func(request, *args, **kwargs) raise PermissionDenied return _wrapped_view

用法是:

@teacher_required def achievement_list(request): queryset = Achievement.objects.select_related('teacher').all() if not request.user.is_staff: queryset = queryset.filter(teacher=request.user) # 后续逻辑不变

逻辑说明:这样一套代码,教师登录后只能看到teacher=request.user的过滤结果,而管理员由于is_staff=True能看全部,最妙的是视图函数主体完全不用写两份。PermissionDenied异常会被 Django 自动捕获并返回 403 页面,不用自己在视图里做重定向。

这段代码的组合虽然简单,但能同时体现“登录保护”和“角色分支”两个知识点。如果源码里用的是@login_required加if not request.user.is_staff: return HttpResponseForbidden的写法,等价,但要注意HttpResponseForbidden不会异常给中间件处理,模板层拿不到状态码信息。

5.2 统计视图:按院系、成果类型、时间段聚合的写法

统计功能是答辩时的展示重点。Django ORM 里annotate配合Count是统计法宝。这里我把“各院系成果数量 + 各成果类型数量”做成一个接口为例:

from django.db.models import Count, Q from django.http import JsonResponse def statistics_data(request): # 按院系统计已通过成果数量 by_department = ( Achievement.objects.filter(status='approved') .values('teacher__department') .annotate(total=Count('id')) .order_by('-total') ) # 按类型统计 by_category = ( Achievement.objects.filter(status='approved') .values('category') .annotate(total=Count('id')) ) # 按近五年年份统计 from datetime import date current_year = date.today().year by_year = ( Achievement.objects.filter(status='approved', publish_date__year__gte=current_year - 4) .values('publish_date__year') .annotate(total=Count('id')) .order_by('publish_date__year') ) result = { 'by_department': list(by_department), 'by_category': list(by_category), 'by_year': list(by_year), } return JsonResponse(result)

逻辑说明:values('teacher__department')会按教师所属院系分组,annotate(total=Count('id'))给每组统计数量。跨表分组在 Django 里用双下划线穿透到关联表字段,这个语法必须熟练。publish_date__year__gte是年份范围的字段过滤,publish_date__year在values里作为分组键也合法,返回的 dict 键会是publish_date__year字符串,前端好直接用。

前端图表可以用 ECharts 或 Chart.js 接收这个 JSON 渲染柱状图和饼图。如果源码包自带的统计页是表格形式而非图表,也正常,但答辩时最好能补一个简单的柱状图,视觉冲击力远大于表格。

数据统计这块最大的环境坑是:publish_date字段为 NULL 的记录在按年份分组时会被忽略,导出统计结果前先把脏数据处理干净,否则“总量对不上”会让老师当场质疑数据的一致性。

6. 迁移导入模板和坑:SQLite 换 MySQL、导出 Excel、时区与静态文件问题

6.1 从 SQLite 换到 MySQL:连接配置与密码认证方式

毕设系统用 SQLite 开发很舒服,但答辩演示时总被人问“如果数据量大了怎么办”,这个问题的标准回应就是“可以切换到 MySQL”。切换时最容易遇到的问题是mysqlclient装不上和时区报错。常见配置如下:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'achievement_db', 'USER': 'root', 'PASSWORD': 'yourpassword', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", 'charset': 'utf8mb4', }, } }

参数说明:utf8mb4是必须的,否则论文标题里的特殊符号或中文在某些字符集下会报Incorrect string value,这个错误直接让插入语句崩溃。sql_mode设置STRICT_TRANS_TABLES是为了在字段超长时立刻报错,而不是静默截断数据,这在导入批次数据时特别能排查隐患。

注意:MySQL 8.0 默认的认证插件是caching_sha2_password,如果mysqlclient版本过旧,连接时会报Authentication plugin 'caching_sha2_password' cannot be loaded。解决办法是安装最新版mysqlclient,或在 MySQL 里把用户认证方式改回mysql_native_password,二选一,别在代码里折腾。

迁移数据的实操步骤是:

python manage.py dumpdata > data.json # 修改 settings.py 里的数据库配置 python manage.py migrate python manage.py loaddata data.json

逻辑说明:dumpdata导出的是 fixture 格式,包含模型数据和主键关系。跨库迁移如果顺序不对,先migrate建表再loaddata导数据就能避免“表不存在”的错误。不建议直接把 SQLite 文件拷给 MySQL 用,二进制格式不兼容。

6.2 导出 Excel 报表:用 pandas 还是 openpyxl 的一线取舍

科研管理系统几乎都要求“导出统计表”。实现时有两个常见流派:一是用pandas的to_excel,简单粗暴;二是用openpyxl手工建 Workbook,能完全控制格式。毕设项目我的推荐是openpyxl,因为pandas导出的表格式单一,且引入依赖较重。核心代码思路是:

from openpyxl import Workbook from django.http import HttpResponse def export_excel(request): wb = Workbook() ws = wb.active ws.title = '成果汇总' # 表头 headers = ['工号', '姓名', '院系', '成果类型', '成果名称', '发表时间', '审核状态'] for col, header in enumerate(headers, 1): ws.cell(row=1, column=col, value=header) queryset = Achievement.objects.select_related('teacher').all().order_by('teacher__department') for row, achievement in enumerate(queryset, 2): ws.cell(row=row, column=1, value=achievement.teacher.teacher_id) ws.cell(row=row, column=2, value=achievement.teacher.username) ws.cell(row=row, column=3, value=achievement.teacher.department) ws.cell(row=row, column=4, value=achievement.get_category_display()) ws.cell(row=row, column=5, value=achievement.title) ws.cell(row=row, column=6, value=achievement.publish_date.isoformat() if achievement.publish_date else '') ws.cell(row=row, column=7, value=achievement.get_status_display()) response = HttpResponse( content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ) response['Content-Disposition'] = 'attachment; filename=achievements.xlsx' wb.save(response) return response

参数说明:get_category_display()是 Django 对 choice 字段自动生成的方法,能把存储值paper转成人话“论文”,导出报表时必须用这个而不是原始值,否则老师看到英文选项会很困惑。Content-Disposition里的attachment告诉浏览器这是下载而不是直接在浏览器里打开。wb.save(response)直接把 Excel 内容写进 HTTP 响应对象,不用先存临时文件再打开,内存和磁盘都省了。

这条导出逻辑是“能抄作业”的典型,但要注意字段顺序和表头要在代码里写死,数据永远不会自动生成表头。

6.3 三个必踩的坑与对应排查记录

现象一:页面样式全丢,登录页裸奔白板。
原因:DEBUG = False时 Django 默认不处理模板里的静态文件路径,{% static 'css/bootstrap.css' %}找不到资源总线。
解决:开发环境把DEBUG = True;部署时用python manage.py collectstatic收集静态文件到指定目录,再让 Nginx 或后端路由去服务这个目录。

现象二:点击“提交成果”按钮后,页面提示 500 错误,后台日志显示IntegrityError: NOT NULL constraint failed: achievement.teacher_id。
原因:表单里没有 teacher 字段,form.save()时没有调用commit=False,teacher 外键没被赋值。
解决:回看第 4.1 节的写法,先commit=False再achievement.teacher = request.user。

现象三:导出中文文件名乱码或下载的文件为 0 字节。
原因:URL 对中文文件名不支持,且openpyxl保存到HttpResponse时,某些情况下响应对象未正确关闭。
解决:Content-Disposition里文件名用 ASCII 拼音或英文,比如achievements.xlsx而不是成果表.xlsx;保存后用response.close()显式关闭。

现象四:列表页过慢,每页加载 3 秒以上。
原因:模板里对每条成果调用了achievement.teacher.department之类的跨表属性,每行触发一次 SQL 查询,数据量一上去必然慢。
解决:在视图里加select_related('teacher')一次 JOIN,把关联对象提前取出来。这也是这段血泪经验里最推荐立刻修改的一行。

7. 让这个项目从“优”到“答辩有亮点”:给列表页加一个可视化的成果热度面板

源码跑通、功能完整,这只是底线。答辩时老师更感兴趣的是你有没有在“管理系统”之外做过主动思考。我比较推荐一个性价比极高的扩展——在首页加入一个教师成果趋势面板,用按年度和按类型的双维度图表呈现,前端用 ECharts。别小看这一个功能,它能同时展示 Django 的 ORM 聚合能力和前端数据可视化能力,申优、答辩、面试讲项目时都能直接当主线。

实现思路不复杂:视图里写一个 JSON 接口,返回最近五年每年各类型成果数量;前端页面加载后 fetch 这个接口,然后把数据塞给 ECharts 的堆叠柱状图。后端代码我在 5.2 节已经给了核心片段,这里补一个更贴近图表结构的聚合版本:

from django.db.models import Count from django.http import JsonResponse def chart_data(request): from datetime import date current_year = date.today().year years = [current_year - i for i in range(4, -1, -1)] rows = ( Achievement.objects.filter(status='approved', publish_date__year__gte=years[0]) .values('publish_date__year', 'category') .annotate(total=Count('id')) ) data = {year: {cat: 0 for cat in ['paper', 'project', 'patent', 'book']} for year in years} for row in rows: data[row['publish_date__year']][row['category']] = row['total'] return JsonResponse({'years': years, 'data': data})

逻辑说明:首先把年份列表固定为近五年,初始化一个嵌套字典把所有组合补零,然后遍历 ORM 聚合结果填充。这比把数据库结果直接丢给前端要稳,因为数据库里没有某年某类型的数据时就不会返回该行,前端拿到的 JSON 就会缺 key,图表画出来断层。

模板里嵌 ECharts 的最小形态:

<div id="chart" style="width:100%; height:400px;"></div> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script> fetch('/achievement/chart-data/') .then(response => response.json()) .then(data => { const chart = echarts.init(document.getElementById('chart')); const categories = ['论文', '项目', '专利', '著作']; const series = categories.map(catKey => ({ name: catKey, type: 'bar', stack: 'total', data: data.years.map(year => data.data[year][catKey]), })); chart.setOption({ tooltip: { trigger: 'axis' }, legend: { data: categories }, xAxis: { type: 'category', data: data.years }, yAxis: { type: 'value' }, series: series, }); }); </script>

这段代码有一层隐藏的细节:ECharts 的 CDN 引入了echarts.min.js,国内网络环境加载一般没问题,但答辩现场的演示机如果没外网,图表会白屏。我的习惯是把 echarts.min.js 下载下来放进static/js目录里,用{% static %}引入,避免现场翻车。你也可以在答辩前提前准备好离线包,这是很多人交过学费后才养成的习惯。

运行这段代码的正确步骤是先启动 Django 的 runserver,浏览器访问首页,打开开发者工具看 Network 面板里/achievement/chart-data/是否返回 200 和合法 JSON。如果返回 500,大概率是publish_date__year__gte里用了years[0]导致年份比较条件写反了,排查时把years[0]打印出来看下。如果接口返回了数据但图表是空白,检查data.data[year][catKey]的值是不是都是 0,这基本就是status过滤条件把数据全滤空了,查一下录入时成果状态是不是默认approved,或者把 filter 里的status='approved'去掉看效果。

做完这个可视化面板,再去调整列表页的 table 行高、按钮颜色这些细枝末节,浪费时间的性价比很低。我记得第一次做类似系统时,花了两天时间调 CSS 对齐,结果答辩老师根本没看表格,一眼就盯上了首页的统计图。之后我把时间全花在数据聚合和图表联动上,项目完成度和汇报效果都提升了很多。希望这个方向能帮你把毕业设计从“能做出来”推进到“有东西可以聊”。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询