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 %}
-
-
-
-
- {% 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 %}
+
+
+
+
+ {% 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 %}
+
+
+
+
+
+
+
+
+
+
+
上传文件
+
端点: POST /api/v1/temp-upload/
+
+
参数
+
+
+
+ | 参数名 |
+ 类型 |
+ 必填 |
+ 说明 |
+
+
+
+
+ | title |
+ string |
+ 是 |
+ 文件标题 |
+
+
+ | file |
+ file |
+ 是 |
+ 文件 (最大500MB) |
+
+
+ | expire_type |
+ string |
+ 是 |
+ 过期时间: 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'