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 说明
+
+
+
端点:
+
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 @@
+
+
+
+
+
+
+ {% 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 0000000..baaaead
Binary files /dev/null and b/dairy.png differ
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
+
+```
+
+### 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