
在洪涝灾害风险预测平台的开发过程中平台搭载了大语言模型LLM智能问答与文献分析模块。为了配套实现文献自主学习、文献精度校验、文献汇总扫读与分类管理能力我为平台开发了一套完整的自定义文件管理中心。该模块支持用户自定义上传洪涝领域研究文献、数据集、报表图片同时提供文件预览、下载、收藏、删除、数量统计等全套功能。上传的PDF、Excel、图片等文件可被平台LLM调用解析实现文献内容精读、要点汇总、资料分类归档极大提升了洪涝灾害研究文献的收集效率与数据分析便捷度。本文将完整记录该文件管理模块的开发全过程重点解决模板继承图标兼容冲突、Django前后端联调、CSRF跨站防护、文件上传安全校验、用户文件权限隔离等核心问题梳理开发踩坑细节与优化方案为同类Web平台文件功能开发提供完整参考。免责说明本文为个人毕业设计技术复盘项目为仿真演示类科研平台所有数据均为模拟测试公开数据仅用于技术学习与学术交流不具备官方业务效力。完整开发实现逐步更新中.......一、需求与初始问题1. 支持多格式文件上传PDF文献、Excel数据集、图片、压缩包等洪涝研究常用文件2. 实现文件在线预览、下载、收藏、删除、时间展示、大小统计功能3. 严格用户权限隔离用户仅可查看、操作自己上传的文献与文件4. 文件数据可被平台LLM接口调用实现文献解析、汇总、精读分析5. 页面统一继承平台基础模板保留侧边栏、顶部导航、智能问答窗口保证全站UI一致性。问题项目初期独立文件管理页面 file-management.html 基于 Font Awesome 4.7 开发所有图标、UI样式渲染正常包含完整模拟数据与交互逻辑。但平台全站页面需统一继承公共基础模板 base.html该模板引入了 Font Awesome 6 模糊最新版CDN存在两个致命兼容问题1. FA6 废弃、重命名了大量 FA4 的经典图标类名原页面 fa fa-file-excel-o、fa fa-file-pdf-o 等类名完全失效2. 基础模板CDN版本模糊、加载不稳定图标资源请求失败页面所有图标统一变成空白小方框UI完全错乱。解决方案为了不改动全局模板、不影响全站其他页面样式采用模板隔离方案复制完整base.html结构新建专属模板base_fa4.html将其图标库替换为稳定可用的 Font Awesome 4.7 固定CDN。文件管理页面单独继承base_fa4.html既保留平台统一的导航栏、侧边栏、LLM问答窗口又完美兼容原有FA4图标样式彻底解决图标渲染失效问题。# models.py import os from django.db import models from django.contrib.auth.models import User class FileRecord(models.Model): # 关联上传用户实现权限隔离 user models.ForeignKey(User, on_deletemodels.CASCADE, related_namefiles, verbose_name上传用户) name models.CharField(max_length255, verbose_name文件名称) file_path models.FileField(upload_touploads/, verbose_name文件存储路径) file_type models.CharField(max_length20, blankTrue, verbose_name文件类型) file_size models.PositiveIntegerField(default0, verbose_name文件大小(字节)) collected models.BooleanField(defaultFalse, verbose_name是否收藏) upload_time models.DateTimeField(auto_now_addTrue, verbose_name上传时间) download_count models.PositiveIntegerField(default0, verbose_name下载次数) class Meta: verbose_name 用户文件记录 verbose_name_plural 用户文件记录 ordering [-upload_time] def save(self, *args, **kwargs): # 根据后缀自动识别文件类型适配文献、数据集、图片场景 ext os.path.splitext(self.name)[1].lower() type_map { .xlsx:excel, .xls:excel, .pdf:pdf, .csv:csv, .jpg:image,.jpeg:image,.png:image,.gif:image, .zip:zip,.rar:zip,.7z:zip } self.file_type type_map.get(ext, other) super().save(*args, **kwargs) def get_size_mb(self): # 字节转MB保留1位小数前端展示友好 if self.file_size 0: return 0.0 return round(self.file_size / 1024 / 1024, 1) def __str__(self): return self.name关键API视图view.py部分摒弃不安全的session取值方式、移除随意的csrf_exempt、增加用户权限校验、请求方法校验、异常捕获修复原始代码所有高危漏洞实现安全可用的文件CRUD接口。# views.py # 获取当前用户文件列表 login_required def get_files(request): # 安全写法直接使用request.user杜绝session越权风险 user request.user files FileRecord.objects.filter(useruser) data [] for f in files: data.append({ id: f.id, name: f.name, type: f.file_type, size: f.get_size_mb(), sizeUnit: MB, uploadTime: f.upload_time.strftime(%Y-%m-%d %H:%M:%S), collected: f.collected, downloadCount: f.download_count, previewUrl: f/api/files/{f.id}/preview/ if f.file_type in [image,pdf] else None }) return JsonResponse(data, safeFalse) # 文件上传接口安全修复版 login_required def upload_files(request): if request.method ! POST: return JsonResponse({msg: 请求方式错误}, status405) user request.user uploaded_files request.FILES.getlist(files) created [] # 文件名清洗防御路径遍历攻击 for file in uploaded_files: safe_filename os.path.basename(file.name) save_path fuploads/user_{user.id}/{safe_filename} path default_storage.save(save_path, ContentFile(file.read())) record FileRecord.objects.create( useruser, namesafe_filename, file_pathpath, file_sizefile.size ) created.append({ id: record.id, name: record.name, type: record.file_type, size: record.get_size_mb(), uploadTime: record.upload_time.strftime(%Y-%m-%d %H:%M:%S) }) return JsonResponse(created, safeFalse, status201) # 文件删除接口权限校验 login_required def delete_file(request, file_id): if request.method ! DELETE: return JsonResponse({msg: 请求方式错误}, status405) try: file_obj FileRecord.objects.get(idfile_id) # 横向越权防护仅文件所属用户可删除 if file_obj.user ! request.user: return JsonResponse({msg: 无权限操作}, status403) # 删除服务器文件数据库记录 if default_storage.exists(file_obj.file_path.name): default_storage.delete(file_obj.file_path.name) file_obj.delete() return JsonResponse({msg: 删除成功}) except FileRecord.DoesNotExist: return JsonResponse({msg: 文件不存在}, status404) # 文件下载接口 login_required def download_file(request, file_id): try: file_obj FileRecord.objects.get(idfile_id) if file_obj.user ! request.user: return JsonResponse({msg: 无权限操作}, status403) # 下载次数自增 file_obj.download_count 1 file_obj.save() response FileResponse(file_obj.file_path.open()) response[Content-Disposition] fattachment; filename{file_obj.name} return response except FileRecord.DoesNotExist: return JsonResponse({msg: 文件不存在}, status404) # 文件预览接口图片/PDF在线预览 login_required def preview_file(request, file_id): try: file_obj FileRecord.objects.get(idfile_id) if file_obj.user ! request.user: return JsonResponse({msg: 无权限操作}, status403) response FileResponse(file_obj.file_path.open()) # 在线预览配置 if file_obj.file_type pdf: response[Content-Type] application/pdf response[Content-Disposition] inline elif file_obj.file_type image: response[Content-Disposition] inline return response except FileRecord.DoesNotExist: return JsonResponse({msg: 文件不存在}, status404) # 文件收藏切换接口 login_required def toggle_collect(request, file_id): if request.method ! POST: return JsonResponse({msg: 请求方式错误}, status405) try: file_obj FileRecord.objects.get(idfile_id) if file_obj.user ! request.user: return JsonResponse({msg: 无权限操作}, status403) # 反转收藏状态 file_obj.collected not file_obj.collected file_obj.save() return JsonResponse({collected: file_obj.collected}) except FileRecord.DoesNotExist: return JsonResponse({msg: 文件不存在}, status404)URL路由# urls.py urlpatterns [ # 文件管理接口 path(api/files/, views.get_files), path(api/files/upload/, views.upload_files), path(api/files/int:file_id/, views.delete_file), path(api/files/int:file_id/download/, views.download_file), path(api/files/int:file_id/preview/, views.preview_file), path(api/files/int:file_id/toggle_collect/, views.toggle_collect), ]前端核心适配与CSRF跨站防护Django 对 POST、DELETE 等修改性请求强制开启CSRF防护前端所有文件操作接口必须携带CSRF Token否则会报403权限错误。封装通用获取Token方法适配所有请求。// 获取Cookie中的CSRF Token function getCsrfToken() { let cookieValue null; if (document.cookie document.cookie ! ) { const cookies document.cookie.split(;); for (let i 0; i cookies.length; i) { const cookie cookies[i].trim(); if (cookie.substring(0, 10) (csrftoken)) { cookieValue decodeURIComponent(cookie.substring(10)); break; } } } return cookieValue; } // 文件上传请求示例 async function uploadFiles() { const formData new FormData(); // 追加前端选择的文件 fileList.forEach(file { formData.append(files, file); }); const res await fetch(/api/files/upload/, { method: POST, headers: { X-CSRFToken: getCsrfToken() }, body: formData, credentials: same-origin }) const data await res.json(); // 上传成功后刷新文件列表 loadFileList(); } // 删除、收藏接口同理携带CSRF Token此处不再重复赘述1.模板继承图标空白原因全局模板 FA6 与页面 FA4.7 图标类名不兼容CDN 加载不稳定图标渲染失效。解决新建 FA4 专属基础模板局部隔离版本冲突保留页面布局的同时兼容旧图标。2.文件上传 403 CSRF 报错原因Django 对 POST 请求强制 CSRF 校验前端请求未携带令牌触发拦截。解决前端封装 Token 获取方法所有修改类请求统一携带 CSRF 请求头。3.用户文件越权访问漏洞原因接口仅根据文件 ID 查询数据未校验文件归属用户。解决所有文件操作接口增加当前用户归属校验实现用户数据隔离。4.特殊文件名存在路径遍历风险原因原生文件名含路径字符存在服务器路径穿透、文件泄露风险。解决使用 basename 清洗文件名统一安全存储防御路径遍历攻击。5.PDF、图片预览空白原因后端未配置预览响应头浏览器默认走下载模式。解决针对图片、PDF 单独配置资源类型和 inline 预览响应头支持浏览器在线渲染。