每年到毕设季,总能看到一批同学被题目里的“技术栈”吓住。像“基于Java+SSM+Django好例文共享平台”这种题目,乍一看Java、Python各占一半,不少同学第一反应是“这俩怎么混在一起”。但说实话,这种题目反而是毕设里最好做、也最容易讲出亮点的一类。它本质就是一个内容管理平台,核心业务是“共享例文”:用户能看文章、搜文章、传文章、收藏评论,管理员能审核、分类、统计。真正要琢磨的不是框架本身,而是怎么把两套后端合理地拆开,让它们各干各的活,同时共享一套数据。
这篇文章就按我做过类似项目的经验,把这个题目从需求拆解、数据库设计、前后端分工,到常见坑位和答辩讲法,完整过一遍。想做毕设、课设,或者只是想快速搭一个“文章共享+后台管理”系统当练习的同学,可以照着这个思路落地。
1. 项目整体设计与思路拆解
1.1 例文共享平台的业务本质
先想清楚用户是谁、要解决什么问题。
例文共享平台通常面向学生、写作者和需要参考资料的人。普通人打开网站,能按分类浏览例文,能搜索关键词,能点进详情页看完整内容,看到好的可以收藏,有疑问可以评论,手头有优质素材也可以上传分享。管理员则通过后台维护分类、审核投稿、管理用户、查看统计数据。
所以这本质上就是一个“内容发布 + 前台展示 + 后台管理”的经典结构。别管标题里挂着多少关键词,落到功能上就是:用户端要好看好用,管理端要有效率,数据要能跑通。
1.2 双后端架构是亮点还是累赘
题目里同时出现SSM和Django,很多同学误以为要把两套框架写进同一个项目,一上来就卡住。实际做法不是“混编”,而是两套独立的后端应用,共同连同一个数据库。
我在实际开发中用的分工方式是:
- Django端负责用户前台:例文广场、分类浏览、关键词搜索、详情页、收藏、评论、个人中心。Django的模型和模板语法非常适合快速做数据展示,而且自带Admin能省不少事。
- SSM端负责管理后台:管理员登录、投稿审核、分类管理、用户禁用、数据统计。这块需要灵活的权限控制和大批量数据操作,SpringMVC+MyBatis写起来很顺手。
两个服务各跑各的端口(比如Django跑8000,SSM跑8080),数据统一落到同一套MySQL。这样做的好处是:功能边界清晰,代码不会失控;答辩时“双技术栈协同”本身就是个非常能讲的点;各自框架的强项都得到了发挥。
当然代价也有:开发时要维护两套工程、两套登录逻辑。这个我在第5章会讲怎么处理最省力。
1.3 SSM和Django的职责边界
再具体划分一下,避免写着写着串了:
| 模块 | 归属端 | 关键技术点 |
|---|---|---|
| 注册登录 | Django端 | 自定义User模型、密码哈希 |
| 例文浏览/搜索 | Django端 | ORM过滤、分页、全文检索 |
| 例文详情/收藏/评论 | Django端 | 一对多关联、AJAX局部刷新 |
| 投稿上传 | Django端 | 文件类型校验、富文本 |
| 分类管理 | SSM端 | MyBatis CRUD |
| 投稿审核 | SSM端 | 状态机切换、审核备注 |
| 用户管理 | SSM端 | 启用/禁用、角色分配 |
| 数据统计 | SSM端 | 分组聚合、图表JSON输出 |
有一个设计上的细节值得注意:审核过的例文在前台展示时,只从Django端查询;而管理员查看所有投稿时,从SSM端查询。两边的查询条件不完全一致,因此SQL的过滤逻辑要“各写各的”,不要试图共用。这也是为什么我用两套框架而不是只套一套的原因——各自查询视角不同,强行合一套反而别扭。
2. 核心功能模块与数据库设计
2.1 从功能清单反推表结构
我一般先列功能清单,再画E-R图,最后落表。这个项目核心表大概6到8张:
- 用户表(user):用户名、密码密文、昵称、角色(普通用户/管理员)、状态(正常/禁用)、注册时间
- 分类表(category):分类名称、父分类ID、排序号
- 例文表(essay):标题、摘要、正文内容、封面图、附件路径、所属分类、作者ID、审核状态、浏览量、收藏量、创建时间
- 评论表(comment):所属例文ID、评论人ID、内容、评论时间、回复父ID
- 收藏表(favorite):用户ID、例文ID、收藏时间(联合唯一约束)
- 审核记录表(audit_log):例文ID、操作人ID、原状态、新状态、备注、操作时间
- 公告表(notice):前台轮播公告内容
这套表结构基本能覆盖标题里所有关键词的落地:优秀例文就是essay表里的审核通过数据,例文分享就是投稿+审核流程,好文推荐就是首页的推荐列表和排行榜,作文辅导可以做成分类标签,素材共享则体现在附件下载上。
2.2 关键字段设计的几个考虑
审核状态字段是这类平台的重中之重。我推荐用tinyint存状态码,0代表待审核,1代表已发布,2代表已驳回。不要用字符串“pending/published/rejected”,数字不仅省空间,而且排序和筛选都方便。前端查询时永远只带“状态=1”的条件,管理员端才能看到全部。
多级分类也是一个容易忽略的点。例文可能按“小学作文 / 记叙文”这样的两级分类来组织,所以分类表里最好有parent_id字段,顶级的用0表示。写查询时用嵌套循环拼树形结构,或者直接用两次查询在内存里组树,对这个项目体量足够了。
浏览量和收藏量可以用冗余字段存计数,而不是实时count(*)。用户每次打开详情页就把essay表里的view_count加1,收藏页就同步更新favorite_count。这样统计页展示数据时不用做聚合查询,速度很快。量小,不存在并发写问题。
2.3 索引与唯一约束
为了检索和权限控制不会拖慢页面,要在这几个地方建索引:
ALTER TABLE essay ADD INDEX idx_category (category_id, status); ALTER TABLE essay ADD INDEX idx_create_time (create_time); ALTER TABLE favorite ADD UNIQUE KEY uk_user_essay (user_id, essay_id);idx_category这个联合索引能同时覆盖“某分类下已发布的内容”这类最高频查询。收藏表的联合唯一约束则保证同一个用户不能重复收藏同一篇例文,直接在数据库层挡住了脏数据。SQL层面把约束建好,业务代码里只需要做异常捕获和友好提示就行。
3. SSM管理端的核心实现
3.1 基于角色与状态的管理流程
管理后台核心就一个字“控”:控制谁能进后台,控制内容能不能上线。
管理员登录我用的是Spring Security的简单模式,登录成功后把角色信息写入Session,所有管理接口再做一层拦截器校验。这个小细节能避免后面的麻烦:如果你只检查“是否登录”不检查“是否管理员”,普通用户直接拼URL就能打开审核页面,答辩时问到验收安全问题就尴尬了。
审核流程我设计成三步走:
- 管理员看到待审核列表,点详情预览例文内容和附件
- 通过或驳回,驳回必须填写原因
- 状态更新并写入audit_log表,前台用户会在个人中心看到“审核不通过:缺少结尾段落”这类反馈
@Transactional public void audit(Integer essayId, Integer status, String remark) { Essay essay = essayMapper.selectById(essayId); AuditLog log = new AuditLog(); log.setEssayId(essayId); log.setOldStatus(essay.getStatus()); log.setNewStatus(status); log.setRemark(remark); auditLogMapper.insert(log); essayMapper.updateStatus(essayId, status); }这段代码加了@Transactional,确保审核记录和状态更新要么都成功要么都失败,不会出现状态改了但日志没写的情况。很多同学刚做毕设时会忽略这种一致性,但审核场景恰恰是最容易出双写问题的。
3.2 分类维护与动态SQL
分类管理的难点在二级分类。页面需要“拉着一级分类走”,下拉框选了父级后,子分类列表要通过AJAX动态加载。MyBatis里写一个按parent_id查询的Mapper即可,前端用jQuery的change事件触发请求。
后台列表页的筛选条件经常变化:按分类查、按状态查、按标题模糊查、按时间范围查。这种场景一定要用MyBatis的动态SQL,不然每种组合写一个Mapper方法,代码会膨胀到没法维护。
<select id="searchEssay" resultType="Essay"> SELECT * FROM essay <where> <if test="categoryId != null and categoryId != ''"> AND category_id = #{categoryId} </if> <if test="status != null"> AND status = #{status} </if> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR summary LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY create_time DESC </select><where>标签会自动去掉多余的AND,这是MyBatis里最容易写错也最容易调试的地方。建议初次尝试时,先在日志里打开SQL输出,用mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl查看拼接结果,确认没有语法问题再往下走。
3.3 统计报表的后端接口
数据统计页建议用折线图和柱状图展示:近七天的投稿量、各分类的例文数量、用户活跃度。后端只需要提供JSON接口,前端图表用ECharts或Chart.js拉数据渲染。
JSON接口写起来不复杂,但字段命名要统一。比如返回格式统一用{code: 0, msg: "success", data: [...]},这样前端渲染和错误处理都方便。我见过好几个项目因为后端一会儿返回data,一会儿返回result,前端到处判空,调试到崩溃。提前定好统一包装类,是管理端开发里最实用的习惯。
4. Django用户端的核心实现
4.1 例文广场与检索
Django端的主战场是前台展示。例文广场的首页按推荐位、最新发布、热门排行三个维度来组织,全部通过ORM查询完成:
def home(request): recommended = Essay.objects.filter(status=1, is_recommend=True)[:6] recent = Essay.objects.filter(status=1).order_by('-create_time')[:10] hot = Essay.objects.filter(status=1).order_by('-view_count')[:10] return render(request, 'index.html', { 'recommended': recommended, 'recent': recent, 'hot': hot, })这里有个细节要提醒:永远带上status=1这个条件。哪怕你数据库中绝大部分数据都是已审核,也应明确过滤,因为这关系到前后端的数据边界一致性。一旦漏掉,待审核的文章就会泄漏到前台,这属于演示时的大事故。
搜索功能的实现,按标题+摘要+正文三个字段做icontains模糊查询:
results = Essay.objects.filter( Q(title__icontains=keyword) | Q(summary__icontains=keyword) | Q(content__icontains=keyword), status=1 ).distinct()数据量在几万条以内,这个查询方式完全够用。如果以后数据量大了,再考虑MySQL全文索引或ES,但那是后话。毕设阶段把ORM的Q对象用熟,比引入一套检索中间件性价比高得多。
4.2 详情页、收藏与评论
详情页的压力主要在于“如何不整页刷新完成交互”。点赞、收藏、评论这类操作,我推荐用Django自带的视图返回JSON片段,再用原生AJAX或Fetch在页面上局部更新。
@require_POST def toggle_favorite(request, essay_id): user = request.user fav, created = Favorite.objects.get_or_create(user=user, essay_id=essay_id) if not created: fav.delete() count = Favorite.objects.filter(essay_id=essay_id).count() return JsonResponse({'status': 'ok', 'is_fav': created, 'count': count})用get_or_create一行就处理了“加收藏”和“取消收藏”的切换,返回的count直接刷新页面上的收藏数。这种写法既能展示你对ORM的熟练度,又避免了多余的手写SQL。
评论功能有两点值得注意:一是评论要支持按时间倒序并做分页,二是提交评论时不要直接拼HTML渲染,Django模板自动转义能帮我们挡掉大部分XSS风险。如果做的是前后端分离接口,也要记得在保存前做HTML标签过滤。
4.3 投稿上传与素材共享
投稿是“例文共享”的核心入口。用户在个人中心点击“发表例文”,需要填入标题、选择分类、上传封面、粘贴正文、可选传附件。
文件上传这一块,Django的FileField搭配MEDIA_ROOT配置很成熟。但有两个坑必须提前绕开:
- 文件类型白名单一定要在视图层校验。不能只靠前端限制扩展名,后端的校验才是真正的安全防线。
- 同名文件覆盖问题。给文件加时间戳重命名,避免两个用户上传的同名文件互相覆盖。
我建议重命名逻辑写成这样:
import os, time, random def handle_uploaded_file(file): ext = os.path.splitext(file.name)[1] filename = f"{int(time.time())}_{random.randint(1000,9999)}{ext}" return filename这些基本功在实际项目中看似琐碎,但恰恰是评审老师最爱追问的细节。问到“如何避免用户上传错误文件类型”时,你能立刻讲出后端白名单校验和重命名策略,这一问就稳了。
5. 常见问题与排查技巧实录
5.1 两套服务的登录状态互不相通
这个问题几乎每个做双后端项目的人都会遇到。用户在Django端登录了,跳转到SSM管理的8080端口仍然要求登录,反之亦然。原因很简单:两个Web应用使用的是各自独立的Session,互相看不到。
我的处理思路是先用职责分离降低复杂度:普通用户只走Django端,管理员只走SSM端,两边用户体系互不交织。管理员单独在SSM端注册和维护,普通用户不能直接注册出管理员身份。对于毕设而言这个方案最稳,代码简单,讲解也清晰。
如果确实想做到“一个账号两端通用”,更优雅的做法是引入JWT统一认证。用户登录后Django签发Token,SSM端在拦截器里校验Token的签名和有效期,再把用户信息解析出来。这个方案在答辩时是加分项,但实现量会大不少。我的建议是:时间宽裕就做统一认证,时间紧张就做职责分离,不要两个方案混用,否则调试会非常痛苦。
5.2 中文乱码
这个问题通常无症状地出现在两个地方:MySQL连接和JSON返回。
MySQL连接字符串一定要显式指定UTF-8:
jdbc:mysql://localhost:3306/essay_share?useUnicode=true&characterEncoding=utf8mb4在Django侧,数据库配置也要加上:
'OPTIONS': { 'charset': 'utf8mb4', }建库时直接用utf8mb4,比utf8更好,因为它能存下表情符号这类四字节字符。很多同学栽在“中文正常但表情发不了”的诡异问题上,根源就是库的字符集不是utf8mb4。
5.3 全文检索搜不到内容
icontains匹配通常是能用的,但有一种情况会突然“搜不到”:你输入的关键词带空格或特殊符号。比如用户搜“春节 作文”,中间带了个空格,整体作为一个词去匹配,自然什么都查不到。
解决方法是把keyword按空白拆开,每个词单独过滤再组合:
qs = Essay.objects.filter(status=1) for kw in keyword.split(): qs = qs.filter(title__icontains=kw)这种白跑分词虽然简单,但足以覆盖绝大多数搜索场景。把它写进代码后,体验会明显提升,搜“作文 坚持”这类短语也能出结果了。
5.4 部署后图片附件404
本地开发图片显示正常,换到服务器上就404,80%的情况是MEDIA_ROOT和MEDIA_URL配置不一致,或者是生产环境没接静态文件服务。
Django部署后DEBUG=False时不会自动服务媒体文件,需要在Nginx或同级网关里加一段映射:
location /media/ { alias /var/www/essay_share/media/; root /none; }SSM端的文件路径如果是绝对路径,比如D:/upload/essay/,换服务器就废了。正确做法是存相对路径upload/essay/xxx.docx,然后通过配置项拼绝对路径读取。数据库里存的永远是逻辑路径,物理路径只在配置里出现,这样项目才能轻松迁移。
5.5 上传大文件超时的经验
例文平台偶尔会遇到有人传很大的PDF或压缩包。Django默认文件大小限制没设,但请求体一大,Nginx层先会挡掉。我设的client_max_body_size 20m,Django端再用MAX_UPLOAD_SIZE做应用层校验,超过就直接返回提示。测试时我专门用了一个25MB的PDF验证过,前端给提示“文件过大,请压缩后上传”,用户不会卡在没有任何反馈的页面里。
6. 整体调试、演示录制与答辩经验
6.1 从零到一的调试顺序
双后端项目最忌讳从登录开始调试。我最常用的顺序是:
- 先建库建表,用SQL脚本整理好基础数据
- 调通SSM端的最简单列表接口,确认数据库连接没问题
- 调通Django端的一条查询,确认ORM能读到同一批数据
- 做管理端审核流程,改状态后去数据库里看,确认值变了
- 做用户端浏览流程,确认只显示已审核数据
- 最后再做登录、收藏、评论这些关联操作
这个顺序的好处是:每一步都验证了“数据从前端到后端到数据库”的通路,出问题时能最快缩小范围。如果先做登录,很可能绕了半天才发现是数据库连接串配错了,那才叫浪费时间。
6.2 演示数据准备的几个讲究
答辩演示效果好不好,很大程度上看预置数据是否专业。
我预置了10篇分类齐全的例文,覆盖记叙文、议论文、应用文、散文等不同分类,标题里带着用户常见的关键词,比如“我的家乡”“论坚持的力量”“一封道歉信”。用户测试账号有两类,一个是普通账号,能演示收藏、评论、投稿;一个是管理员账号,能演示审核、驳回、统计。评论区和收藏区都留了至少5条现成数据,这样演示时打开页面不是空荡荡的,观感差别非常大。
还有个小技巧:演示前把浏览器缩放到合适的比例,让首页的推荐位、分类导航和搜索框同时出现在一屏内。这个细节虽然跟技术无关,但能让评审老师三秒钟内理解“这个平台能干什么”。
6.3 答辩时怎么讲双后端技术亮点
答辩时不要按“登录注册、例文列表、后台管理”这种流水账讲,要把亮点串成故事。
我建议按三条主线讲:
- 协同架构:为什么用SSM+Django?因为用户端需要快速的模板渲染和ORM查询,管理端需要严格的权限控制和灵活的SQL组合,两个框架各取其长。数据共享通过统一数据库实现,接口间不直接调用,降低耦合。
- 安全设计:文件上传白名单校验、富文本XSS过滤、密码哈希存储、状态字段隔离前台展示和后台审核,提到任何一点都能展示你考虑问题比“做个网页”深了一层。
- 可维护性:审核日志表保证了操作可追溯,动态SQL应对多变筛选条件,统一JSON返回结构让前后端联调更顺畅。
有同学担心双技术栈会被追问“为什么不做成微服务”。我的应对很简单:项目体量用微服务是过度设计,我们采用分模块部署的双应用结构,既达到了技术栈覆盖的目的,又避免了分布式带来的额外复杂度。这个回答既诚实,又展示了你在架构取舍上的判断力。
最后再分享一个经验:这类内容共享平台题目,判断做得好不好,关键看“状态流是否闭环”。投稿是什么状态、审核通过后发生了什么、驳回后用户能否看到原因,这些细节点比页面做得多华丽更能体现真实开发水平。把数据流走通,把边界情况处理好,你的毕设一定能稳稳通过。