This commit is contained in:
2026-11-04 16:21:18 +08:00
parent cc66e98f61
commit c2775b732e
3 changed files with 28 additions and 9 deletions
+3
View File
@@ -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
+4 -4
View File
@@ -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)
+21 -5
View File
@@ -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):