174 lines
8.2 KiB
Python
174 lines
8.2 KiB
Python
import logging
|
||
|
||
from api.types.colg import HistoryGold, OfficialGold, RealTimeGold
|
||
from api.utils import official_gold, thirdpart_gold
|
||
from api.core.Gzip import register_gzip_request
|
||
import json
|
||
from fastapi import APIRouter, BackgroundTasks, Depends, Query
|
||
from redis.asyncio import Redis as AsyncRedis
|
||
from api.core.Response import response
|
||
from typing import Annotated
|
||
import requests
|
||
from config.main import config
|
||
from datetime import datetime, timezone, timedelta
|
||
from api.core.Response import Return as Response
|
||
|
||
router = APIRouter()
|
||
logger = logging.getLogger('dcalc.colg')
|
||
|
||
register_gzip_request(router)
|
||
|
||
regionMap = {
|
||
'跨1': 1,
|
||
'跨2': 2,
|
||
'跨3A': 3,
|
||
'跨3B': 4,
|
||
'跨4': 5,
|
||
'跨5': 6,
|
||
'跨6': 7,
|
||
'跨7': 8,
|
||
}
|
||
|
||
url = 'https://poservice.colg.cn/api/v1/trend/getCoinTrendDataKol'
|
||
|
||
payload = {'token': config.COLG_TOKEN, 'area': 0, 'time_type': 'other', 'start_time': '2026-01-06', 'end_time': '2026-01-06'}
|
||
|
||
def get_date_str(timestamp: int) -> str:
|
||
utc_time = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
||
beijing_time = utc_time + timedelta(hours=8)
|
||
return beijing_time.strftime('%Y-%m-%d %H:%M:%S')
|
||
|
||
|
||
@router.get('/gold/realtime/', operation_id='realtimeGold', response_model=Response[list[RealTimeGold]])
|
||
async def get_real_time_gold(redis: Annotated[AsyncRedis, Depends(thirdpart_gold.get_redis)], background_tasks: BackgroundTasks) -> Response[list[RealTimeGold]]:
|
||
"""获取网站金币单价,取页面第4~10条单价均值,元/万金币,两位小数。
|
||
|
||
updataTime为数据所属半小时刻度的数字秒级时间戳,:25/:55提前采集对应:30/:00数据。与thirdpart接口共用Redis缓存和
|
||
后台刷新任务:30分钟自动更新,也支持按需补齐,逐区间隔8秒;初始化未齐时响应体code=202。
|
||
"""
|
||
return await get_thirdpart_gold(redis, background_tasks)
|
||
|
||
|
||
@router.get('/gold/history/', operation_id='historyGold', response_model=Response[HistoryGold] | Response[None])
|
||
def get_history_gold(region: Annotated[int, Query(..., description='跨区id')], timeRange: Annotated[int, Query(..., description='时间跨度天数')] = 7) -> Response[HistoryGold] | Response[None]:
|
||
"""
|
||
获取历史金价数据
|
||
|
||
Args:
|
||
region (int): 跨区id,必填参数,跨区id对应的映射如下
|
||
{
|
||
'跨1': 1,
|
||
'跨2': 2,
|
||
'跨3A': 3,
|
||
'跨3B': 4,
|
||
'跨4': 5,
|
||
'跨5': 6,
|
||
'跨6': 7,
|
||
'跨7': 8,
|
||
}
|
||
timeRange (int, optional): 时间跨度天数,默认为7天
|
||
|
||
Returns:
|
||
Response[HistoryGold]: 返回历史金价数据的响应对象
|
||
|
||
Raises:
|
||
无显式异常,但会在跨区ID无效时返回404错误码
|
||
|
||
Note:
|
||
- region参数必须在regionMap的值范围内
|
||
- timeRange参数是可选的,使用默认值7表示最近7天的数据
|
||
"""
|
||
if region not in regionMap.values():
|
||
return response(
|
||
code=404,
|
||
message='跨区ID无效',
|
||
data=None,
|
||
)
|
||
region_name = [k for k, v in regionMap.items() if v == region][0]
|
||
end_date = datetime.now()
|
||
start_date = end_date - timedelta(days=timeRange - 1)
|
||
payload['area'] = region
|
||
payload['start_time'] = start_date.strftime('%Y-%m-%d')
|
||
payload['end_time'] = end_date.strftime('%Y-%m-%d')
|
||
data = []
|
||
echartsData = {}
|
||
try:
|
||
req = requests.post(url, headers={'Content-Type': 'application/json'}, data=json.dumps(payload))
|
||
req.raise_for_status()
|
||
resp = req.json().get('data', {})
|
||
for i in range(len(resp['x'])):
|
||
data.append(
|
||
{
|
||
'date': get_date_str(resp['x'][i]).split(' ')[0],
|
||
'avg': resp['y'][i],
|
||
'zero': resp['y1'][i],
|
||
}
|
||
)
|
||
except Exception as _:
|
||
return response(data=None)
|
||
echartsData = {
|
||
'title': {'text': f'DNF{region_name}近{timeRange}天金币价格趋势分析', 'left': 'center'},
|
||
'toolbox': {'feature': {'saveAsImage': {}, 'dataView': {'readOnly': True}, 'magicType': {'type': ['line', 'bar']}}},
|
||
'tooltip': {'trigger': 'axis', 'axisPointer': {'type': 'cross', 'label': {'backgroundColor': '#6a7985'}}},
|
||
'legend': {'data': ['平均金价', '12时金价'], 'bottom': 0},
|
||
'xAxis': {'type': 'category', 'name': '日期', 'boundaryGap': False, 'data': [item['date'] for item in data]},
|
||
'yAxis': {'type': 'value', 'name': '价格 (万金币/1人民币)', 'min': (min([item['avg'] for item in data] + [item['zero'] for item in data]) // 5) * 5, 'max': (max([item['avg'] for item in data] + [item['zero'] for item in data]) // 5) * 5 + 5, 'axisLabel': {'formatter': '{value} 万金币'}},
|
||
'series': [
|
||
{'name': '平均金价', 'type': 'line', 'smooth': True, 'data': [item['avg'] for item in data], 'markPoint': {'data': [{'type': 'max', 'name': '最大值'}, {'type': 'min', 'name': '最小值'}]}},
|
||
{'name': '12时金价', 'type': 'line', 'smooth': True, 'data': [item['zero'] for item in data], 'markLine': {'data': [{'type': 'average', 'name': '平均值'}]}},
|
||
],
|
||
}
|
||
return response(data={'region': region_name, 'data': data, 'echartsData': echartsData})
|
||
|
||
|
||
@router.get('/gold/official/', operation_id='officialGold', response_model=Response[list[OfficialGold]])
|
||
async def get_official_gold(redis: Annotated[AsyncRedis, Depends(thirdpart_gold.get_redis)]) -> Response[list[OfficialGold]]:
|
||
"""
|
||
获取DNF官方金币寄售实时数据
|
||
|
||
Returns:
|
||
Response[list[OfficialGold]]: 包含官方金币寄售数据的响应对象
|
||
Note:
|
||
- 该接口会返回所有跨区的官方金币寄售数据,包括最低成交价排名前5的价格和最高成交价排名前5的价格
|
||
- 数据来源于DNF官方提供的接口,可能会有一定的延迟和不稳定性
|
||
- 30分钟自动刷新,失败保留最多24小时的旧值;认证失败且配置了BARK_URL时发送通知
|
||
"""
|
||
try:
|
||
data = await official_gold.get_data(redis)
|
||
except Exception:
|
||
logger.exception('fetch official gold failed')
|
||
return response(code=503, data=[], message='官方金价缓存暂不可用')
|
||
if not data:
|
||
return response(code=503, data=[], message='官方金价暂不可用,请稍后重试')
|
||
return response(data=data)
|
||
|
||
|
||
@router.get('/gold/thirdpart/', operation_id='thirdpartGold', response_model=Response[list[RealTimeGold]])
|
||
async def get_thirdpart_gold(redis: Annotated[AsyncRedis, Depends(thirdpart_gold.get_redis)], background_tasks: BackgroundTasks) -> Response[list[RealTimeGold]]:
|
||
"""获取跨1~跨8(含跨3A/3B)的第三方单价均值,单位:元/万金币。
|
||
|
||
不额外限制最低总价;汇总渠道报价并按公开页面展示规则排序、去重,
|
||
对i.item1的第4~10条(Python切片[3:10])显示单价取算术平均,
|
||
四舍五入至两位小数,不按库存或份数加权。
|
||
|
||
每30分钟自动后台刷新,跨区页面最短间隔8秒(每分钟不超过8次),Redis锁避免重复刷新。
|
||
初始化跨区未齐时响应体code=202并注明进度;9区缓存齐全才返回code=200。
|
||
刷新中可返回部分已完成跨区,缺失跨区不补0。
|
||
已有缓存即时返回,失败保留原更新时间,旧值最多保留24小时。
|
||
该接口在DEBUG模式下也使用缓存,Redis不可用时不直接抓取上游。
|
||
"""
|
||
try:
|
||
data = await thirdpart_gold.read_cache(redis)
|
||
except Exception:
|
||
logger.exception('read thirdpart gold cache failed')
|
||
return response(code=503, data=[], message='第三方金价缓存暂不可用')
|
||
try:
|
||
await thirdpart_gold.schedule_refresh(redis, background_tasks, data)
|
||
except Exception:
|
||
logger.exception('schedule thirdpart gold refresh failed')
|
||
return response(code=200 if data else 503, data=data, message='第三方金价刷新暂不可用')
|
||
if len(data) < len(thirdpart_gold.REGIONS):
|
||
return response(code=202, data=data,
|
||
message=f'第三方金价缓存尚未齐全({len(data)}/{len(thirdpart_gold.REGIONS)}跨区),后台刷新中或等待重试')
|
||
return response(data=data)
|