django tip1 ### model.DateTimeField

本文介绍了Django模型中model.py文件的部分代码细节,探讨了auto_now_add字段的作用,即记录对象创建的时间,并指出当该字段设为True时,后台将默认隐藏auto_now字段,同时演示了如何使用defaulttimezone.now()来设置默认时间。
model.py片段

django tip1 ### model.DateTimeField

...

django tip1 ### model.DateTimeField

...

django tip1 ### model.DateTimeField   

后台显示

django tip1 ### model.DateTimeField

说明:

auto_now_add 创建时间。值为True时,后台默认不显示
auto_now 修改时间。 值为True时,后台默认不显示
显示默认值用default timezone.now()

使用本地时间

setting.py
django tip1 ### model.DateTimeField

时间测试
from django.test import TestCase

# Create your tests here.
from djprocms.wsgi import *

from django.utils import  timezone
print(timezone.now())

django tip1 ### model.DateTimeField

转载于:https://blog.51cto.com/13606158/2072131

后端显示; [02/Sep/2025 15:54:01] "POST /api/login/client/ HTTP/1.1" 200 176 [02/Sep/2025 15:54:02] "GET /api/client/projects/ HTTP/1.1" 200 615 [02/Sep/2025 15:54:04] "GET /api/customer/project/ HTTP/1.1" 200 613 瀹㈡埛瀹㈡埛A鐨勯」鐩按绋诲熀鍥犵紪杈戦」鐩殑鎶ュ憡锛?<QuerySet [<Report: 杞戒綋淇℃伅鎶ュ憡锛堟按绋诲熀鍥犵紪杈戦」鐩級 >, <Report: 鍝佺杞寲鍙婂啀鐢熻兘鍔涙祴璇曟姤鍛婏紙姘寸ɑ鍩哄洜缂栬緫椤圭洰锛?, <Report: 鎶楁€х瓫閫夊強闃虫€ч壌瀹氭姤鍛婏 紙姘寸ɑ鍩哄洜缂栬緫椤圭洰锛?, <Report: 浜や粯鎶ュ憡锛堟按绋诲熀鍥犵紪杈戦」鐩級>]> > 鎶ュ憡1鐨勬枃浠禪RL锛?http://192.168.1.17:8000/media/reports/2025/09/g10Ik6wo8mueb001a8dd4b4f8056baa6092fd565219c_UOsbHVB.pdf 鎶ュ憡5鐨勬枃浠禪RL锛?http://192.168.1.17:8000/media/reports/2025/09/QHBJk1NWfydeb001a8dd4b4f8056baa6092fd565219c_c0Pdh9v.pdf 鎶ュ憡10鐨勬枃浠禪RL锛?http://192.168.1.17:8000/media/reports/2025/09/PtlshNO88w8Fb001a8dd4b4f8056baa6092fd565219c_LErTtnq.pdf 鎶ュ憡39鐨勬枃浠禪RL锛?http://192.168.1.17:8000/media/reports/2025/09/eYMSdCZWsRI9b001a8dd4b4f8056baa6092fd565219c.pdf 杩斿洖缁欏鎴风殑鎶ュ憡鏁版嵁锛?[{'id': 1, 'project': 5, 'project_name': '姘寸ɑ鍩哄洜缂栬緫椤圭洰', 'report_type': 1, 'report_type_name': '杞戒綋淇℃伅鎶ュ憡', 'file_url': 'http://192.168.1.17:8000/media/reports/2025/09/g10Ik6wo8mueb001a8dd4b4f8056baa6092fd565219c_UOsbHVB.pdf', 'upload_time': '2025-09-02T06:38:34.624515Z', 'uploader': 1}, {'id': 5, 'project': 5, 'project_name': '姘寸ɑ鍩哄洜缂栬緫椤圭洰', 'report_type': 2, 'report_type_name': '鍝佺杞寲鍙婂啀鐢熻兘鍔涙祴璇曟姤鍛?, 'file_url': 'http://192.168.1.17:8000/media/reports/2025/09/QHBJk1NWfydeb001a8dd4b4f8056baa6092fd565219c_c0Pdh9v.pdf', 'upload_time': '2025-09-01T06:46:23.248237Z', 'uploader': 1}, {'id': 10, 'project': 5, 'project_name': '姘寸ɑ鍩哄洜缂栬緫椤圭洰', 'report_type': 3, 'report_type_name': '鎶楁€х瓫閫夊強闃虫€ч壌瀹氭姤鍛?, 'file_url': '前端:cust/report.wxml代码:<view class="container"> <!-- 项目信息头部 --> <view class="project-header" wx:if="{{selectedProject}}"> <view class="project-title">{{selectedProject.project_name}}</view> <view class="project-meta"> <text>{{selectedProject.client_name}}</text> <text class="status-tag">状态:{{selectedProject.statusText}}</text> </view> </view> <!-- 报告文件夹列表 --> <view class="folder-list" wx:if="{{selectedProject}}"> <!-- 使用block包裹循环以避免解析问题 --> <block wx:for="{{reportTypes}}" wx:key="type" wx:for-item="item"> <view class="folder-item"> <!-- 移除可能引起解析冲突的调试信息 --> <!-- 文件夹信息 --> <view class="folder-info"> <view class="folder-icon">📁</view> <view class="folder-details"> <view class="folder-name">{{item.name}}</view> <!-- 报告状态判断 --> <view class="report-status uploaded" wx:if="{{hasReported(Number(item.type))}}"> ✅ 已上传 </view> <view class="report-status unuploaded" wx:else> ⚠️ 未上传 </view> </view> </view> <!-- 下载按钮 --> <button class="action-btn view-btn" bindtap="handleReportView" data-reporttype="{{item.type}}" wx:if="{{hasReported(Number(item.type))}}" > 下载 </button> </view> </block> </view> <!-- 空状态提示 --> <view class="empty-tip" wx:if="{{!selectedProject}}"> 加载中...或暂无关联项目 </view> <!-- 客户端底部导航 --> <customer-tabbar id="tabbar"></customer-tabbar> </view> cust/report.js代码:Page({ data: { selectedProject: null, // 客户唯一关联项目 reportTypes: [ // 固定4类报告,与实验员端一致 { type: 1, name: "载体信息报告" }, { type: 2, name: "品种转化及再生能力测试报告" }, { type: 3, name: "抗性筛选及阳性鉴定报告" }, { type: 4, name: "交付报告" } ], projectReports: {}, // 项目-报告类型映射 reportsData: {} // 报告详情存储(key: 项目ID-报告类型) }, onPullDownRefresh() { // 仅当项目已加载时,刷新报告列表 if (this.data.selectedProject) { this.getProjectReports(() => { // 刷新完成后,停止下拉刷新动画 wx.stopPullDownRefresh(); wx.showToast({ title: "已同步最新报告", icon: "none" }); }); } else { // 项目未加载时,先刷新项目再刷新报告 this.getCustomerProject(() => { this.getProjectReports(() => { wx.stopPullDownRefresh(); wx.showToast({ title: "已同步最新项目和报告", icon: "none" }); }); }); } }, onLoad() { this.getCustomerProject(); // 加载客户关联的唯一项目 }, /** * 1. 获取客户关联的唯一项目(客户与项目一对一) */ getCustomerProject() { const token = wx.getStorageSync("token"); if (!token) { wx.showToast({ title: "请先登录", icon: "none" }); wx.navigateTo({ url: "/pages/login/login" }); return; } wx.request({ url: "http://192.168.1.17:8000/api/customer/project/", // 客户专属项目接口 method: "GET", header: { "Authorization": `Token ${token}`, "content-type": "application/json" }, success: (res) => { if (res.statusCode !== 200) { wx.showToast({ title: `获取项目失败:${res.statusCode}`, icon: "none" }); return; } // 客户仅关联1个项目,直接取返回对象(非数组) const project = res.data || null; if (!project || !project.project_id) { wx.showToast({ title: "暂无关联项目", icon: "none" }); return; } // 格式化项目状态(数字+文本) const formattedProject = { ...project, status: Number(project.status), statusText: this.getStatusText(Number(project.status)), reportedTypes: [] // 初始化已上传报告类型数组 }; this.setData({ selectedProject: formattedProject }, () => { this.getProjectReports(); // 项目加载后获取报告列表 }); }, fail: (err) => { console.error("客户项目接口请求失败:", err); wx.showToast({ title: "获取项目失败(网络/接口错误)", icon: "none" }); } }); }, /** * 2. 获取当前项目的所有报告 */ getProjectReports(callback) { const token = wx.getStorageSync("token"); const { selectedProject } = this.data; if (!selectedProject) { if (typeof callback === 'function') callback(); return; } wx.request({ url: "http://192.168.1.17:8000/api/customer/reports/", method: "GET", header: { Authorization: `Token ${token}`, "content-type": "application/json" }, success: (res) => { console.log("客户报告接口返回完整数据:", res); // 新增:打印完整响应 if (res.statusCode !== 200) { wx.showToast({ title: `获取报告失败:${res.statusCode}`, icon: "none" }); if (typeof callback === 'function') callback(); return; } const reportData = Array.isArray(res.data) ? res.data : []; console.log("客户项目ID(前端):", selectedProject.project_id); // 新增:打印前端项目ID console.log("后端返回的报告列表:", reportData); // 新增:打印后端返回的报告 const projectReports = {}; const reportsData = {}; const projectId = Number(selectedProject.project_id); // 强制转为数字(关键:避免类型不匹配) reportData.forEach(report => { console.log("单个报告数据:", report); // 新增:打印每个报告的详情 const reportType = Number(report.report_type); // 强制转为数字 if (!projectReports[projectId]) projectReports[projectId] = []; if (!projectReports[projectId].includes(reportType)) { projectReports[projectId].push(reportType); } reportsData[`${projectId}-${reportType}`] = report; }); console.log("最终关联的报告类型:", projectReports[projectId]); // 新增:打印最终关联的报告类型 this.setData({ projectReports, reportsData, "selectedProject.reportedTypes": projectReports[projectId] || [] }, () => { if (typeof callback === 'function') callback(); }); }, fail: (err) => { console.error("客户报告接口请求失败:", err); wx.showToast({ title: "获取报告列表失败", icon: "none" }); if (typeof callback === 'function') callback(); } }); }, /** * 3. 辅助:获取项目状态文本(与实验员端一致) */ getStatusText(status) { const statusMap = { 1: '载体构建阶段', 2: '品种转化及再生能力测试阶段', 3: '农杆苗侵染阶段', 4: '不定芽诱导阶段', 5: '抗性筛选及阳性鉴定阶段', 6: '温室培养阶段', 7: '交付' }; return statusMap[status] || '未知状态'; }, /** * 4. 辅助:判断报告是否已上传(用于渲染状态) */ hasReported(type) { const { selectedProject } = this.data; // 1. 基础校验:确保项目和报告类型数组存在 if (!selectedProject || !Array.isArray(selectedProject.reportedTypes)) { console.log("hasReported:项目或报告类型数组不存在", selectedProject); return false; } // 2. 强制统一类型:都转为数字(关键修复) const targetType = Number(type); // 传入的type可能是字符串(如从wxml中获取) const reportedTypes = selectedProject.reportedTypes.map(item => Number(item)); // 已上传的类型转为数字 // 3. 打印日志:确认类型和判断结果 console.log(`hasReported:目标类型${targetType},已上传类型${reportedTypes},结果${reportedTypes.includes(targetType)}`); // 4. 判断是否包含 return reportedTypes.includes(targetType); }, /** * 5. 下载/查看报告(核心功能) */ handleReportView(e) { const { reporttype } = e.currentTarget.dataset; const selectedProject = this.data.selectedProject; const token = wx.getStorageSync("token"); // 补充Token有效性检查 if (!token) { wx.showToast({ title: "登录已过期,请重新登录", icon: "none" }); wx.redirectTo({ url: "/pages/login/login" }); return; } if (!selectedProject) { wx.showToast({ title: "请先选择项目", icon: "none" }); return; } // 生成报告唯一标识(项目ID+报告类型) const key = `${selectedProject.project_id}-${reporttype}`; const report = this.data.reportsData[key]; if (report && report.file_url) { wx.showLoading({ title: "加载中..." }); wx.downloadFile({ url: report.file_url, header: { Authorization: `Token ${token}`, // 确保Token正确传递 'content-type': 'application/pdf' // 补充PDF文件类型标识 }, success: (downloadRes) => { wx.hideLoading(); if (downloadRes.statusCode === 200) { // 打开PDF文件(指定文件类型为pdf) wx.openDocument({ fileType: 'pdf', filePath: downloadRes.tempFilePath, success: () => console.log('文件打开成功:', downloadRes.tempFilePath), fail: (err) => { console.error('打开PDF失败:', err); wx.showToast({ title: '请安装PDF阅读器后重试', icon: 'none' }); } }); } else { console.error('文件下载失败,状态码:', downloadRes.statusCode); wx.showToast({ title: `下载失败(状态码:${downloadRes.statusCode})`, icon: 'none' }); } }, fail: (err) => { wx.hideLoading(); console.error('下载请求失败(网络/URL错误):', err); wx.showToast({ title: '网络错误,请检查URL或Token', icon: 'none' }); } }); } else { wx.showToast({ title: '报告文件不存在或未上传', icon: 'none' }); } } });后端代码:views.py:from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from rest_framework import status,generics from django.contrib.auth.hashers import check_password from wxtest.models import Experimenter, Client,ProjectContract,Report # 同应用内的模型 from .serializers import ExperimenterLoginSerializer, ClientLoginSerializer,ProjectContractSerializer,ReportSerializer # 同应用内的序列化器 from rest_framework.permissions import AllowAny # 导入允许匿名访问的权限类 from rest_framework.authtoken.models import Token # 导入Token模型 from django.contrib.auth import authenticate from rest_framework.exceptions import PermissionDenied from django.contrib.auth.models import User from django.db import IntegrityError import os from django.utils import timezone class ExperimenterLoginView(APIView): permission_classes = [AllowAny] """实验员登录视图""" def post(self, request): serializer = ExperimenterLoginSerializer(data=request.data) if serializer.is_valid(): password = serializer.validated_data['password'] username = serializer.validated_data['username'] try: experimenter = Experimenter.objects.get(username=username) if password == experimenter.password: # 检查是否有对应的User记录,如果没有则创建 if not experimenter.user: user = User.objects.create_user( username=username, password=password ) experimenter.user = user experimenter.save() else: user = experimenter.user # 生成或获取Token token, created = Token.objects.get_or_create(user=user) return Response({ 'status': 'success', 'user_type': 'experimenter', 'user_id': experimenter.experimenter_id, 'username': experimenter.username, 'token': token.key, # 返回Token 'message': '登录成功' }) return Response({'status': 'error', 'message': '密码错误'}, status=status.HTTP_400_BAD_REQUEST) except Experimenter.DoesNotExist: return Response({'status': 'error', 'message': '实验员不存在'}, status=status.HTTP_404_NOT_FOUND) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class ClientLoginView(APIView): permission_classes = [AllowAny] # 客户登录接口也需要添加 """客户登录视图(合同编号+客户名称验证)""" def post(self, request): serializer = ClientLoginSerializer(data=request.data) if serializer.is_valid(): contract_number = serializer.validated_data['contract_number'] name = serializer.validated_data['name'] try: client = Client.objects.get(contract_number=contract_number) if client.name == name: # 验证客户名称 # 为客户创建或获取对应的User记录以生成token if not hasattr(client, 'user') or not client.user: # 创建一个与客户关联的User user = User.objects.create_user( username=f"client_{contract_number}", password=contract_number # 使用合同编号作为初始密码 ) # 在Client模型中添加user字段后才能使用下面这行 client.user = user client.save() else: user = client.user # 生成或获取Token token, created = Token.objects.get_or_create(user=user) return Response({ 'status': 'success', 'user_type': 'client', 'user_id': client.client_id, 'name': client.name, 'contract_number': client.contract_number, 'token': token.key, # 添加token返回 'message': '登录成功' }) return Response({'status': 'error', 'message': '客户名称错误'}, status=status.HTTP_400_BAD_REQUEST) except Client.DoesNotExist: return Response({'status': 'error', 'message': '客户不存在'}, status=status.HTTP_404_NOT_FOUND) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class ClientProjectListView(generics.ListAPIView): """客户项目列表接口 - 返回当前登录客户的所有项目""" serializer_class = ProjectContractSerializer permission_classes = [IsAuthenticated] # 需要登录验证 def get_queryset(self): # 获取当前登录用户关联的客户 user = self.request.user try: client = Client.objects.get(user=user) # 通过user关联找到客户 # 返回该客户的所有项目(一对一关联) return ProjectContract.objects.filter(client=client) except Client.DoesNotExist: raise PermissionDenied("没有找到该客户的信息") class ProjectContractListView(generics.ListAPIView): """获取当前实验员的所有项目""" serializer_class = ProjectContractSerializer permission_classes = [IsAuthenticated] def get_queryset(self): # 只返回当前实验员负责的项目 return ProjectContract.objects.filter( experimenter=self.request.user.experimenter ).select_related('client') class ProjectContractCreateView(generics.CreateAPIView): """创建项目合同""" queryset = ProjectContract.objects.all() serializer_class = ProjectContractSerializer permission_classes = [IsAuthenticated] class ProjectContractUpdateView(generics.UpdateAPIView): """修改项目合同信息""" queryset = ProjectContract.objects.all() serializer_class = ProjectContractSerializer permission_classes = [IsAuthenticated] lookup_field = 'project_id' def get_object(self): obj = super().get_object() # 检查当前用户是否有权修改此项目 if obj.experimenter != self.request.user.experimenter: raise PermissionDenied("您无权修改此项目") return obj class ProjectContractDeleteView(generics.DestroyAPIView): """删除项目合同""" queryset = ProjectContract.objects.all() permission_classes = [IsAuthenticated] lookup_field = 'project_id' def get_object(self): obj = super().get_object() # 检查当前用户是否有权删除此项目 if obj.experimenter != self.request.user.experimenter: raise PermissionDenied("您无权删除此项目") return obj def destroy(self, request, *args, **kwargs): instance = self.get_object() self.perform_destroy(instance) return Response( {"status": "success", "message": "项目删除成功"}, status=status.HTTP_200_OK ) class ExperimenterProjectReportListView(generics.ListAPIView): serializer_class = ReportSerializer permission_classes = [IsAuthenticated] def get_queryset(self): # 仅返回当前实验员负责的项目的所有报告 experimenter = self.request.user.experimenter return Report.objects.filter( project__experimenter=experimenter ).select_related("project") # 关联查询优化 # 2. 上传报告(支持新增/覆盖:同一项目同一类型自动覆盖) class ReportUploadView(generics.CreateAPIView): serializer_class = ReportSerializer permission_classes = [IsAuthenticated] def create(self, request, *args, **kwargs): # 1. 获取前端传的核心参数:项目ID、报告类型、PDF文件 try: # 关键:将project_id和report_type转为整数,避免类型错误 project_id = int(request.data.get("project")) report_type = int(request.data.get("report_type")) except (ValueError, TypeError): return Response( {"error": "项目ID和报告类型必须是数字"}, status=status.HTTP_400_BAD_REQUEST ) file = request.FILES.get("file") # 注意:文件通过FILES传递 # 2. 基础验证 if not (project_id and report_type and file): return Response( {"error": "项目ID、报告类型、PDF文件均为必填"}, status=status.HTTP_400_BAD_REQUEST ) # 验证文件类型(仅PDF) if not file.name.endswith(".pdf"): return Response( {"error": "仅支持PDF格式文件"}, status=status.HTTP_400_BAD_REQUEST ) # 3. 查询项目实例(移除权限验证,只检查项目是否存在) try: # 仅验证项目是否存在,不限制所属实验员 project = ProjectContract.objects.get(project_id=project_id) except ProjectContract.DoesNotExist: return Response( {"error": "项目不存在"}, # 提示信息也相应修改 status=status.HTTP_404_NOT_FOUND ) # 4. 处理“新增/覆盖”逻辑:同一项目同一类型仅保留最新文件 report_data = { "project": project, "report_type": report_type, "file": file, # 确保file是request.FILES获取的正确文件对象 "uploader": request.user.experimenter } try: # 尝试创建新报告 report = Report.objects.create(**report_data) print(f"新报告创建成功,文件路径:{report.file.url}") # 调试日志 return Response( {"status": "success", "message": "报告上传成功", "file_url": report.file.url}, # 返回URL给前端 status=status.HTTP_201_CREATED ) except IntegrityError: # 覆盖已有报告的文件 report = Report.objects.get(project=project, report_type=report_type) old_file_path = report.file.path # 可选:删除旧文件,避免占用空间 if os.path.exists(old_file_path): os.remove(old_file_path) report.file = file # 更新文件 report.upload_time = timezone.now() # 手动更新上传时间(之前代码此处有误,需修复!) report.save() print(f"报告文件更新成功,新文件路径:{report.file.url}") # 调试日志 return Response( {"status": "success", "message": "报告已更新", "file_url": report.file.url}, # 返回最新URL status=status.HTTP_200_OK ) # 3. 可选:删除报告(用于错误报告的清理) class ReportDeleteView(generics.DestroyAPIView): queryset = Report.objects.all() permission_classes = [IsAuthenticated] lookup_field = "id" # 按报告ID删除 def destroy(self, request, *args, **kwargs): self.perform_destroy(self.get_object()) return Response( {"status": "success", "message": "报告已删除"}, status=status.HTTP_200_OK ) # 客户专属接口:获取当前登录客户关联的唯一项目 class CustomerProjectView(APIView): """ 客户专属接口:获取当前登录客户关联的唯一项目 """ permission_classes = [IsAuthenticated] def get(self, request): # 1. 通过登录用户关联找到客户 try: client = Client.objects.get(user=request.user) except Client.DoesNotExist: raise PermissionDenied("未找到当前用户关联的客户信息") # 2. 获取客户关联的唯一项目(一对一关系) try: project = ProjectContract.objects.get(client=client) except ProjectContract.DoesNotExist: return Response( {"error": "当前客户暂无关联项目"}, status=status.HTTP_404_NOT_FOUND ) # 3. 序列化返回项目信息 serializer = ProjectContractSerializer(project) return Response(serializer.data, status=status.HTTP_200_OK) class CustomerReportListView(APIView): """客户专属接口:获取当前客户项目的所有报告""" permission_classes = [IsAuthenticated] serializer_class = ReportSerializer def get(self, request): # 1. 通过登录用户关联找到客户(必做:确保是客户身份) try: client = Client.objects.get(user=request.user) except Client.DoesNotExist: raise PermissionDenied("未找到当前用户关联的客户信息") # 2. 关键修复:通过客户的一对一关联获取项目(client.project_contract) # 原因:Client与ProjectContract是一对一,反向关联名是project_contract(在models.py中定义) try: # 直接通过客户获取关联的项目(避免filter查询,一对一用get或直接访问反向关联) project = client.project_contract # 这里的project_contract对应models.py中Client的related_name # 3. 筛选该项目的所有报告 reports = Report.objects.filter(project=project).select_related("project") # 打印日志:确认获取到的报告(后端调试用) print(f"客户{client.name}的项目{project.project_name}的报告:", reports) except ProjectContract.DoesNotExist: return Response( {"error": "当前客户暂无关联项目,无法获取报告"}, status=status.HTTP_404_NOT_FOUND ) # 4. 序列化返回(确保报告数据正确) serializer = self.serializer_class(reports, many=True) # 打印日志:确认返回给前端的报告数据 print(f"返回给客户的报告数据:", serializer.data) return Response(serializer.data, status=status.HTTP_200_OK) serializers.py代码:from rest_framework import serializers from wxtest.models import Experimenter, Client,ProjectContract,Report from django.conf import settings class ExperimenterLoginSerializer(serializers.ModelSerializer): """实验员登录序列化器""" username = serializers.CharField(required=True) password = serializers.CharField(required=True, write_only=True) class Meta: model = Experimenter fields = ['password', 'username'] class ClientLoginSerializer(serializers.ModelSerializer): """客户登录序列化器 - 使用合同编号+客户名称登录""" contract_number = serializers.CharField(required=True) name = serializers.CharField(required=True) # 改为客户名称验证 class Meta: model = Client fields = ['contract_number', 'name'] class ProjectContractSerializer(serializers.ModelSerializer): """项目合同序列化器(用于创建/修改)""" # 新增:通过实验员关联获取用户名 experimenter_username = serializers.CharField(source='experimenter.username', read_only=True) client_name = serializers.CharField(source='client.name', read_only=True) client_contract_number = serializers.CharField( source='client.contract_number', read_only=True ) client_contract_input = serializers.CharField( source='client.contract_number', write_only=True, required=False, # 默认为非必填,后续动态调整 help_text="客户合同编号(创建项目时必填)" ) class Meta: model = ProjectContract # 关键修改:在 fields 中添加 missing 的字段(variety_name、experiment_purpose) fields = [ 'project_id', 'project_name', 'gene_id', 'start_date', 'end_date', 'status', 'client_name', 'client_contract_number', 'client_contract_input', 'experimenter', 'experimenter_username', 'variety_name', # 品种名称(对应前端 item.variety_name) 'experiment_purpose' # 实验目的(对应前端 item.experiment_purpose) ] extra_kwargs = { 'project_id': {'read_only': True}, 'experimenter': {'read_only': True}, } # 动态设置client_contract_input的必填性(原有逻辑不变) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance is None: self.fields['client_contract_input'].required = True else: self.fields['client_contract_input'].required = False # 验证客户合同编号(原有逻辑不变) def validate_client_contract_input(self, value): if value and not Client.objects.filter(contract_number=value).exists(): raise serializers.ValidationError("该合同编号对应的客户不存在") return value # 创建项目(原有逻辑不变,**validated_data 会自动包含新增字段**) def create(self, validated_data): experimenter = self.context['request'].user.experimenter client_data = validated_data.pop('client') client = Client.objects.get(contract_number=client_data['contract_number']) # 新增字段(variety_name、experiment_purpose)会通过 **validated_data 自动存入数据库 project = ProjectContract.objects.create( client=client, experimenter=experimenter, **validated_data ) return project # 更新项目(原有逻辑不变,新增字段会自动更新) def update(self, instance, validated_data): if 'client' in validated_data: validated_data.pop('client') if 'client_contract_number' in validated_data: validated_data.pop('client_contract_number') # 新增字段会通过 super().update 自动更新 return super().update(instance, validated_data) class ReportSerializer(serializers.ModelSerializer): """报告序列化器:含文件URL、报告类型名称""" # 显示报告类型的中文名称(如“载体信息报告”) report_type_name = serializers.CharField(source="get_report_type_display", read_only=True) # 显示项目名称(关联项目的基本信息) project_name = serializers.CharField(source="project.project_name", read_only=True) # 显示文件的完整访问URL(需配置MEDIA_URL) file_url = serializers.SerializerMethodField(read_only=True) class Meta: model = Report fields = [ "id", "project", "project_name", "report_type", "report_type_name", "file", "file_url", "upload_time", "uploader" ] extra_kwargs = { "uploader": {"read_only": True}, # 上传人自动关联当前实验员,无需前端传 "file": {"write_only": True} # 文件二进制数据仅用于上传,返回用file_url } # 自定义方法:生成文件的完整访问URL def get_file_url(self, obj): if not obj.file or not obj.file.name: print(f"警告:报告{obj.id}的文件不存在") return None # 确保BASE_URL与后端服务地址一致(如http://192.168.1.17:8000) base_url = getattr(settings, 'BASE_URL', 'http://192.168.1.17:8000') file_relative_path = obj.file.url # Django自动生成的媒体文件相对路径(如/media/reports/2025/09/xxx.pdf) # 拼接完整URL(处理相对路径是否带"/"的情况) if file_relative_path.startswith("/"): full_url = f"{base_url}{file_relative_path}" else: full_url = f"{base_url}/{file_relative_path}" print(f"报告{obj.id}的文件URL:", full_url) # 调试用,确认URL正确 return full_url model.spy代码:from django.db import models from django.core.validators import RegexValidator from django.contrib.auth.hashers import make_password from django.contrib.auth.models import User from django.utils import timezone from django import forms class Experimenter(models.Model): """实验员表""" user = models.OneToOneField( User, on_delete=models.CASCADE, related_name='experimenter', # 反向关联名,方便通过User获取Experimenter(如user.experimenter) null=True # 关键:允许为空,旧数据的 user_id 可以是 NULL ) experimenter_id = models.AutoField(primary_key=True, verbose_name='实验员ID') username = models.CharField(max_length=50, unique=True, verbose_name='登录用户名') password = models.CharField(max_length=100, verbose_name='密码') # 改为普通密码字段 phone = models.CharField( max_length=20, verbose_name='手机号', validators=[RegexValidator(r'^1[3-9]\d{9}$', '请输入有效的手机号')] ) class Meta: db_table = 'experimenter' verbose_name = '实验员' verbose_name_plural = '实验员列表' def __str__(self): return self.username class Client(models.Model): """客户表(与项目合同一对一关联)""" # 添加与User模型的一对一关联 user = models.OneToOneField( User, on_delete=models.CASCADE, related_name='client', # 反向关联名 null=True # 允许为空,兼容旧数据 ) client_id = models.AutoField(primary_key=True, verbose_name='客户ID') name = models.CharField(max_length=100, verbose_name='客户姓名') contract_number = models.CharField(max_length=100, unique=True, verbose_name='合同编号(登录凭证)') phone = models.CharField( max_length=20, verbose_name='手机号', validators=[RegexValidator(r'^1[3-9]\d{9}$', '请输入有效的手机号')] ) class Meta: db_table = 'client' verbose_name = '客户' verbose_name_plural = '客户列表' def __str__(self): return f"{self.name}({self.contract_number})" class ProjectContract(models.Model): """项目合同表(与客户一对一关联)""" project_id = models.AutoField(primary_key=True, verbose_name='项目ID') client = models.OneToOneField( Client, on_delete=models.CASCADE, db_column='client_id', related_name='project_contract', verbose_name='关联客户' ) experimenter = models.ForeignKey( Experimenter, on_delete=models.CASCADE, db_column='experimenter_id', related_name='managed_projects', verbose_name='负责人' ) project_name = models.CharField(max_length=200, verbose_name='项目名称') gene_id = models.CharField(max_length=50, blank=True, null=True, verbose_name='基因编号') variety_name = models.CharField(max_length=100, blank=True, null=True, verbose_name='品种名称') experiment_purpose = models.TextField(blank=True, null=True, verbose_name='实验目的') start_date = models.DateField(verbose_name='启动日期') end_date = models.DateField(blank=True, null=True, verbose_name='结束日期') status = models.IntegerField( choices=[ (1, '载体构建阶段'), (2, '品种转化及再生能力测试阶段'), (3, '农杆苗侵染阶段'), (4, '不定芽诱导阶段'), (5, '抗性筛选及阳性鉴定阶段'), (6, '温室培养阶段'), (7, '交付') ], default=1, verbose_name='项目状态' ) class Meta: db_table = 'project_contract' verbose_name = '项目合同' verbose_name_plural = '项目合同列表' def __str__(self): return f"{self.project_name}({self.client.contract_number})" #实验报告 class Report(models.Model): """报告模型:关联项目+存储4类报告PDF""" # 报告类型:与需求中的4类报告一一对应 REPORT_TYPE_CHOICES = [ (1, "载体信息报告"), (2, "品种转化及再生能力测试报告"), (3, "抗性筛选及阳性鉴定报告"), (4, "交付报告"), ] project = models.ForeignKey( ProjectContract, on_delete=models.CASCADE, related_name="reports", # 反向关联:项目→所有报告 verbose_name="关联项目" ) report_type = models.IntegerField( choices=REPORT_TYPE_CHOICES, verbose_name="报告类型" ) file = models.FileField( upload_to="reports/%Y/%m/", # 文件存储路径:media/reports/年/月/ verbose_name="PDF报告文件", null=True, blank=True # 允许初始为空(未上传) ) uploader = models.ForeignKey( Experimenter, on_delete=models.CASCADE, verbose_name="上传人(实验员)" ) upload_time = models.DateTimeField( default=timezone.now, verbose_name="上传时间" ) class Meta: db_table = "report" verbose_name = "项目报告" verbose_name_plural = "项目报告列表" # 唯一约束:同一项目同一报告类型只能有1个 unique_together = ("project", "report_type") def __str__(self): return f"{self.get_report_type_display()}({self.project.project_name})" midedleware.py代码:from django.http import HttpResponseForbidden from django.utils.deprecation import MiddlewareMixin from rest_framework.authtoken.models import Token class MediaFileAuthMiddleware(MiddlewareMixin): """仅验证用户登录状态,允许所有登录用户访问报告文件""" def process_request(self, request): # 仅拦截报告文件的访问请求 if request.path.startswith("/media/reports/"): # 验证Token是否存在且有效 token_header = request.META.get("HTTP_AUTHORIZATION") if not token_header or not token_header.startswith("Token "): return HttpResponseForbidden("请先登录") # 提取并验证Token token_key = token_header.split(" ")[1] try: # 只需验证Token存在即可,不检查具体权限 Token.objects.get(key=token_key) return None # Token有效,允许访问 except Token.DoesNotExist: return HttpResponseForbidden("无效的登录状态") # 非报告文件路径,直接放行 return None ,为什么当进入客户首页页面后,进入report页面,显示的报告仍为未上传,但是数据库中的数据是存在的, 前端返回数据:[system] WeChatLib: 3.8.10 (2025.7.4 16:39:26) [system] Subpackages: N/A [system] LazyCodeLoading: false app.js? [sm]:10 App onLaunch 执行 [Deprecation] SharedArrayBuffer will require cross-origin isolation as of M92, around July 2021. See https://developer.chrome.com/blog/enabling-shared-array-buffer/ for more details. [system] Launch Time: 2624 ms Tue Sep 02 2025 15:54:01 GMT+0800 (中国标准时间) 配置中关闭合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书检查 工具未校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书。 onLogin @ login.js? [sm]:66 login.js? [sm]:75 登录响应数据: {status: "success", user_type: "client", user_id: 1, name: "客户A", contract_number: "HT2023001", …} login.js? [sm]:94 登录成功,用户类型: client login.js? [sm]:95 准备调用 app.loginSuccess app.js? [sm]:28 loginSuccess 执行,用户信息: {status: "success", user_type: "client", user_id: 1, name: "客户A", contract_number: "HT2023001", …} app.js? [sm]:44 用户信息存储成功 app.js? [sm]:106 当前用户类型: client app.js? [sm]:113 尝试跳转到: /pages/customer/index/index index.js? [sm]:15 首页获取到的userData: {status: "success", user_type: "client", user_id: 1, name: "客户A", contract_number: "HT2023001", …} [pages/customer/index/index] 提示: text 组件包含了长文本,可以考虑增加 user-select 属性,方便用户复制。 report.js? [sm]:105 客户报告接口返回完整数据: {data: Array(4), header: Proxy, statusCode: 200, cookies: Array(0), accelerateType: "none", …} report.js? [sm]:113 客户项目ID(前端): 5 report.js? [sm]:114 后端返回的报告列表: (4) [{…}, {…}, {…}, {…}]0: {id: 1, project: 5, project_name: "水稻基因编辑项目", report_type: 1, report_type_name: "载体信息报告", …}1: {id: 5, project: 5, project_name: "水稻基因编辑项目", report_type: 2, report_type_name: "品种转化及再生能力测试报告", …}2: {id: 10, project: 5, project_name: "水稻基因编辑项目", report_type: 3, report_type_name: "抗性筛选及阳性鉴定报告", …}3: {id: 39, project: 5, project_name: "水稻基因编辑项目", report_type: 4, report_type_name: "交付报告", …}length: 4nv_length: (...)__proto__: Array(0) report.js? [sm]:121 单个报告数据: {id: 1, project: 5, project_name: "水稻基因编辑项目", report_type: 1, report_type_name: "载体信息报告", …}file_url: "http://192.168.1.17:8000/media/reports/2025/09/g10Ik6wo8mueb001a8dd4b4f8056baa6092fd565219c_UOsbHVB.pdf"id: 1project: 5project_name: "水稻基因编辑项目"report_type: 1report_type_name: "载体信息报告"upload_time: "2025-09-02T06:38:34.624515Z"uploader: 1__proto__: Object report.js? [sm]:121 单个报告数据: {id: 5, project: 5, project_name: "水稻基因编辑项目", report_type: 2, report_type_name: "品种转化及再生能力测试报告", …}file_url: "http://192.168.1.17:8000/media/reports/2025/09/QHBJk1NWfydeb001a8dd4b4f8056baa6092fd565219c_c0Pdh9v.pdf"id: 5project: 5project_name: "水稻基因编辑项目"report_type: 2report_type_name: "品种转化及再生能力测试报告"upload_time: "2025-09-01T06:46:23.248237Z"uploader: 1__proto__: Object report.js? [sm]:121 单个报告数据: {id: 10, project: 5, project_name: "水稻基因编辑项目", report_type: 3, report_type_name: "抗性筛选及阳性鉴定报告", …}file_url: "http://192.168.1.17:8000/media/reports/2025/09/PtlshNO88w8Fb001a8dd4b4f8056baa6092fd565219c_LErTtnq.pdf"id: 10project: 5project_name: "水稻基因编辑项目"report_type: 3report_type_name: "抗性筛选及阳性鉴定报告"upload_time: "2025-09-01T06:46:28.001618Z"uploader: 1__proto__: Object report.js? [sm]:121 单个报告数据: {id: 39, project: 5, project_name: "水稻基因编辑项目", report_type: 4, report_type_name: "交付报告", …}file_url: "http://192.168.1.17:8000/media/reports/2025/09/eYMSdCZWsRI9b001a8dd4b4f8056baa6092fd565219c.pdf"id: 39project: 5project_name: "水稻基因编辑项目"report_type: 4report_type_name: "交付报告"upload_time: "2025-09-01T06:46:32.225784Z"uploader: 1__proto__: Object report.js? [sm]:130 最终关联的报告类型: (4) [1, 2, 3, 4]0: 11: 22: 33: 4length: 4nv_length: (...)__proto__: Array(0) 终端上显示http://192.168.1.17:8000/media/reports/2025/09/PtlshNO88w8Fb001a8dd4b4f8056baa6092fd565219c_LErTtnq.pdf', 'upload_time': '2025-09-01T06:46:28.001618Z', 'uploader': 1}, {'id': 39, 'project': 5, 'project_name': '姘寸ɑ鍩哄洜缂栬緫椤圭洰', 'report_type': 4, 'report_type_name': '浜や 粯鎶ュ憡', 'file_url': 'http://192.168.1.17:8000/media/reports/2025/09/eYMSdCZWsRI9b001a8dd4b4f8056baa6092fd565219c.pdf', 'upload_time': '2025-09-01T06:46:32.225784Z', 'uploader': 1}] ] [02/Sep/2025 15:54:04] "GET /api/customer/reports/ HTTP/1.1" 200 1197。但是客户页面上显示为未上传,也就无法下载。怎么解决问题,修改相关代码
09-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值