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