优化
This commit is contained in:
+49
-22
@@ -1,18 +1,50 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from api.core.Gzip import gzip_zip, gzip_unzip
|
from api.core.Gzip import gzip_zip, gzip_unzip
|
||||||
from config.main import config
|
from config.main import config
|
||||||
|
|
||||||
|
logger = logging.getLogger('dcalc.redis_cache')
|
||||||
|
|
||||||
async def get_redis_info_async(redis, key, fun, expire = 0, without_gzip=False):
|
|
||||||
|
def _read_cached(redis, key, without_gzip):
|
||||||
|
content = redis.get(key)
|
||||||
|
if content is None:
|
||||||
|
return None
|
||||||
|
return json.loads(content if without_gzip else gzip_unzip(content))
|
||||||
|
|
||||||
|
|
||||||
|
def _write_cached(redis, key, info, expire, without_gzip):
|
||||||
|
content = json.dumps(info) if without_gzip else gzip_zip(json.dumps(info))
|
||||||
|
if expire > 0:
|
||||||
|
redis.set(key, content, ex=expire)
|
||||||
|
else:
|
||||||
|
redis.set(key, content)
|
||||||
|
|
||||||
|
|
||||||
|
async def _read_cached_async(redis, key, without_gzip):
|
||||||
|
content = await redis.get(key)
|
||||||
|
if content is None:
|
||||||
|
return None
|
||||||
|
return json.loads(content if without_gzip else gzip_unzip(content))
|
||||||
|
|
||||||
|
|
||||||
|
async def _write_cached_async(redis, key, info, expire, without_gzip):
|
||||||
|
content = json.dumps(info) if without_gzip else gzip_zip(json.dumps(info))
|
||||||
|
if expire > 0:
|
||||||
|
await redis.set(key, content, ex=expire)
|
||||||
|
else:
|
||||||
|
await redis.set(key, content)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_redis_info_async(redis, key, fun, expire=0, without_gzip=False):
|
||||||
info = None
|
info = None
|
||||||
if not config.DEBUG_MODE and redis:
|
if not config.DEBUG_MODE and redis:
|
||||||
try:
|
try:
|
||||||
if without_gzip:
|
info = await _read_cached_async(redis, key, without_gzip)
|
||||||
info = json.loads(redis.get(key))
|
|
||||||
else:
|
|
||||||
info = json.loads(gzip_unzip(redis.get(key)))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logger.warning('read Redis cache failed key=%s', key, exc_info=True)
|
||||||
info = None
|
info = None
|
||||||
if info is None:
|
if info is None:
|
||||||
if inspect.iscoroutinefunction(fun):
|
if inspect.iscoroutinefunction(fun):
|
||||||
@@ -20,31 +52,26 @@ async def get_redis_info_async(redis, key, fun, expire = 0, without_gzip=False):
|
|||||||
else:
|
else:
|
||||||
info = fun()
|
info = fun()
|
||||||
if not config.DEBUG_MODE and redis and info is not None:
|
if not config.DEBUG_MODE and redis and info is not None:
|
||||||
if without_gzip:
|
try:
|
||||||
redis.set(key, json.dumps(info))
|
await _write_cached_async(redis, key, info, expire, without_gzip)
|
||||||
else:
|
except Exception:
|
||||||
redis.set(key, gzip_zip(json.dumps(info)))
|
logger.warning('write Redis cache failed key=%s', key, exc_info=True)
|
||||||
if expire > 0:
|
|
||||||
redis.expire(key, expire)
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
def get_redis_info(redis, key, fun, expire = 0, without_gzip=False):
|
|
||||||
|
def get_redis_info(redis, key, fun, expire=0, without_gzip=False):
|
||||||
info = None
|
info = None
|
||||||
if not config.DEBUG_MODE and redis:
|
if not config.DEBUG_MODE and redis:
|
||||||
try:
|
try:
|
||||||
if without_gzip:
|
info = _read_cached(redis, key, without_gzip)
|
||||||
info = json.loads(redis.get(key))
|
|
||||||
else:
|
|
||||||
info = json.loads(gzip_unzip(redis.get(key)))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logger.warning('read Redis cache failed key=%s', key, exc_info=True)
|
||||||
info = None
|
info = None
|
||||||
if info is None:
|
if info is None:
|
||||||
info = fun()
|
info = fun()
|
||||||
if not config.DEBUG_MODE and redis and info is not None:
|
if not config.DEBUG_MODE and redis and info is not None:
|
||||||
if without_gzip:
|
try:
|
||||||
redis.set(key, json.dumps(info))
|
_write_cached(redis, key, info, expire, without_gzip)
|
||||||
else:
|
except Exception:
|
||||||
redis.set(key, gzip_zip(json.dumps(info)))
|
logger.warning('write Redis cache failed key=%s', key, exc_info=True)
|
||||||
if expire > 0:
|
|
||||||
redis.expire(key, expire)
|
|
||||||
return info
|
return info
|
||||||
|
|||||||
+4
-3
@@ -1,19 +1,20 @@
|
|||||||
import time
|
import time
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
from sqlalchemy.orm import Session
|
from redis.asyncio import Redis as AsyncRedis
|
||||||
from fastapi import Depends, Header
|
from fastapi import Depends, Header
|
||||||
|
|
||||||
from api.core.exception import ResponseException
|
from api.core.exception import ResponseException
|
||||||
from core.character.adventure import get_adv_info
|
from core.character.adventure import get_adv_info
|
||||||
from database.connect import get_db, get_redis
|
from database.connect import get_async_redis, get_redis
|
||||||
from api.core.Auth import AlterState
|
from api.core.Auth import AlterState
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
from api.core.Redis import get_redis_info
|
from api.core.Redis import get_redis_info
|
||||||
|
|
||||||
SessionDep = Annotated[Session, Depends(get_db)]
|
|
||||||
RedisDep = Annotated[Redis, Depends(get_redis)]
|
RedisDep = Annotated[Redis, Depends(get_redis)]
|
||||||
|
AsyncRedisDep = Annotated[AsyncRedis, Depends(get_async_redis)]
|
||||||
|
|
||||||
|
|
||||||
def alterToken(redis: RedisDep, alter_token: str | None = Header(None)):
|
def alterToken(redis: RedisDep, alter_token: str | None = Header(None)):
|
||||||
|
|||||||
+32
-33
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
from api.types.colg import HistoryGold, OfficialGold, RealTimeGold
|
from api.types.colg import HistoryGold, OfficialGold, RealTimeGold
|
||||||
from api.core.Gzip import register_gzip_request
|
from api.core.Gzip import register_gzip_request
|
||||||
@@ -6,7 +7,7 @@ import json
|
|||||||
from fastapi import APIRouter, Query
|
from fastapi import APIRouter, Query
|
||||||
from api.core.Response import response
|
from api.core.Response import response
|
||||||
from api.core.Redis import get_redis_info, get_redis_info_async
|
from api.core.Redis import get_redis_info, get_redis_info_async
|
||||||
from api.dp import RedisDep
|
from api.dp import AsyncRedisDep, RedisDep
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
import requests
|
import requests
|
||||||
from config.main import config
|
from config.main import config
|
||||||
@@ -15,6 +16,7 @@ from api.core.Response import Return as Response
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
logger = logging.getLogger('dcalc.colg')
|
||||||
|
|
||||||
register_gzip_request(router)
|
register_gzip_request(router)
|
||||||
|
|
||||||
@@ -39,6 +41,29 @@ payload_helper = {
|
|||||||
'token': config.HELPER_TOKEN,
|
'token': config.HELPER_TOKEN,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OFFICIAL_GOLD_URL = 'https://dzhu.qq.com/zangyi/activity/app'
|
||||||
|
OFFICIAL_GOLD_REGIONS = ('1', '2', '3A', '3B', '4', '5', '6', '7')
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_official_gold() -> list[dict]:
|
||||||
|
timeout = aiohttp.ClientTimeout(total=10)
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
|
||||||
|
async def fetch_region(region: str) -> dict:
|
||||||
|
form = {**payload_helper, 'serverNo': region}
|
||||||
|
async with session.post(OFFICIAL_GOLD_URL, data=form) as resp:
|
||||||
|
resp.raise_for_status()
|
||||||
|
payload = await resp.json(content_type=None)
|
||||||
|
data = payload['data']
|
||||||
|
return {
|
||||||
|
'updataTime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'region': f'跨{region}',
|
||||||
|
'goldDealRank': [int(item['price']) for item in data['godDealRank']],
|
||||||
|
'goldTopDealRank': [int(item['price']) for item in data['godTopDealRank']],
|
||||||
|
}
|
||||||
|
|
||||||
|
return await asyncio.gather(*(fetch_region(region) for region in OFFICIAL_GOLD_REGIONS))
|
||||||
|
|
||||||
|
|
||||||
def get_date_str(timestamp: int) -> str:
|
def get_date_str(timestamp: int) -> str:
|
||||||
utc_time = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
utc_time = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
||||||
@@ -47,7 +72,7 @@ def get_date_str(timestamp: int) -> str:
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/gold/realtime/', operation_id='realtimeGold', response_model=Response[list[RealTimeGold]])
|
@router.get('/gold/realtime/', operation_id='realtimeGold', response_model=Response[list[RealTimeGold]])
|
||||||
async def get_real_time_gold(redis: RedisDep) -> Response[list[RealTimeGold]]:
|
def get_real_time_gold(redis: RedisDep) -> Response[list[RealTimeGold]]:
|
||||||
"""
|
"""
|
||||||
返回所有跨区的实时金价信息
|
返回所有跨区的实时金价信息
|
||||||
"""
|
"""
|
||||||
@@ -74,7 +99,7 @@ async def get_real_time_gold(redis: RedisDep) -> Response[list[RealTimeGold]]:
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/gold/history/', operation_id='historyGold', response_model=Response[HistoryGold] | Response[None])
|
@router.get('/gold/history/', operation_id='historyGold', response_model=Response[HistoryGold] | Response[None])
|
||||||
async def get_history_gold(region: Annotated[int, Query(..., description='跨区id')], timeRange: Annotated[int, Query(..., description='时间跨度天数')] = 7) -> Response[HistoryGold] | Response[None]:
|
def get_history_gold(region: Annotated[int, Query(..., description='跨区id')], timeRange: Annotated[int, Query(..., description='时间跨度天数')] = 7) -> Response[HistoryGold] | Response[None]:
|
||||||
"""
|
"""
|
||||||
获取历史金价数据
|
获取历史金价数据
|
||||||
|
|
||||||
@@ -146,7 +171,7 @@ async def get_history_gold(region: Annotated[int, Query(..., description='跨区
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/gold/official/', operation_id='officialGold', response_model=Response[list[OfficialGold]])
|
@router.get('/gold/official/', operation_id='officialGold', response_model=Response[list[OfficialGold]])
|
||||||
async def get_official_gold(redis: RedisDep) -> Response[list[OfficialGold]]:
|
async def get_official_gold(redis: AsyncRedisDep) -> Response[list[OfficialGold]]:
|
||||||
"""
|
"""
|
||||||
获取DNF官方金币寄售实时数据
|
获取DNF官方金币寄售实时数据
|
||||||
|
|
||||||
@@ -156,36 +181,10 @@ async def get_official_gold(redis: RedisDep) -> Response[list[OfficialGold]]:
|
|||||||
- 该接口会返回所有跨区的官方金币寄售数据,包括最低成交价排名前5的价格和最高成交价排名前5的价格
|
- 该接口会返回所有跨区的官方金币寄售数据,包括最低成交价排名前5的价格和最高成交价排名前5的价格
|
||||||
- 数据来源于DNF官方提供的接口,可能会有一定的延迟和不稳定性
|
- 数据来源于DNF官方提供的接口,可能会有一定的延迟和不稳定性
|
||||||
"""
|
"""
|
||||||
official_url = 'https://dzhu.qq.com/zangyi/activity/app'
|
|
||||||
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
||||||
key = 'openapi:colg:real_time_gold_officical'
|
key = 'openapi:colg:real_time_gold_officical'
|
||||||
list = ['1', '2', '3A', '3B', '4', '5', '6', '7']
|
|
||||||
try:
|
try:
|
||||||
|
data = await get_redis_info_async(redis, key, _fetch_official_gold, 30 * 60)
|
||||||
async def get_data():
|
except Exception:
|
||||||
datas = []
|
logger.exception('fetch official gold failed')
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
tasks = []
|
|
||||||
for i in list:
|
|
||||||
payload_helper['serverNo'] = i
|
|
||||||
query = '&'.join(f'{k}={v}' for k, v in payload_helper.items())
|
|
||||||
tasks.append(session.post(official_url, data=query, headers=headers, timeout=10))
|
|
||||||
responses = await asyncio.gather(*tasks)
|
|
||||||
for index, resp in enumerate(responses):
|
|
||||||
res = json.loads(await resp.read())
|
|
||||||
datas.append(
|
|
||||||
{
|
|
||||||
'updataTime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
|
||||||
'region': f'跨{list[index]}',
|
|
||||||
'goldDealRank': [int(i.get('price')) for i in res['data']['godDealRank']],
|
|
||||||
'goldTopDealRank': [int(i.get('price')) for i in res['data']['godTopDealRank']],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return datas
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
return response(data=[])
|
return response(data=[])
|
||||||
# 设置半小时过期一次
|
|
||||||
data = await get_redis_info_async(redis, key, get_data, 30 * 60)
|
|
||||||
return response(data=data)
|
return response(data=data)
|
||||||
|
|||||||
+10
-10
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Body, Request, Response
|
|||||||
from api.core.Auth import createToken
|
from api.core.Auth import createToken
|
||||||
from api.core.Response import response
|
from api.core.Response import response
|
||||||
from api.core.Redis import get_redis_info, get_redis_info_async
|
from api.core.Redis import get_redis_info, get_redis_info_async
|
||||||
from api.dp import RedisDep, AltersDep
|
from api.dp import AsyncRedisDep, RedisDep, AltersDep
|
||||||
from core.basic.equipment import get_equipment
|
from core.basic.equipment import get_equipment
|
||||||
from core.character.adventure import get_adv_info, get_adv_list
|
from core.character.adventure import get_adv_info, get_adv_list
|
||||||
from core.basic.character import createCharacter
|
from core.basic.character import createCharacter
|
||||||
@@ -21,7 +21,7 @@ register_gzip_request(router)
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/adventure')
|
@router.get('/adventure')
|
||||||
async def get_adventure_info(request: Request, redis: RedisDep, name: str = None):
|
def get_adventure_info(request: Request, redis: RedisDep, name: str = None):
|
||||||
adventure_info = get_redis_info(redis, 'dcalc:adventure', get_adv_list)
|
adventure_info = get_redis_info(redis, 'dcalc:adventure', get_adv_list)
|
||||||
if name:
|
if name:
|
||||||
total = [item for job in adventure_info for item in job.get('children', [])]
|
total = [item for job in adventure_info for item in job.get('children', [])]
|
||||||
@@ -31,7 +31,7 @@ async def get_adventure_info(request: Request, redis: RedisDep, name: str = None
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/token/get/{alter}')
|
@router.get('/token/get/{alter}')
|
||||||
async def getToken(redis: RedisDep, alter: str, request: Request, version: str = None, equVersion: str = '0'):
|
def getToken(redis: RedisDep, alter: str, request: Request, version: str = None, equVersion: str = '0'):
|
||||||
if get_adv_info(alter) is None:
|
if get_adv_info(alter) is None:
|
||||||
raise ResponseException('无效的职业')
|
raise ResponseException('无效的职业')
|
||||||
token = createToken(alter, equVersion, redis)
|
token = createToken(alter, equVersion, redis)
|
||||||
@@ -39,7 +39,7 @@ async def getToken(redis: RedisDep, alter: str, request: Request, version: str =
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/character')
|
@router.get('/character')
|
||||||
async def get_character_info(request: Request, state: AltersDep, redis: RedisDep):
|
def get_character_info(request: Request, state: AltersDep, redis: RedisDep):
|
||||||
def get_character():
|
def get_character():
|
||||||
return createCharacter(state.alter, state.equVersion).getInfo()
|
return createCharacter(state.alter, state.equVersion).getInfo()
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ async def get_character_info(request: Request, state: AltersDep, redis: RedisDep
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/info/oath/{suitId}/{skillId}')
|
@router.get('/info/oath/{suitId}/{skillId}')
|
||||||
async def get_oath_info(state: AltersDep, suitId: str, skillId: str, rarity: str, lv: int, redis: RedisDep):
|
def get_oath_info(state: AltersDep, suitId: str, skillId: str, rarity: str, lv: int, redis: RedisDep):
|
||||||
def get_info():
|
def get_info():
|
||||||
skill = None
|
skill = None
|
||||||
version = state.equVersion
|
version = state.equVersion
|
||||||
@@ -73,7 +73,7 @@ async def get_oath_info(state: AltersDep, suitId: str, skillId: str, rarity: str
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/skill/{skillId}/{level}')
|
@router.get('/skill/{skillId}/{level}')
|
||||||
async def get_skill_info(state: AltersDep, skillId: str, level: int, redis: RedisDep):
|
def get_skill_info(state: AltersDep, skillId: str, level: int, redis: RedisDep):
|
||||||
"""
|
"""
|
||||||
获取技能信息
|
获取技能信息
|
||||||
"""
|
"""
|
||||||
@@ -99,7 +99,7 @@ async def get_skill_info(state: AltersDep, skillId: str, level: int, redis: Redi
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/dnfhelper/{uid}/list')
|
@router.get('/dnfhelper/{uid}/list')
|
||||||
async def get_dnfhelper_list(uid: str, redis: RedisDep):
|
async def get_dnfhelper_list(uid: str, redis: AsyncRedisDep):
|
||||||
cache_key = f'dnfhelper:{uid}'
|
cache_key = f'dnfhelper:{uid}'
|
||||||
|
|
||||||
async def get_list():
|
async def get_list():
|
||||||
@@ -111,7 +111,7 @@ async def get_dnfhelper_list(uid: str, redis: RedisDep):
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/dnfhelper/{uid}/{id}/')
|
@router.get('/dnfhelper/{uid}/{id}/')
|
||||||
async def get_dnfhelper_detail(uid: str, id: str, state: AltersDep, redis: RedisDep):
|
async def get_dnfhelper_detail(uid: str, id: str, state: AltersDep, redis: AsyncRedisDep):
|
||||||
cache_key = f'dnfhelper:{uid}:{id}'
|
cache_key = f'dnfhelper:{uid}:{id}'
|
||||||
|
|
||||||
async def get_detail():
|
async def get_detail():
|
||||||
@@ -123,7 +123,7 @@ async def get_dnfhelper_detail(uid: str, id: str, state: AltersDep, redis: Redis
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/dnfhelper/{uid}/')
|
@router.get('/dnfhelper/{uid}/')
|
||||||
async def get_info_by_dnfhelper(uid: str, state: AltersDep, redis: RedisDep):
|
async def get_info_by_dnfhelper(uid: str, state: AltersDep):
|
||||||
try:
|
try:
|
||||||
result = await get_user_info_by_id(uid, state)
|
result = await get_user_info_by_id(uid, state)
|
||||||
return response(data=result)
|
return response(data=result)
|
||||||
@@ -134,7 +134,7 @@ async def get_info_by_dnfhelper(uid: str, state: AltersDep, redis: RedisDep):
|
|||||||
|
|
||||||
|
|
||||||
@router.post('/skillTree/')
|
@router.post('/skillTree/')
|
||||||
async def get_skill_by_code(state: AltersDep, code=Body(None)):
|
def get_skill_by_code(state: AltersDep, code=Body(None)):
|
||||||
try:
|
try:
|
||||||
skills = get_skill_tree_info(code.get('code', ''))
|
skills = get_skill_tree_info(code.get('code', ''))
|
||||||
if skills.get('job', {}).get('class', '') != state.alter.replace('_carry', ''):
|
if skills.get('job', {}).get('class', '') != state.alter.replace('_carry', ''):
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ register_gzip_request(router)
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/skills/', operation_id='skillsList', response_model=SearchSkillResponse)
|
@router.get('/skills/', operation_id='skillsList', response_model=SearchSkillResponse)
|
||||||
async def get_skills_list(
|
def get_skills_list(
|
||||||
redis: RedisDep,
|
redis: RedisDep,
|
||||||
skillName: Annotated[str | None, Query(description='技能名称,支持模糊查询')] = None,
|
skillName: Annotated[str | None, Query(description='技能名称,支持模糊查询')] = None,
|
||||||
) -> SearchSkillResponse:
|
) -> SearchSkillResponse:
|
||||||
@@ -62,7 +62,7 @@ async def get_skills_list(
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/jobInfo/', operation_id='jobsInfo', response_model=JobInfoResponse)
|
@router.get('/jobInfo/', operation_id='jobsInfo', response_model=JobInfoResponse)
|
||||||
async def get_jobs_info(
|
def get_jobs_info(
|
||||||
redis: RedisDep,
|
redis: RedisDep,
|
||||||
jobName: Annotated[str, Query(..., description='职业名称,支持模糊查询')] = None,
|
jobName: Annotated[str, Query(..., description='职业名称,支持模糊查询')] = None,
|
||||||
) -> JobInfoResponse:
|
) -> JobInfoResponse:
|
||||||
@@ -98,7 +98,7 @@ async def get_jobs_info(
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/{jobId}/{jobGrowId}/skills/', operation_id='skillsByJob', response_model=SkillsListResponse)
|
@router.get('/{jobId}/{jobGrowId}/skills/', operation_id='skillsByJob', response_model=SkillsListResponse)
|
||||||
async def get_cn_skills_by_job(
|
def get_cn_skills_by_job(
|
||||||
redis: RedisDep,
|
redis: RedisDep,
|
||||||
jobId: Annotated[str, Path(..., description='职业')],
|
jobId: Annotated[str, Path(..., description='职业')],
|
||||||
jobGrowId: Annotated[str, Path(..., description='转职')],
|
jobGrowId: Annotated[str, Path(..., description='转职')],
|
||||||
@@ -122,7 +122,7 @@ async def get_cn_skills_by_job(
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/{jobId}/{jobGrowId}/{skillId}/', operation_id='skillDetail', response_model=SkillDetailResponse)
|
@router.get('/{jobId}/{jobGrowId}/{skillId}/', operation_id='skillDetail', response_model=SkillDetailResponse)
|
||||||
async def get_cn_skill_info(
|
def get_cn_skill_info(
|
||||||
redis: RedisDep,
|
redis: RedisDep,
|
||||||
jobId: Annotated[str, Path(..., description='职业')],
|
jobId: Annotated[str, Path(..., description='职业')],
|
||||||
jobGrowId: Annotated[str, Path(..., description='转职')],
|
jobGrowId: Annotated[str, Path(..., description='转职')],
|
||||||
@@ -164,7 +164,7 @@ async def get_cn_skill_info(
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/skillData/summary/', operation_id='skillDataSummary', response_model=SkillDataSummaryResponse)
|
@router.get('/skillData/summary/', operation_id='skillDataSummary', response_model=SkillDataSummaryResponse)
|
||||||
async def get_skill_data_summary(
|
def get_skill_data_summary(
|
||||||
redis: RedisDep,
|
redis: RedisDep,
|
||||||
job: Annotated[str, Query(description='职业')],
|
job: Annotated[str, Query(description='职业')],
|
||||||
skills: Annotated[
|
skills: Annotated[
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
import redis
|
"""Database connection helpers live in :mod:`database.connect`."""
|
||||||
|
|
||||||
pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True)
|
|
||||||
|
|||||||
+27
-11
@@ -1,11 +1,17 @@
|
|||||||
from functools import cache
|
from functools import cache
|
||||||
|
|
||||||
import redis
|
import redis
|
||||||
|
import redis.asyncio as async_redis
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.engine import Engine
|
||||||
from sqlalchemy.pool import NullPool
|
|
||||||
from config.main import BACKEND_DIR, config
|
from config.main import BACKEND_DIR, config
|
||||||
|
|
||||||
RedisPool = redis.ConnectionPool.from_url(config.REDIS_URL)
|
# redis-py 8 defaults to RESP3. Keep the existing RESP2 wire contract so the
|
||||||
|
# client remains compatible with older Redis servers and proxies.
|
||||||
|
RedisPool = redis.ConnectionPool.from_url(config.REDIS_URL, protocol=2)
|
||||||
|
AsyncRedisPool = async_redis.ConnectionPool.from_url(config.REDIS_URL, protocol=2)
|
||||||
|
_db_engines: set[Engine] = set()
|
||||||
|
|
||||||
|
|
||||||
def get_redis_client():
|
def get_redis_client():
|
||||||
@@ -17,16 +23,26 @@ def get_redis():
|
|||||||
yield redis_conn
|
yield redis_conn
|
||||||
|
|
||||||
|
|
||||||
|
async def get_async_redis():
|
||||||
|
redis_conn = async_redis.Redis(connection_pool=AsyncRedisPool)
|
||||||
|
try:
|
||||||
|
yield redis_conn
|
||||||
|
finally:
|
||||||
|
await redis_conn.aclose()
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def get_db_engine(version='0'):
|
def get_db_engine(version='0'):
|
||||||
db_path = BACKEND_DIR / 'dataFiles' / f'dcalc_{version}.db'
|
db_path = BACKEND_DIR / 'dataFiles' / f'dcalc_{version}.db'
|
||||||
return create_engine(
|
engine = create_engine(f'sqlite:///{db_path}')
|
||||||
f'sqlite:///{db_path}',
|
_db_engines.add(engine)
|
||||||
connect_args={'check_same_thread': False},
|
return engine
|
||||||
poolclass=NullPool,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
async def close_connection_pools() -> None:
|
||||||
with Session(get_db_engine()) as session:
|
RedisPool.close()
|
||||||
yield session
|
await AsyncRedisPool.aclose()
|
||||||
|
for engine in _db_engines:
|
||||||
|
engine.dispose()
|
||||||
|
_db_engines.clear()
|
||||||
|
get_db_engine.cache_clear()
|
||||||
|
|||||||
+5
-1
@@ -10,6 +10,7 @@ from api.core import Exceptions, Gzip, Middleware
|
|||||||
from api.core.Logging import configure_logging
|
from api.core.Logging import configure_logging
|
||||||
from api.main import api_router
|
from api.main import api_router
|
||||||
from config.main import config
|
from config.main import config
|
||||||
|
from database.connect import close_connection_pools
|
||||||
|
|
||||||
configure_logging()
|
configure_logging()
|
||||||
|
|
||||||
@@ -19,7 +20,10 @@ async def lifespan(_: FastAPI):
|
|||||||
print('dcalc is starting...')
|
print('dcalc is starting...')
|
||||||
print('Application starting...')
|
print('Application starting...')
|
||||||
print('Application startup complete.')
|
print('Application startup complete.')
|
||||||
yield
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
await close_connection_pools()
|
||||||
|
|
||||||
|
|
||||||
application = FastAPI(docs_url='/doc', redoc_url=None, lifespan=lifespan)
|
application = FastAPI(docs_url='/doc', redoc_url=None, lifespan=lifespan)
|
||||||
|
|||||||
@@ -5,16 +5,20 @@ description = "A backend project for dcalc, a calculator application."
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp>=3.13.3",
|
"aiohttp>=3.14.3",
|
||||||
"fastapi[all]>=0.136.3",
|
"fastapi[all]>=0.141.1",
|
||||||
"fastmcp>=3.3.1",
|
"fastmcp>=3.3.1",
|
||||||
"redis>=5.2.1",
|
"pydantic>=2.13.4",
|
||||||
"requests>=2.32.3",
|
"pydantic-settings>=2.15.0",
|
||||||
"sqlalchemy>=2.0.38",
|
"redis>=8.1.0",
|
||||||
|
"requests>=2.34.2",
|
||||||
|
"sqlalchemy>=2.0.52",
|
||||||
|
"uvicorn[standard]>=0.52.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
"httpx2>=2.10.0",
|
||||||
"pytest>=8.4.0",
|
"pytest>=8.4.0",
|
||||||
"ruff>=0.15.15",
|
"ruff>=0.15.15",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from api.routes import colg
|
||||||
|
|
||||||
|
|
||||||
|
class FakeResponse:
|
||||||
|
def __init__(self, region):
|
||||||
|
self.region = region
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, traceback):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def raise_for_status(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def json(self, content_type=None):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
return {
|
||||||
|
'data': {
|
||||||
|
'godDealRank': [{'price': '100'}, {'price': '101'}],
|
||||||
|
'godTopDealRank': [{'price': '200'}, {'price': '201'}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSession:
|
||||||
|
def __init__(self, calls, **kwargs):
|
||||||
|
self.calls = calls
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, traceback):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def post(self, url, data):
|
||||||
|
self.calls.append((url, data))
|
||||||
|
return FakeResponse(data['serverNo'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_official_gold_uses_async_http_with_isolated_forms(monkeypatch):
|
||||||
|
calls = []
|
||||||
|
sessions = []
|
||||||
|
|
||||||
|
def create_session(**kwargs):
|
||||||
|
session = FakeSession(calls, **kwargs)
|
||||||
|
sessions.append(session)
|
||||||
|
return session
|
||||||
|
|
||||||
|
monkeypatch.setattr(colg.aiohttp, 'ClientSession', create_session)
|
||||||
|
|
||||||
|
result = asyncio.run(colg._fetch_official_gold())
|
||||||
|
|
||||||
|
assert [data['serverNo'] for _, data in calls] == list(colg.OFFICIAL_GOLD_REGIONS)
|
||||||
|
assert all(url == colg.OFFICIAL_GOLD_URL for url, _ in calls)
|
||||||
|
assert len({id(data) for _, data in calls}) == len(colg.OFFICIAL_GOLD_REGIONS)
|
||||||
|
assert [item['region'] for item in result] == [f'跨{region}' for region in colg.OFFICIAL_GOLD_REGIONS]
|
||||||
|
assert result[0]['goldDealRank'] == [100, 101]
|
||||||
|
assert result[0]['goldTopDealRank'] == [200, 201]
|
||||||
|
assert sessions[0].kwargs['timeout'].total == 10
|
||||||
|
assert 'serverNo' not in colg.payload_helper
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
from api.core.Redis import get_redis_info, get_redis_info_async
|
||||||
|
from config.main import config
|
||||||
|
from database.connect import AsyncRedisPool, RedisPool
|
||||||
|
|
||||||
|
|
||||||
|
class FakeRedis:
|
||||||
|
def __init__(self, value=None, fail_write=False):
|
||||||
|
self.value = value
|
||||||
|
self.fail_write = fail_write
|
||||||
|
self.set_calls = []
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def set(self, key, value, **kwargs):
|
||||||
|
if self.fail_write:
|
||||||
|
raise RuntimeError('Redis unavailable')
|
||||||
|
self.set_calls.append((key, value, kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
class FakeAsyncRedis(FakeRedis):
|
||||||
|
async def get(self, key):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
async def set(self, key, value, **kwargs):
|
||||||
|
super().set(key, value, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shared_pool_keeps_resp2_compatibility():
|
||||||
|
assert RedisPool.connection_kwargs['protocol'] == 2
|
||||||
|
assert AsyncRedisPool.connection_kwargs['protocol'] == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_cache_sets_value_and_ttl_in_one_command(monkeypatch):
|
||||||
|
monkeypatch.setattr(config, 'DEBUG_MODE', False)
|
||||||
|
redis = FakeRedis()
|
||||||
|
|
||||||
|
result = get_redis_info(redis, 'key', lambda: {'value': 1}, expire=60, without_gzip=True)
|
||||||
|
|
||||||
|
assert result == {'value': 1}
|
||||||
|
assert redis.set_calls == [('key', json.dumps(result), {'ex': 60})]
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_cache_write_failure_does_not_fail_request(monkeypatch):
|
||||||
|
monkeypatch.setattr(config, 'DEBUG_MODE', False)
|
||||||
|
redis = FakeRedis(fail_write=True)
|
||||||
|
|
||||||
|
assert get_redis_info(redis, 'key', lambda: {'value': 1}, without_gzip=True) == {'value': 1}
|
||||||
|
|
||||||
|
|
||||||
|
def test_async_cache_awaits_async_redis_io(monkeypatch):
|
||||||
|
monkeypatch.setattr(config, 'DEBUG_MODE', False)
|
||||||
|
redis = FakeAsyncRedis()
|
||||||
|
|
||||||
|
async def load():
|
||||||
|
return {'value': 1}
|
||||||
|
|
||||||
|
result = asyncio.run(get_redis_info_async(redis, 'key', load, expire=60, without_gzip=True))
|
||||||
|
|
||||||
|
assert result == {'value': 1}
|
||||||
|
assert redis.set_calls == [('key', json.dumps(result), {'ex': 60})]
|
||||||
Generated
+635
-413
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user