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 - 临时文件上传 # API - 临时文件上传
path('api/v1/temp-upload/', views.api_temp_upload, name='api_temp_upload'), 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/', views.family_tasks, name='family_tasks'),
path('family-tasks/add/', views.add_family_task, name='add_family_task'), 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.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from datetime import timedelta, datetime from datetime import timedelta, datetime
from functools import wraps
import json
import os import os
import logging import logging
from loguru import logger from loguru import logger
@@ -42,6 +44,7 @@ def is_weasyprint_available():
from .models import ( from .models import (
ReadingRecord, ReadingRecord,
ReadingType,
InsightRecord, InsightRecord,
Summary, Summary,
FamilyTask, FamilyTask,
@@ -51,7 +54,10 @@ from .models import (
SummaryCategory, SummaryCategory,
PublicContentType, PublicContentType,
PublicContent, PublicContent,
TempMessage TempMessage,
Priority,
PlanType,
Status
) )
from .forms import ( from .forms import (
ReadingRecordForm, ReadingRecordForm,
@@ -65,6 +71,41 @@ from .forms import (
TempMessageForm 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 @login_required
def index(request): def index(request):
@@ -871,6 +912,8 @@ def pdf_list(request):
return render(request, 'core/pdf_list.html', context) return render(request, 'core/pdf_list.html', context)
@csrf_exempt
@require_api_token
def api_submit_summary(request): def api_submit_summary(request):
"""API提交汇总记录 - 仅接受指定分类和发言人的记录""" """API提交汇总记录 - 仅接受指定分类和发言人的记录"""
logger.info("API: 收到汇总记录提交请求") logger.info("API: 收到汇总记录提交请求")
@@ -931,6 +974,7 @@ def api_submit_summary(request):
return JsonResponse({'success': False, 'message': f"提交失败: {str(e)}"}, status=500) return JsonResponse({'success': False, 'message': f"提交失败: {str(e)}"}, status=500)
@csrf_exempt @csrf_exempt
@require_api_token
def api_temp_upload(request): def api_temp_upload(request):
"""API临时文件上传""" """API临时文件上传"""
logger.info("API: 收到临时文件上传请求") logger.info("API: 收到临时文件上传请求")
@@ -1130,3 +1174,149 @@ def delete_public_content(request, pk):
context = {'content': content} context = {'content': content}
return render(request, 'core/delete_public_content.html', context) 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' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# API Token configuration
API_TOKEN = 'diary-family-api-token-2026'
# Login URL configuration # Login URL configuration
LOGIN_URL = '/login/' LOGIN_URL = '/login/'