- api/report/ 包:query(连库+SQL)/ views(2 视图)/ serializers(OpenAPI)/ tests(17 单测) - urls.py 注册 news/reports/、news/events/;settings.py SPECTACULAR 加「日报」tag - .env.example 补 NEWS_DB_* 占位配置;README/continuation.md 更新 - docs/news_report_api.md 使用手册;CLAUDE*.md 修正 CLI 路径为 finance/ 前缀
148 lines
5.6 KiB
Python
148 lines
5.6 KiB
Python
"""
|
||
日报查询 API 视图。
|
||
|
||
- GET /api/news/reports/ 日报查询(默认最近 24 小时;传 id 返回单份详情含事件)
|
||
- GET /api/news/events/ 跨日报重要事件聚合(最近 N 天 importance >= 阈值)
|
||
"""
|
||
|
||
from datetime import datetime, timedelta
|
||
|
||
from django.utils import timezone
|
||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||
from rest_framework.decorators import api_view
|
||
from rest_framework.response import Response
|
||
|
||
from . import query as report_query
|
||
from .serializers import (
|
||
ImportantEventSerializer,
|
||
ReportDetailSerializer,
|
||
ReportListSerializer,
|
||
)
|
||
|
||
_REPORT_TYPES = ("finance", "intl")
|
||
_SECTIONS = ("xwlb", "news", "cninfo", "intl")
|
||
|
||
|
||
def _parse_date_param(request, name):
|
||
"""解析 YYYY-MM-DD 参数,非法格式抛 ValueError。"""
|
||
raw = request.GET.get(name)
|
||
if not raw:
|
||
return None
|
||
try:
|
||
return datetime.strptime(raw, "%Y-%m-%d").date()
|
||
except ValueError:
|
||
raise ValueError(f"{name} 格式错误,应为 YYYY-MM-DD")
|
||
|
||
|
||
@extend_schema(
|
||
parameters=[
|
||
OpenApiParameter(name='report_type', type=str, required=False,
|
||
description='日报类型:finance | intl(默认两者)'),
|
||
OpenApiParameter(name='start_date', type=str, required=False,
|
||
description='起始日期 YYYY-MM-DD(默认:当前时间往前 24 小时)'),
|
||
OpenApiParameter(name='end_date', type=str, required=False,
|
||
description='结束日期 YYYY-MM-DD(默认今天)'),
|
||
OpenApiParameter(name='id', type=int, required=False,
|
||
description='日报 id,指定时返回单份详情(含事件,按板块/序号排序)'),
|
||
],
|
||
responses={200: ReportDetailSerializer},
|
||
description='AI 财经日报查询:默认返回最近 24 小时的日报列表(每天每类型取最新一份);'
|
||
'传 id 返回单份详情含事件',
|
||
tags=['日报'],
|
||
)
|
||
@api_view(['GET'])
|
||
def news_reports(request):
|
||
try:
|
||
report_id = request.GET.get('id')
|
||
if report_id is not None:
|
||
report_id = int(report_id)
|
||
if report_id <= 0:
|
||
raise ValueError("id 必须为正整数")
|
||
report_type = request.GET.get('report_type')
|
||
if report_type and report_type not in _REPORT_TYPES:
|
||
raise ValueError("report_type 仅支持 finance / intl")
|
||
start_date = _parse_date_param(request, 'start_date')
|
||
end_date = _parse_date_param(request, 'end_date')
|
||
except ValueError as e:
|
||
return Response({'error': str(e)}, status=400)
|
||
|
||
now = timezone.now()
|
||
start_date = start_date or (now - timedelta(hours=24)).date()
|
||
end_date = end_date or now.date()
|
||
|
||
try:
|
||
data = report_query.fetch_reports(
|
||
report_type=report_type,
|
||
start_date=start_date,
|
||
end_date=end_date,
|
||
report_id=report_id,
|
||
)
|
||
except Exception as e:
|
||
return Response({'error': f'查询失败: {e}'}, status=500)
|
||
|
||
if report_id is not None:
|
||
if data is None:
|
||
return Response({'error': f'日报 id={report_id} 不存在'}, status=404)
|
||
return Response(data)
|
||
return Response(data)
|
||
|
||
|
||
def _get_int_param(request, name, default, lo, hi):
|
||
"""解析整数参数并校验范围,非法抛 ValueError。"""
|
||
raw = request.GET.get(name)
|
||
if raw is None:
|
||
return default
|
||
try:
|
||
value = int(raw)
|
||
except ValueError:
|
||
raise ValueError(f"{name} 必须为整数")
|
||
if not (lo <= value <= hi):
|
||
raise ValueError(f"{name} 需在 {lo}~{hi} 之间")
|
||
return value
|
||
|
||
|
||
@extend_schema(
|
||
parameters=[
|
||
OpenApiParameter(name='days', type=int, required=False, default=7,
|
||
description='最近 N 天(1~365)'),
|
||
OpenApiParameter(name='importance', type=int, required=False, default=4,
|
||
description='最低重要度(1~5)'),
|
||
OpenApiParameter(name='report_type', type=str, required=False,
|
||
description='日报类型:finance | intl(默认两者)'),
|
||
OpenApiParameter(name='section', type=str, required=False,
|
||
description='板块:xwlb | news | cninfo | intl(默认全部)'),
|
||
OpenApiParameter(name='limit', type=int, required=False, default=100,
|
||
description='返回条数上限(1~500)'),
|
||
],
|
||
responses={200: ImportantEventSerializer(many=True)},
|
||
description='跨日报重要事件聚合:最近 N 天 importance >= 阈值的事件,'
|
||
'按重要度、日期降序',
|
||
tags=['日报'],
|
||
)
|
||
@api_view(['GET'])
|
||
def news_events(request):
|
||
try:
|
||
days = _get_int_param(request, 'days', 7, 1, 365)
|
||
importance = _get_int_param(request, 'importance', 4, 1, 5)
|
||
limit = _get_int_param(request, 'limit', 100, 1, 500)
|
||
report_type = request.GET.get('report_type')
|
||
if report_type and report_type not in _REPORT_TYPES:
|
||
raise ValueError("report_type 仅支持 finance / intl")
|
||
section = request.GET.get('section')
|
||
if section and section not in _SECTIONS:
|
||
raise ValueError("section 仅支持 xwlb / news / cninfo / intl")
|
||
except ValueError as e:
|
||
return Response({'error': str(e)}, status=400)
|
||
|
||
try:
|
||
data = report_query.fetch_important_events(
|
||
days=days,
|
||
importance=importance,
|
||
report_type=report_type,
|
||
section=section,
|
||
limit=limit,
|
||
)
|
||
except Exception as e:
|
||
return Response({'error': f'查询失败: {e}'}, status=500)
|
||
return Response(data)
|