feat: API Token鉴权 + 感悟/阅读/今日计划写入接口

This commit is contained in:
xiaji
2026-06-07 18:28:57 +08:00
parent 0e318b1c36
commit a06061d80e
3 changed files with 203 additions and 1 deletions

View File

@@ -40,6 +40,15 @@ urlpatterns = [
# API - 临时文件上传
path('api/v1/temp-upload/', views.api_temp_upload, name='api_temp_upload'),
# API - 感悟记录提交
path('api/v1/insight/submit/', views.api_submit_insight, name='api_submit_insight'),
# API - 阅读记录提交
path('api/v1/reading/submit/', views.api_submit_reading, name='api_submit_reading'),
# API - 今日计划提交
path('api/v1/plan/submit/', views.api_submit_plan, name='api_submit_plan'),
# 家庭事项
path('family-tasks/', views.family_tasks, name='family_tasks'),
path('family-tasks/add/', views.add_family_task, name='add_family_task'),

View File

@@ -10,6 +10,8 @@ from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from datetime import timedelta, datetime
from functools import wraps
import json
import os
import logging
from loguru import logger
@@ -42,6 +44,7 @@ def is_weasyprint_available():
from .models import (
ReadingRecord,
ReadingType,
InsightRecord,
Summary,
FamilyTask,
@@ -51,7 +54,10 @@ from .models import (
SummaryCategory,
PublicContentType,
PublicContent,
TempMessage
TempMessage,
Priority,
PlanType,
Status
)
from .forms import (
ReadingRecordForm,
@@ -65,6 +71,41 @@ from .forms import (
TempMessageForm
)
# ==================== API Token 鉴权 ====================
def require_api_token(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return JsonResponse({'success': False, 'message': '缺少认证Token'}, status=401)
token = auth_header[7:]
if token != settings.API_TOKEN:
return JsonResponse({'success': False, 'message': 'Token无效'}, status=401)
return view_func(request, *args, **kwargs)
return _wrapped_view
def _get_request_data(request):
content_type = request.content_type or ''
if 'application/json' in content_type:
try:
return json.loads(request.body.decode('utf-8'))
except (json.JSONDecodeError, UnicodeDecodeError):
return {}
return request.POST
def _parse_date(date_str):
if not date_str:
return timezone.now().date()
try:
return datetime.strptime(date_str.strip(), '%Y-%m-%d').date()
except (ValueError, AttributeError):
return timezone.now().date()
# =======================================================
# 首页视图
@login_required
def index(request):
@@ -871,6 +912,8 @@ def pdf_list(request):
return render(request, 'core/pdf_list.html', context)
@csrf_exempt
@require_api_token
def api_submit_summary(request):
"""API提交汇总记录 - 仅接受指定分类和发言人的记录"""
logger.info("API: 收到汇总记录提交请求")
@@ -931,6 +974,7 @@ def api_submit_summary(request):
return JsonResponse({'success': False, 'message': f"提交失败: {str(e)}"}, status=500)
@csrf_exempt
@require_api_token
def api_temp_upload(request):
"""API临时文件上传"""
logger.info("API: 收到临时文件上传请求")
@@ -1130,3 +1174,149 @@ def delete_public_content(request, pk):
context = {'content': content}
return render(request, 'core/delete_public_content.html', context)
# ==================== API 写入接口 ====================
@csrf_exempt
@require_api_token
def api_submit_insight(request):
logger.info("API: 收到感悟记录提交请求")
if request.method != 'POST':
return JsonResponse({'success': False, 'message': '只支持POST请求'}, status=405)
try:
data = _get_request_data(request)
content = (data.get('content') or '').strip()
speaker_name = (data.get('speaker') or '').strip()
if not content:
return JsonResponse({'success': False, 'message': '内容不能为空'}, status=400)
if not speaker_name:
return JsonResponse({'success': False, 'message': '发言人不能为空'}, status=400)
try:
speaker = FamilyMember.objects.get(name=speaker_name)
except FamilyMember.DoesNotExist:
return JsonResponse({'success': False, 'message': f"发言人 '{speaker_name}' 不存在"}, status=400)
record = InsightRecord.objects.create(
date=_parse_date(data.get('date')),
content=content,
speaker=speaker,
file=request.FILES.get('file')
)
logger.info(f"API: 感悟记录创建成功ID={record.id}")
return JsonResponse({'success': True, 'message': '提交成功', 'id': record.id})
except Exception as e:
logger.error(f"API: 提交感悟记录失败: {str(e)}")
return JsonResponse({'success': False, 'message': f"提交失败: {str(e)}"}, status=500)
@csrf_exempt
@require_api_token
def api_submit_reading(request):
logger.info("API: 收到阅读记录提交请求")
if request.method != 'POST':
return JsonResponse({'success': False, 'message': '只支持POST请求'}, status=405)
try:
data = _get_request_data(request)
type_name = (data.get('type') or '').strip()
title = (data.get('title') or '').strip()
if not type_name:
return JsonResponse({'success': False, 'message': '阅读类型不能为空'}, status=400)
if not title:
return JsonResponse({'success': False, 'message': '标题不能为空'}, status=400)
try:
reading_type = ReadingType.objects.get(name=type_name)
except ReadingType.DoesNotExist:
return JsonResponse({'success': False, 'message': f"阅读类型 '{type_name}' 不存在"}, status=400)
record = ReadingRecord.objects.create(
date=_parse_date(data.get('date')),
type=reading_type,
title=title,
source=(data.get('source') or '').strip() or None,
progress=(data.get('progress') or '').strip() or None,
note=(data.get('note') or '').strip() or None,
file=request.FILES.get('file')
)
logger.info(f"API: 阅读记录创建成功ID={record.id}")
return JsonResponse({'success': True, 'message': '提交成功', 'id': record.id})
except Exception as e:
logger.error(f"API: 提交阅读记录失败: {str(e)}")
return JsonResponse({'success': False, 'message': f"提交失败: {str(e)}"}, status=500)
@csrf_exempt
@require_api_token
def api_submit_plan(request):
logger.info("API: 收到今日计划提交请求")
if request.method != 'POST':
return JsonResponse({'success': False, 'message': '只支持POST请求'}, status=405)
try:
data = _get_request_data(request)
content = (data.get('content') or '').strip()
if not content:
return JsonResponse({'success': False, 'message': '内容不能为空'}, status=400)
speaker_name = (data.get('speaker') or '').strip()
if speaker_name:
try:
speaker = FamilyMember.objects.get(name=speaker_name)
except FamilyMember.DoesNotExist:
return JsonResponse({'success': False, 'message': f"发言人 '{speaker_name}' 不存在"}, status=400)
else:
speaker = FamilyMember.objects.get_or_create(name='机器人')[0]
priority_name = (data.get('priority') or '').strip()
if priority_name:
try:
priority = Priority.objects.get(name=priority_name)
except Priority.DoesNotExist:
return JsonResponse({'success': False, 'message': f"优先级 '{priority_name}' 不存在"}, status=400)
else:
priority = Priority.objects.get_or_create(name='')[0]
type_name = (data.get('type') or '').strip()
if type_name:
try:
plan_type = PlanType.objects.get(name=type_name)
except PlanType.DoesNotExist:
return JsonResponse({'success': False, 'message': f"计划类型 '{type_name}' 不存在"}, status=400)
else:
plan_type = PlanType.objects.get_or_create(name='其他')[0]
status_name = (data.get('status') or '').strip()
if status_name:
try:
status = Status.objects.get(name=status_name)
except Status.DoesNotExist:
return JsonResponse({'success': False, 'message': f"状态 '{status_name}' 不存在"}, status=400)
else:
status = Status.objects.get_or_create(name='未开始')[0]
record = TodayPlan.objects.create(
date=_parse_date(data.get('date')),
content=content,
speaker=speaker,
priority=priority,
type=plan_type,
status=status,
)
logger.info(f"API: 今日计划创建成功ID={record.id}")
return JsonResponse({'success': True, 'message': '提交成功', 'id': record.id})
except Exception as e:
logger.error(f"API: 提交今日计划失败: {str(e)}")
return JsonResponse({'success': False, 'message': f"提交失败: {str(e)}"}, status=500)

View File

@@ -125,6 +125,9 @@ STATIC_URL = 'static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# API Token configuration
API_TOKEN = 'diary-family-api-token-2026'
# Login URL configuration
LOGIN_URL = '/login/'