Compare commits
3 Commits
3aa311b9da
...
b3847ed98d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3847ed98d | ||
|
|
aba4933a95 | ||
|
|
ce7d39f36c |
@@ -126,3 +126,20 @@ class PublicContentForm(forms.ModelForm):
|
||||
'url': forms.URLInput(attrs={'class': 'form-control', 'placeholder': '请输入链接地址'}),
|
||||
'sort_order': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '请输入排序值'}),
|
||||
}
|
||||
|
||||
|
||||
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']
|
||||
@@ -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="更新时间")
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -6,14 +6,34 @@
|
||||
<h2 class="mb-0">
|
||||
<i class="bi bi-globe me-2 text-info"></i>公开内容
|
||||
</h2>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-cloud-arrow-up me-1"></i>API上传
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end p-3" style="min-width: 350px;">
|
||||
<li>
|
||||
<h6 class="dropdown-header">临时文件上传 API</h6>
|
||||
<p class="small text-muted mb-2">端点: <code>POST /api/v1/temp-upload/</code></p>
|
||||
<hr>
|
||||
<p class="small mb-1"><strong>参数:</strong></p>
|
||||
<ul class="small mb-2">
|
||||
<li><code>title</code> - 文件标题</li>
|
||||
<li><code>file</code> - 文件(最大500MB)</li>
|
||||
<li><code>expire_type</code> - expire_1h / expire_1d / expire_7d</li>
|
||||
</ul>
|
||||
<p class="small mb-1"><strong>cURL示例:</strong></p>
|
||||
<pre class="small bg-dark text-white p-2 rounded"><code>curl -X POST -F "file=@f.pdf" -F "title=文件" -F "expire_type=expire_1d" /api/v1/temp-upload/</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
<div>
|
||||
<a href="{% url 'add_public_content' %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-lg me-1"></i>添加内容
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if content_by_type %}
|
||||
{% for type_name, contents in content_by_type.items %}
|
||||
@@ -38,6 +58,9 @@
|
||||
{% else %}
|
||||
<i class="bi bi-file-earmark-text me-1"></i>{{ content.title }}
|
||||
{% endif %}
|
||||
{% if content.is_temp_file %}
|
||||
<span class="badge bg-warning text-dark ms-2">临时文件</span>
|
||||
{% endif %}
|
||||
</h6>
|
||||
{% if content.content %}
|
||||
<p class="text-muted small mb-2">{{ content.content|truncatechars:200 }}</p>
|
||||
@@ -47,6 +70,12 @@
|
||||
<i class="bi bi-download me-1"></i>下载文件
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if content.is_temp_file and content.expire_at %}
|
||||
{% load humanize %}
|
||||
<span class="badge bg-secondary">
|
||||
<i class="bi bi-clock me-1"></i>{{ content.expire_at|naturaltime }}过期
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="btn-group ms-3">
|
||||
@@ -77,4 +106,79 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 临时文件上传表单 - 对人类用户视觉隐藏,agent可访问 -->
|
||||
<div style="width: 1px; height: 1px; overflow: hidden; position: absolute; left: -9999px;">
|
||||
<form method="post" enctype="multipart/form-data" action="{% url 'api_temp_upload' %}">
|
||||
{% csrf_token %}
|
||||
<input type="text" name="title" placeholder="文件标题">
|
||||
<select name="expire_type">
|
||||
<option value="expire_1h">1小时</option>
|
||||
<option value="expire_1d">1天</option>
|
||||
<option value="expire_7d">7天</option>
|
||||
</select>
|
||||
<input type="file" name="file">
|
||||
<button type="submit">上传</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- API文档 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="bi bi-terminal me-2"></i>临时文件上传 API
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted">上传文件</h6>
|
||||
<p><strong>端点:</strong> <code>POST /api/v1/temp-upload/</code></p>
|
||||
|
||||
<h6 class="text-muted mt-3">参数</h6>
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>参数名</th>
|
||||
<th>类型</th>
|
||||
<th>必填</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>string</td>
|
||||
<td>是</td>
|
||||
<td>文件标题</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>file</td>
|
||||
<td>file</td>
|
||||
<td>是</td>
|
||||
<td>文件 (最大500MB)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>expire_type</td>
|
||||
<td>string</td>
|
||||
<td>是</td>
|
||||
<td>过期时间: expire_1h (1小时) / expire_1d (1天) / expire_7d (7天)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h6 class="text-muted mt-3">响应示例</h6>
|
||||
<pre class="bg-dark text-white p-3 rounded"><code>{
|
||||
"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"
|
||||
}</code></pre>
|
||||
|
||||
<h6 class="text-muted mt-3">cURL 示例</h6>
|
||||
<pre class="bg-dark text-white p-3 rounded"><code>curl -X POST -F "file=@document.pdf" -F "title=测试文件" -F "expire_type=expire_1d" http://localhost:8000/api/v1/temp-upload/</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -37,6 +37,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'),
|
||||
path('family-tasks/add/', views.add_family_task, name='add_family_task'),
|
||||
|
||||
@@ -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,10 +1044,8 @@ 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
|
||||
@@ -990,8 +1053,11 @@ def public_content(request):
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
from celery import Celery
|
||||
from django.conf import settings
|
||||
from celery.schedules import crontab
|
||||
|
||||
# 设置默认的Django设置模块
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'diary_family.settings')
|
||||
@@ -14,6 +15,14 @@ app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||
# 自动发现任务
|
||||
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
|
||||
|
||||
# Celery Beat 周期任务配置
|
||||
app.conf.beat_schedule = {
|
||||
'cleanup-expired-temp-files': {
|
||||
'task': 'core.tasks.cleanup_expired_temp_files',
|
||||
'schedule': crontab(minute=0), # 每小时整点执行
|
||||
},
|
||||
}
|
||||
|
||||
@app.task(bind=True)
|
||||
def debug_task(self):
|
||||
"""调试任务"""
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user