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)的第三方平均比例,单位:万金币/元。 不额外限制最低总价;汇总渠道报价并按公开页面展示规则排序、去重, 对第4~10条(Python切片[3:10])原始单价逐条计算1/Price后取算术平均, 四舍五入至两位小数,不按库存或份数加权。 每30分钟自动后台刷新,跨区页面最短间隔8秒(每分钟不超过8次),Redis锁避免重复刷新。 初始化跨区未齐时响应体code=202、data=[]并注明进度;9区缓存齐全才返回code=200及完整数据。 已有缓存即时返回,失败保留原更新时间,旧值最多保留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') complete = len(data) == len(thirdpart_gold.REGIONS) return response(code=200 if complete else 503, data=data if complete else [], message='第三方金价刷新暂不可用') if len(data) < len(thirdpart_gold.REGIONS): return response(code=202, data=[], message=f'第三方金价缓存尚未齐全({len(data)}/{len(thirdpart_gold.REGIONS)}跨区),后台刷新中或等待重试') return response(data=data)