feat: API Token鉴权 + 感悟/阅读/今日计划写入接口
This commit is contained in:
@@ -39,6 +39,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'),
|
||||
|
||||
192
core/views.py
192
core/views.py
@@ -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)
|
||||
|
||||
@@ -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/'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user