feat(djapi): 新增日报查询 API(news/reports + news/events)及文档

- 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/ 前缀
This commit is contained in:
2026-08-05 20:51:42 +08:00
parent 22ce3a6aea
commit 6ec198687c
17 changed files with 1275 additions and 21 deletions
+147
View File
@@ -0,0 +1,147 @@
"""
日报查询 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)