From ce7d39f36ce5e189891968e9609eaf3c7212c31e Mon Sep 17 00:00:00 2001 From: xiaji Date: Mon, 25 May 2026 21:08:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=85=AC=E5=BC=80=E5=86=85=E5=AE=B9):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=B4=E6=97=B6=E6=96=87=E4=BB=B6=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=811?= =?UTF-8?q?=E5=B0=8F=E6=97=B6/1=E5=A4=A9/7=E5=A4=A9=E8=BF=87=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/forms.py | 19 +- core/models.py | 12 ++ core/tasks.py | 55 ++++++ core/templates/core/public_content.html | 244 ++++++++++++++++-------- core/urls.py | 3 + core/views.py | 78 +++++++- diary_family/settings.py | 4 + 7 files changed, 328 insertions(+), 87 deletions(-) diff --git a/core/forms.py b/core/forms.py index 66f0914..a7eff5c 100644 --- a/core/forms.py +++ b/core/forms.py @@ -125,4 +125,21 @@ class PublicContentForm(forms.ModelForm): 'file': forms.FileInput(attrs={'class': 'form-control'}), 'url': forms.URLInput(attrs={'class': 'form-control', 'placeholder': '请输入链接地址'}), 'sort_order': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '请输入排序值'}), - } \ No newline at end of file + } + + +class TempUploadForm(forms.ModelForm): + """临时文件上传表单""" + expire_type = forms.ChoiceField( + choices=[ + ('expire_1h', '1小时'), + ('expire_1d', '1天'), + ('expire_7d', '7天'), + ], + widget=forms.Select(attrs={'class': 'form-select'}), + label='过期时间' + ) + + class Meta: + model = PublicContent + fields = ['title', 'file'] \ No newline at end of file diff --git a/core/models.py b/core/models.py index 3c8d24d..1f765f4 100644 --- a/core/models.py +++ b/core/models.py @@ -240,6 +240,18 @@ class PublicContent(models.Model): url = models.URLField(blank=True, null=True, verbose_name="链接地址") sort_order = models.IntegerField(default=0, verbose_name="排序") is_published = models.BooleanField(default=True, verbose_name="是否发布") + is_temp_file = models.BooleanField(default=False, verbose_name="临时文件") + expire_type = models.CharField( + max_length=10, + choices=[ + ('expire_1h', '1小时'), + ('expire_1d', '1天'), + ('expire_7d', '7天'), + ], + blank=True, null=True, + verbose_name="过期类型" + ) + expire_at = models.DateTimeField(blank=True, null=True, verbose_name="过期时间") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") diff --git a/core/tasks.py b/core/tasks.py index 58a2f75..648dcbf 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -566,3 +566,58 @@ def celery_send_pdf_report_email(self): 'retries': self.request.retries if hasattr(self, 'request') else 0, 'timestamp': timezone.now().isoformat() } + + +@shared_task(bind=True) +def cleanup_expired_temp_files(self): + """ + 清理过期的临时文件 + 每小时执行一次,删除已过期的临时文件及其物理文件 + """ + task_id = self.request.id if hasattr(self, 'request') else 'unknown' + logger.info(f"[任务 {task_id}] 开始执行清理过期临时文件任务") + + try: + from core.models import PublicContent + + expired_files = PublicContent.objects.filter( + is_temp_file=True, + expire_at__lte=timezone.now() + ) + + deleted_count = 0 + for temp_file in expired_files: + try: + if temp_file.file: + file_path = temp_file.file.path + if os.path.exists(file_path): + os.remove(file_path) + logger.info(f"[任务 {task_id}] 已删除物理文件: {file_path}") + + file_title = temp_file.title + temp_file.delete() + deleted_count += 1 + logger.info(f"[任务 {task_id}] 已删除临时文件记录: {file_title}") + + except Exception as e: + logger.error(f"[任务 {task_id}] 删除临时文件失败: {str(e)}") + + logger.success(f"[任务 {task_id}] 清理完成,共删除 {deleted_count} 个过期临时文件") + return { + 'status': 'success', + 'task_id': task_id, + 'deleted_count': deleted_count, + 'timestamp': timezone.now().isoformat() + } + + except Exception as e: + error_msg = str(e) + error_traceback = traceback.format_exc() + logger.error(f"[任务 {task_id}] 清理过期临时文件失败: {error_msg}") + logger.error(f"[任务 {task_id}] 错误详情:\n{error_traceback}") + return { + 'status': 'failed', + 'task_id': task_id, + 'error': error_msg, + 'timestamp': timezone.now().isoformat() + } diff --git a/core/templates/core/public_content.html b/core/templates/core/public_content.html index a974d36..2f23ae0 100644 --- a/core/templates/core/public_content.html +++ b/core/templates/core/public_content.html @@ -1,80 +1,164 @@ -{% extends 'core/base.html' %} - -{% block content %} - -
-

- 公开内容 -

- {% if user.is_authenticated %} -
- - 添加内容 - -
- {% endif %} -
- -{% if content_by_type %} - {% for type_name, contents in content_by_type.items %} -
-
-
- {{ type_name }} -
- {{ contents|length }} 项 -
-
-
- {% for content in contents %} -
-
-
-
- {% if content.url %} - - {{ content.title }} - - {% else %} - {{ content.title }} - {% endif %} -
- {% if content.content %} -

{{ content.content|truncatechars:200 }}

- {% endif %} - {% if content.file %} - - 下载文件 - - {% endif %} -
- {% if user.is_authenticated %} - - {% endif %} -
-
- {% endfor %} -
-
-
- {% endfor %} -{% else %} -
- -
暂无公开内容
-

请稍后再来查看

- {% if user.is_authenticated %} - - 添加内容 - - {% endif %} -
-{% endif %} -{% endblock %} +{% extends 'core/base.html' %} + +{% block content %} + +
+

+ 公开内容 +

+ {% if user.is_authenticated %} +
+ + 添加内容 + +
+ {% endif %} +
+ +{% if content_by_type %} + {% for type_name, contents in content_by_type.items %} +
+
+
+ {{ type_name }} +
+ {{ contents|length }} 项 +
+
+
+ {% for content in contents %} +
+
+
+
+ {% if content.url %} + + {{ content.title }} + + {% else %} + {{ content.title }} + {% endif %} + {% if content.is_temp_file %} + 临时文件 + {% endif %} +
+ {% if content.content %} +

{{ content.content|truncatechars:200 }}

+ {% endif %} + {% if content.file %} + + 下载文件 + + {% endif %} + {% if content.is_temp_file and content.expire_at %} + {% load humanize %} + + {{ content.expire_at|naturaltime }}过期 + + {% endif %} +
+ {% if user.is_authenticated %} + + {% endif %} +
+
+ {% endfor %} +
+
+
+ {% endfor %} +{% else %} +
+ +
暂无公开内容
+

请稍后再来查看

+ {% if user.is_authenticated %} + + 添加内容 + + {% endif %} +
+{% endif %} + + +
+
+ {% csrf_token %} + + + + +
+
+ + +
+
+
+ 临时文件上传 API +
+
+
+
上传文件
+

端点: POST /api/v1/temp-upload/

+ +
参数
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数名类型必填说明
titlestring文件标题
filefile文件 (最大500MB)
expire_typestring过期时间: expire_1h (1小时) / expire_1d (1天) / expire_7d (7天)
+ +
响应示例
+
{
+  "success": true,
+  "message": "上传成功",
+  "id": 1,
+  "file_url": "/media/public_files/xxx.pdf",
+  "file_name": "document.pdf",
+  "file_size": 1048576,
+  "expire_at": "2026-05-25T18:30:00Z",
+  "expire_type": "expire_1d"
+}
+ +
cURL 示例
+
curl -X POST -F "file=@document.pdf" -F "title=测试文件" -F "expire_type=expire_1d" http://localhost:8000/api/v1/temp-upload/
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 4eb95bf..68ae0ad 100644 --- a/core/urls.py +++ b/core/urls.py @@ -36,6 +36,9 @@ urlpatterns = [ # API - 汇总记录提交 path('api/v1/summary/submit/', views.api_submit_summary, name='api_submit_summary'), + + # API - 临时文件上传 + path('api/v1/temp-upload/', views.api_temp_upload, name='api_temp_upload'), # 家庭事项 path('family-tasks/', views.family_tasks, name='family_tasks'), diff --git a/core/views.py b/core/views.py index 0bc17e3..3dbff2a 100644 --- a/core/views.py +++ b/core/views.py @@ -59,7 +59,8 @@ from .forms import ( FamilyTaskForm, TodayPlanForm, SystemConfigForm, - PublicContentForm + PublicContentForm, + TempUploadForm ) # 首页视图 @@ -927,6 +928,70 @@ def api_submit_summary(request): logger.error(f"API: 提交汇总记录失败: {str(e)}") return JsonResponse({'success': False, 'message': f"提交失败: {str(e)}"}, status=500) +@csrf_exempt +def api_temp_upload(request): + """API临时文件上传""" + logger.info("API: 收到临时文件上传请求") + + if request.method != 'POST': + return JsonResponse({'success': False, 'message': '只支持POST请求'}, status=405) + + try: + title = request.POST.get('title', '').strip() + expire_type = request.POST.get('expire_type', '').strip() + file = request.FILES.get('file') + + if not title: + return JsonResponse({'success': False, 'message': '标题不能为空'}, status=400) + + if not file: + return JsonResponse({'success': False, 'message': '文件不能为空'}, status=400) + + if expire_type not in ['expire_1h', 'expire_1d', 'expire_7d']: + return JsonResponse({'success': False, 'message': '无效的过期类型'}, status=400) + + try: + public_content_type = PublicContentType.objects.get(name='临时文件') + except PublicContentType.DoesNotExist: + public_content_type = PublicContentType.objects.create(name='临时文件') + + expire_delta_map = { + 'expire_1h': timedelta(hours=1), + 'expire_1d': timedelta(days=1), + 'expire_7d': timedelta(days=7), + } + expire_at = timezone.now() + expire_delta_map[expire_type] + + temp_content = PublicContent.objects.create( + type=public_content_type, + title=title, + file=file, + is_published=True, + is_temp_file=True, + expire_type=expire_type, + expire_at=expire_at, + ) + + file_url = request.build_absolute_uri(temp_content.file.url) if temp_content.file else None + file_size = temp_content.file.size if temp_content.file else 0 + + logger.info(f"API: 临时文件创建成功,ID={temp_content.id}, 过期时间={expire_at}") + return JsonResponse({ + 'success': True, + 'message': '上传成功', + 'id': temp_content.id, + 'file_url': file_url, + 'file_name': temp_content.file.name.split('/')[-1] if temp_content.file else None, + 'file_size': file_size, + 'expire_at': expire_at.isoformat(), + 'expire_type': expire_type, + }) + + except Exception as e: + logger.error(f"API: 临时文件上传失败: {str(e)}") + return JsonResponse({'success': False, 'message': f"上传失败: {str(e)}"}, status=500) + + # 获取syslog日志记录器(用于fail2ban检测) syslog_logger = logging.getLogger('django.security.login') @@ -979,21 +1044,22 @@ def user_logout(request): def public_content(request): """公开内容页面 - 无需登录""" logger.info("用户访问公开内容页面") - # 获取所有已发布的公开内容 public_contents = PublicContent.objects.filter(is_published=True) - - # 按类型分组 + content_by_type = {} for content in public_contents: type_name = content.type.name if type_name not in content_by_type: content_by_type[type_name] = [] content_by_type[type_name].append(content) - + + temp_upload_form = TempUploadForm() + context = { 'content_by_type': content_by_type, + 'temp_upload_form': temp_upload_form, } - + return render(request, 'core/public_content.html', context) # 添加公开内容 diff --git a/diary_family/settings.py b/diary_family/settings.py index 1d384f0..a231de2 100644 --- a/diary_family/settings.py +++ b/diary_family/settings.py @@ -190,6 +190,10 @@ CSRF_TRUSTED_ORIGINS = [ # 如果你将来有域名,也可以在这里加上,例如 "https://yourdomain.com" ] +# 文件上传大小限制 (500MB) +DATA_UPLOAD_MAX_MEMORY_SIZE = 524288000 +FILE_UPLOAD_MAX_MEMORY_SIZE = 524288000 + CELERY_BROKER_URL = 'redis://:xjjq1234!@localhost:6379/0' CELERY_RESULT_BACKEND = 'redis://:xjjq1234!@localhost:6379/0'