updte
This commit is contained in:
@@ -39,6 +39,9 @@ uv run ruff check .
|
|||||||
`updataTime` types remain number for realtime and string for official.
|
`updataTime` types remain number for realtime and string for official.
|
||||||
Redis locks prevent duplicate work across workers. Realtime region pages stay
|
Redis locks prevent duplicate work across workers. Realtime region pages stay
|
||||||
8 seconds apart. Failures retain the last successful data for up to 24 hours.
|
8 seconds apart. Failures retain the last successful data for up to 24 hours.
|
||||||
|
Before all realtime regions are available, the response body has `code=202`
|
||||||
|
and `data=[]`; progress appears only in `message`. A successful response never
|
||||||
|
substitutes a partially populated region list for the complete dataset.
|
||||||
- Realtime selects rows 4–10 (slice `[3:10]`), converts each original unit price
|
- Realtime selects rows 4–10 (slice `[3:10]`), converts each original unit price
|
||||||
with `1 / Price`, then averages those ratios in 万金币/元. Rounding to 2 decimals
|
with `1 / Price`, then averages those ratios in 万金币/元. Rounding to 2 decimals
|
||||||
happens only after averaging; prices are not rounded before conversion. A new
|
happens only after averaging; prices are not rounded before conversion. A new
|
||||||
|
|||||||
@@ -152,8 +152,7 @@ async def get_thirdpart_gold(redis: Annotated[AsyncRedis, Depends(thirdpart_gold
|
|||||||
四舍五入至两位小数,不按库存或份数加权。
|
四舍五入至两位小数,不按库存或份数加权。
|
||||||
|
|
||||||
每30分钟自动后台刷新,跨区页面最短间隔8秒(每分钟不超过8次),Redis锁避免重复刷新。
|
每30分钟自动后台刷新,跨区页面最短间隔8秒(每分钟不超过8次),Redis锁避免重复刷新。
|
||||||
初始化跨区未齐时响应体code=202并注明进度;9区缓存齐全才返回code=200。
|
初始化跨区未齐时响应体code=202、data=[]并注明进度;9区缓存齐全才返回code=200及完整数据。
|
||||||
刷新中可返回部分已完成跨区,缺失跨区不补0。
|
|
||||||
已有缓存即时返回,失败保留原更新时间,旧值最多保留24小时。
|
已有缓存即时返回,失败保留原更新时间,旧值最多保留24小时。
|
||||||
该接口在DEBUG模式下也使用缓存,Redis不可用时不直接抓取上游。
|
该接口在DEBUG模式下也使用缓存,Redis不可用时不直接抓取上游。
|
||||||
"""
|
"""
|
||||||
@@ -166,8 +165,9 @@ async def get_thirdpart_gold(redis: Annotated[AsyncRedis, Depends(thirdpart_gold
|
|||||||
await thirdpart_gold.schedule_refresh(redis, background_tasks, data)
|
await thirdpart_gold.schedule_refresh(redis, background_tasks, data)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('schedule thirdpart gold refresh failed')
|
logger.exception('schedule thirdpart gold refresh failed')
|
||||||
return response(code=200 if data else 503, data=data, message='第三方金价刷新暂不可用')
|
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):
|
if len(data) < len(thirdpart_gold.REGIONS):
|
||||||
return response(code=202, data=data,
|
return response(code=202, data=[],
|
||||||
message=f'第三方金价缓存尚未齐全({len(data)}/{len(thirdpart_gold.REGIONS)}跨区),后台刷新中或等待重试')
|
message=f'第三方金价缓存尚未齐全({len(data)}/{len(thirdpart_gold.REGIONS)}跨区),后台刷新中或等待重试')
|
||||||
return response(data=data)
|
return response(data=data)
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ def test_realtime_uses_thirdpart_cache_and_returns_only_defined_fields(monkeypat
|
|||||||
assert isinstance(row['goldPrice'], (int, float))
|
assert isinstance(row['goldPrice'], (int, float))
|
||||||
|
|
||||||
|
|
||||||
def test_realtime_initialization_still_returns_contract_fields(monkeypatch, client):
|
def test_realtime_initialization_returns_empty_array_instead_of_partial_regions(monkeypatch, client):
|
||||||
async def read(redis):
|
async def read(redis):
|
||||||
return [{**cached_item('跨2'), 'stale': False}]
|
return [{**cached_item('跨2'), 'stale': False}]
|
||||||
|
|
||||||
@@ -329,9 +329,8 @@ def test_realtime_initialization_still_returns_contract_fields(monkeypatch, clie
|
|||||||
monkeypatch.setattr(gold, 'schedule_refresh', schedule)
|
monkeypatch.setattr(gold, 'schedule_refresh', schedule)
|
||||||
body = client.get('/api/open/gold/realtime/').json()
|
body = client.get('/api/open/gold/realtime/').json()
|
||||||
assert body['code'] == 202
|
assert body['code'] == 202
|
||||||
assert len(body['data']) == 1
|
assert body['data'] == []
|
||||||
assert set(body['data'][0]) == {'updataTime', 'region', 'code', 'goldPrice'}
|
assert '1/9' in body['message']
|
||||||
assert body['data'][0]['code'] == 'k2'
|
|
||||||
|
|
||||||
|
|
||||||
def test_route_cold_start_and_redis_failure_do_not_block_or_scrape(monkeypatch):
|
def test_route_cold_start_and_redis_failure_do_not_block_or_scrape(monkeypatch):
|
||||||
@@ -363,7 +362,24 @@ def test_route_partial_cache_reports_initialization_progress(monkeypatch):
|
|||||||
result = asyncio.run(colg.get_thirdpart_gold(None, BackgroundTasks()))
|
result = asyncio.run(colg.get_thirdpart_gold(None, BackgroundTasks()))
|
||||||
assert result.code == 202
|
assert result.code == 202
|
||||||
assert '1/9' in result.message
|
assert '1/9' in result.message
|
||||||
assert len(result.data) == 1
|
assert result.data == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('count', 'expected_code'), [(1, 503), (9, 200)])
|
||||||
|
def test_refresh_schedule_failure_serves_only_complete_cache(monkeypatch, count, expected_code):
|
||||||
|
data = [{**cached_item(name), 'stale': False} for name in list(gold.REGIONS.values())[:count]]
|
||||||
|
|
||||||
|
async def read(redis):
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def schedule(*args):
|
||||||
|
raise ConnectionError('Redis unavailable')
|
||||||
|
|
||||||
|
monkeypatch.setattr(gold, 'read_cache', read)
|
||||||
|
monkeypatch.setattr(gold, 'schedule_refresh', schedule)
|
||||||
|
result = asyncio.run(colg.get_thirdpart_gold(None, BackgroundTasks()))
|
||||||
|
assert result.code == expected_code
|
||||||
|
assert result.data == (data if count == 9 else [])
|
||||||
|
|
||||||
|
|
||||||
def test_background_launcher_finishes_before_refresh_completes(monkeypatch):
|
def test_background_launcher_finishes_before_refresh_completes(monkeypatch):
|
||||||
|
|||||||
Reference in New Issue
Block a user