From b1cf94cd230b33532f959d5a296017bdffcd419d Mon Sep 17 00:00:00 2001 From: xiaji Date: Mon, 25 May 2026 22:04:57 +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=E7=95=99=E8=A8=80=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E7=95=99=E8=A8=80=E4=BF=9D=E7=95=9910?= =?UTF-8?q?=E5=88=86=E9=92=9F=EF=BC=8C=E6=98=BE=E7=A4=BA=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=90=8D=E3=80=81=E5=86=85=E5=AE=B9=E3=80=81=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E5=92=8C=E6=9D=A5=E6=BA=90IP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/forms.py | 22 +- core/models.py | 17 ++ core/tasks.py | 46 +++++ core/templates/core/public_content.html | 193 +++++++++++++++++- core/views.py | 27 ++- dairy.png | Bin 0 -> 16831 bytes diary_family/celery.py | 4 + .../2026-05-25-temp-file-upload-design.md | 145 +++++++++++++ 8 files changed, 449 insertions(+), 5 deletions(-) create mode 100644 dairy.png create mode 100644 docs/superpowers/specs/2026-05-25-temp-file-upload-design.md diff --git a/core/forms.py b/core/forms.py index a7eff5c..35e1ef4 100644 --- a/core/forms.py +++ b/core/forms.py @@ -9,7 +9,8 @@ from .models import ( TodayPlan, SystemConfig, FamilyMember, - PublicContent + PublicContent, + TempMessage ) class ReadingRecordForm(forms.ModelForm): @@ -142,4 +143,21 @@ class TempUploadForm(forms.ModelForm): class Meta: model = PublicContent - fields = ['title', 'file'] \ No newline at end of file + fields = ['title', 'file'] + + +class TempMessageForm(forms.ModelForm): + """临时留言表单""" + class Meta: + model = TempMessage + fields = ['username', 'content'] + widgets = { + 'username': forms.TextInput(attrs={'class': 'form-control', 'maxlength': '20', 'placeholder': '用户名(可选,最多20字节)'}), + 'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'maxlength': '1000', 'placeholder': '说点什么...(最多1000字节)'}), + } + + def clean_content(self): + content = self.cleaned_data.get('content', '') + if len(content.encode('utf-8')) > 1000: + raise forms.ValidationError("内容不能超过1000字节") + return content \ No newline at end of file diff --git a/core/models.py b/core/models.py index 1f765f4..2945397 100644 --- a/core/models.py +++ b/core/models.py @@ -262,3 +262,20 @@ class PublicContent(models.Model): def __str__(self): return f"{self.type.name} - {self.title}" + + +class TempMessage(models.Model): + """临时留言""" + username = models.CharField(max_length=20, blank=True, null=True, verbose_name="用户名") + content = models.CharField(max_length=1000, verbose_name="内容") + ip_address = models.GenericIPAddressField(blank=True, null=True, verbose_name="来源IP") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + expire_at = models.DateTimeField(blank=True, null=True, verbose_name="过期时间") + + class Meta: + verbose_name = "临时留言" + verbose_name_plural = "临时留言" + ordering = ['-created_at'] + + def __str__(self): + return f"{self.username or '匿名'} - {self.content[:20]}..." diff --git a/core/tasks.py b/core/tasks.py index 648dcbf..efadaaf 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -621,3 +621,49 @@ def cleanup_expired_temp_files(self): 'error': error_msg, 'timestamp': timezone.now().isoformat() } + + +@shared_task(bind=True) +def cleanup_expired_messages(self): + """ + 清理过期的临时留言 + 每5分钟执行一次,删除已过期的留言 + """ + task_id = self.request.id if hasattr(self, 'request') else 'unknown' + logger.info(f"[任务 {task_id}] 开始执行清理过期留言任务") + + try: + from core.models import TempMessage + + expired_messages = TempMessage.objects.filter(expire_at__lte=timezone.now()) + + deleted_count = 0 + for message in expired_messages: + try: + msg_content = message.content[:20] + message.delete() + deleted_count += 1 + logger.info(f"[任务 {task_id}] 已删除过期留言: {msg_content}...") + + 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 b48c402..9a9fefe 100644 --- a/core/templates/core/public_content.html +++ b/core/templates/core/public_content.html @@ -1,6 +1,140 @@ {% extends 'core/base.html' %} +{% load humanize %} {% block content %} + + + + +
+
API 说明
+
+ API 说明 + +
+
+

端点:

+ POST /api/v1/temp-upload/ +

参数:

+
    +
  • title 文件标题
  • +
  • file 文件(≤500MB)
  • +
  • expire_type expire_1h / 1d / 7d
  • +
+

cURL:

+
curl -X POST \
+  -F "file=@f.pdf" \
+  -F "title=文件" \
+  -F "expire_type=expire_1d" \
+  /api/v1/temp-upload/
+
+
+ + +

@@ -35,6 +169,64 @@

+ +
+
+
+ 临时发言 + 留言仅保留10分钟 +
+
+
+
+ {% csrf_token %} +
+
+ +
+
+ +
+
+ +
+
+
+ + {% if temp_messages %} +
+ {% for message in temp_messages %} +
+
+
+
+ {{ message.username|default:"匿名" }} + + {{ message.created_at|naturaltime }} + +
+

{{ message.content }}

+ + IP: {{ message.ip_address|default:"未知" }} + +
+ + {{ message.created_at|naturaltime }} + +
+
+ {% endfor %} +
+ {% else %} +

+ 暂无留言 +

+ {% endif %} +
+
+ {% if content_by_type %} {% for type_name, contents in content_by_type.items %}
@@ -71,7 +263,6 @@ {% endif %} {% if content.is_temp_file and content.expire_at %} - {% load humanize %} {{ content.expire_at|naturaltime }}过期 diff --git a/core/views.py b/core/views.py index 3dbff2a..8f60677 100644 --- a/core/views.py +++ b/core/views.py @@ -50,7 +50,8 @@ from .models import ( FamilyMember, SummaryCategory, PublicContentType, - PublicContent + PublicContent, + TempMessage ) from .forms import ( ReadingRecordForm, @@ -60,7 +61,8 @@ from .forms import ( TodayPlanForm, SystemConfigForm, PublicContentForm, - TempUploadForm + TempUploadForm, + TempMessageForm ) # 首页视图 @@ -1044,6 +1046,24 @@ def user_logout(request): def public_content(request): """公开内容页面 - 无需登录""" logger.info("用户访问公开内容页面") + + if request.method == 'POST': + form = TempMessageForm(request.POST) + if form.is_valid(): + message = form.save(commit=False) + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + message.ip_address = x_forwarded_for.split(',')[0].strip() + else: + message.ip_address = request.META.get('REMOTE_ADDR') + from datetime import timedelta + message.expire_at = timezone.now() + timedelta(minutes=10) + message.save() + logger.info(f"临时留言: {message.username or '匿名'} - {message.content[:20]}...") + return redirect('public_content') + else: + form = TempMessageForm() + public_contents = PublicContent.objects.filter(is_published=True) content_by_type = {} @@ -1054,10 +1074,13 @@ def public_content(request): content_by_type[type_name].append(content) temp_upload_form = TempUploadForm() + temp_messages = TempMessage.objects.filter(expire_at__gt=timezone.now()) context = { 'content_by_type': content_by_type, 'temp_upload_form': temp_upload_form, + 'temp_messages': temp_messages, + 'temp_message_form': form, } return render(request, 'core/public_content.html', context) diff --git a/dairy.png b/dairy.png new file mode 100644 index 0000000000000000000000000000000000000000..baaaeadbc16851d56d3e65e6c59540c37762967d GIT binary patch literal 16831 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 Hu0mjfndly@ literal 0 HcmV?d00001 diff --git a/diary_family/celery.py b/diary_family/celery.py index 00bbadc..a807ef3 100644 --- a/diary_family/celery.py +++ b/diary_family/celery.py @@ -21,6 +21,10 @@ app.conf.beat_schedule = { 'task': 'core.tasks.cleanup_expired_temp_files', 'schedule': crontab(minute=0), # 每小时整点执行 }, + 'cleanup-expired-messages': { + 'task': 'core.tasks.cleanup_expired_messages', + 'schedule': crontab(minute='*/5'), # 每5分钟执行 + }, } @app.task(bind=True) diff --git a/docs/superpowers/specs/2026-05-25-temp-file-upload-design.md b/docs/superpowers/specs/2026-05-25-temp-file-upload-design.md new file mode 100644 index 0000000..529b94e --- /dev/null +++ b/docs/superpowers/specs/2026-05-25-temp-file-upload-design.md @@ -0,0 +1,145 @@ +# 临时文件上传功能设计 + +**日期**: 2026-05-25 +**状态**: 已批准 + +## 概述 + +在现有 `/public/` 公开内容页面中增加临时文件上传功能,允许任何人上传文件并自动在指定时间后删除。 + +## 功能需求 + +1. 上传临时文件作为 PublicContent 类型发布 +2. 上传者选择过期时间:1小时 / 1天 / 7天 +3. 上传文件最大500MB +4. 过期文件自动从存储中删除 +5. 上传表单对人类用户视觉隐藏(1px宽度),但agent可访问 +6. 提供REST API方式上传文件 + +## 技术方案 + +### 1. 模型改动 + +**文件**: `core/models.py` + +在 `PublicContent` 模型中增加字段: + +```python +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='过期时间') +``` + +### 2. 表单改动 + +**文件**: `core/forms.py` + +扩展 `PublicContentForm`,增加过期类型选择字段。 + +### 3. 上传表单HTML + +**文件**: `core/templates/core/public_content.html` + +在页面底部添加上传表单,设置 `style="width: 1px"` 使人类用户难以阅读,但agent可解析。 + +```html +
+ {% csrf_token %} + {{ temp_upload_form }} + +
+``` + +### 4. API端点 + +**文件**: `core/urls.py` 和 `core/views.py` + +新增 API 端点 `POST /api/v1/temp-upload/`: + +- 路径: `api/v1/temp-upload/` +- 方法: POST +- 参数: + - `file`: 文件 (必填, 最大500MB) + - `title`: 文件标题 (必填) + - `expire_type`: 过期类型 `expire_1h` | `expire_1d` | `expire_7d` (必填) +- 返回: JSON `{ "success": true, "file_url": "...", "expire_at": "..." }` + +### 5. 定时清理任务 + +**文件**: `core/tasks.py` + +新增 Celery 任务 `cleanup_expired_temp_files`: + +```python +@shared_task +def cleanup_expired_temp_files(): + expired_files = PublicContent.objects.filter( + is_temp_file=True, + expire_at__lte=timezone.now() + ) + for file in expired_files: + # 删除物理文件 + if file.file: + file.file.delete() + file.delete() +``` + +配置 Celery Beat 每小时执行。 + +### 6. API文档(写在公开内容页面) + +```markdown +## 临时文件上传 API + +### 上传文件 +POST /api/v1/temp-upload/ + +**参数**: +- `file`: 文件 (multipart/form-data, 最大500MB) +- `title`: 文件标题 (string) +- `expire_type`: 过期时间 (`expire_1h` | `expire_1d` | `expire_7d`) + +**响应**: +```json +{ + "success": true, + "file_url": "/media/temp_files/xxx.pdf", + "expire_at": "2026-05-25T18:30:00Z", + "file_name": "document.pdf", + "file_size": 1048576 +} +``` + +**示例**: +```bash +curl -X POST -F "file=@document.pdf" -F "title=测试文件" -F "expire_type=expire_1d" http://localhost:8000/api/v1/temp-upload/ +``` + +## 数据库迁移 + +```bash +python manage.py makemigrations core --name add_temp_file_fields +python manage.py migrate +``` + +## Celery Beat 配置 + +在 `diary_family/celery.py` 中添加周期任务: + +```python +CELERY_BEAT_SCHEDULE = { + 'cleanup-expired-temp-files': { + 'task': 'core.tasks.cleanup_expired_temp_files', + 'schedule': crontab(minute=0), # 每小时整点执行 + }, +} +``` \ No newline at end of file