This commit is contained in:
2026-11-04 15:35:15 +08:00
parent 4709ea4328
commit 62291c57ec
12 changed files with 363 additions and 72 deletions
+74 -20
View File
@@ -9,16 +9,17 @@ from fastapi import BackgroundTasks
from api.routes import colg
from api.types.colg import GOLD_REGION_CODES
from api.utils import thirdpart_gold as gold
from api.utils.gold_time import refresh_slot
def offer(price, url='listing', platform=17, money=1):
def offer(price, url='listing', platform=17, money=10):
return {'Price': price, 'Money': money, 'Number': 1, 'PfId': platform, 'BuyUrl': url}
def cached_item(region='跨1', age=0):
fetched_at = time.time() - age
return {'region': region, 'goldPrice': 100.0, 'sampleCount': 10,
'code': GOLD_REGION_CODES[region], 'updataTime': int(fetched_at),
return {'region': region, 'goldPrice': .01, 'sampleCount': 7,
'code': GOLD_REGION_CODES[region], 'updataTime': refresh_slot(fetched_at),
'fetchedAt': fetched_at}
@@ -53,22 +54,28 @@ class FakeRedis:
return 0
def test_average_is_mean_of_ratios_without_minimum_money_filter():
# 100 and 50 万/元 => 75; inverse mean price would incorrectly give 66.67.
assert gold.first_page_average([offer(.01), offer(.02)]) == (75.0, 2)
def test_average_uses_displayed_unit_prices_at_indices_three_through_nine():
rows = [offer(i / 100) for i in range(1, 12)]
assert gold.first_page_average(rows) == (.07, 7)
invalid = [offer(0), offer(-1), offer(float('nan')), offer(float('inf')),
offer(.01, money=-1), offer(None), offer(.01, money=float('nan'))]
assert gold.first_page_average(invalid + [offer(.01, money=.5)]) == (100.0, 1)
# 页面允许Money=-1的库存占位报价;另行排除不显示的低额订单。
invalid.pop(4)
assert gold.first_page_average(invalid + [offer(.01, money=.5)] + rows) == (.07, 7)
assert gold.first_page_average([offer(.016789)] * 10) == (.02, 7)
assert gold.first_page_average(rows[:4]) == (.04, 1)
with pytest.raises(ValueError, match='有效报价'):
gold.first_page_average(invalid)
with pytest.raises(ValueError, match='有效报价'):
gold.first_page_average(rows[:3])
def test_page_deduplicates_before_cutoff_and_preserves_7881_variants():
rows = [offer(.01, 'duplicate', 40), offer(.01, 'duplicate', 40)]
rows += [offer(.02, str(i), 17) for i in range(199)]
rows += [offer(.5, 'outside', 17)]
assert gold.first_page_average(rows) == (50.25, 200)
assert gold.first_page_average([offer(.01, platform=44), offer(.02, platform=44)]) == (75.0, 2)
rows += [offer(i / 100, str(i), 17) for i in range(2, 12)]
assert gold.first_page_average(rows) == (.07, 7)
rows = [offer(.01, platform=44)] * 3 + [offer(.04, platform=44)] * 7
assert gold.first_page_average(rows) == (.04, 7)
def test_request_signature_matches_observed_browser_request():
@@ -118,14 +125,20 @@ def test_region_fetches_its_own_seed_config_and_all_initial_channels():
def post(self, url, json, **kwargs):
posts.append((url, json))
return FakeResponse('{"success":1,"data":[{"Price":0.01,"Money":1,"Number":1}]}')
return FakeResponse('{"success":1,"data":' + json_module.dumps([
{'Price': .01, 'Money': 10, 'Number': 1} for _ in range(10)
]) + '}')
result = asyncio.run(gold.fetch_region(Session(), 'kua8'))
json_module = json
started_at = 1793766590 # 固定开始时间,与响应读取完成的当前时间不同。
result = asyncio.run(gold.fetch_region(Session(), 'kua8', started_at=started_at))
assert result['region'] == '跨8'
assert result['code'] == 'k8'
assert isinstance(result['updataTime'], int)
assert result['goldPrice'] == 100
assert result['sampleCount'] == 1
assert result['goldPrice'] == .01
assert result['sampleCount'] == 7
assert result['updataTime'] == refresh_slot(started_at)
assert result['fetchedAt'] == started_at
assert gets == [gold.BASE_URL + '/bijiaqi/dnf/youxibi/kua8',
gold.BASE_URL + '/cate/1838/CoinSale_yxb_254_0.js?ver=935']
assert posts[0][1]['secret'] == hashlib.md5(b'fresh-seed_17_config-sign').hexdigest()
@@ -148,9 +161,25 @@ def test_cache_omits_missing_regions_marks_old_values_and_retains_order():
assert [row['stale'] for row in rows] == [True, False]
def test_realtime_prefetches_at_25_and_remains_fresh_at_30(monkeypatch):
base = 1793764800 // 1800 * 1800
clock = {'now': base + 25 * 60}
monkeypatch.setattr(gold.time, 'time', lambda: clock['now'])
redis = FakeRedis()
old = cached_item(age=25 * 60)
redis.values[f'{gold.CACHE_PREFIX}:kua1'] = json.dumps(old)
assert asyncio.run(gold.read_cache(redis))[0]['stale'] is True
redis.values[f'{gold.CACHE_PREFIX}:kua1'] = json.dumps(cached_item())
prefetched = asyncio.run(gold.read_cache(redis))[0]
assert prefetched['updataTime'] == base + 30 * 60
assert prefetched['stale'] is False
clock['now'] = base + 30 * 60
assert asyncio.run(gold.read_cache(redis))[0]['stale'] is False
def test_invalid_cached_average_is_not_served():
redis = FakeRedis()
redis.values[f'{gold.CACHE_PREFIX}:kua1'] = json.dumps({**cached_item(), 'goldPrice': 0})
redis.values[f'{gold.CACHE_PREFIX}:kua1'] = json.dumps({**cached_item(), 'goldPrice': -1})
redis.values[f'{gold.CACHE_PREFIX}:kua8'] = json.dumps({**cached_item('跨8'), 'sampleCount': 0})
assert asyncio.run(gold.read_cache(redis)) == []
@@ -164,7 +193,7 @@ def test_legacy_cache_gets_epoch_and_region_code_without_refetching():
rows = asyncio.run(gold.read_cache(redis))
assert len(rows) == 1
assert rows[0]['code'] == 'k3a'
assert rows[0]['updataTime'] == int(legacy['fetchedAt'])
assert rows[0]['updataTime'] == refresh_slot(legacy['fetchedAt'])
assert not redis.writes
@@ -203,7 +232,7 @@ def test_refresh_preserves_failed_region_and_releases_its_lock(monkeypatch, limi
monkeypatch.setattr(gold, 'REGION_INTERVAL', 0)
calls = []
async def fetch(session, region):
async def fetch(session, region, *, started_at=None):
calls.append(region)
if region == 'kua1':
raise gold.UpstreamLimited() if limited else ValueError('channel failed')
@@ -357,7 +386,7 @@ def test_shutdown_cancels_refresh_and_releases_redis_lock(monkeypatch):
async def run():
started = asyncio.Event()
async def fetch(session, region):
async def fetch(session, region, *, started_at=None):
started.set()
await asyncio.Event().wait()
@@ -382,7 +411,7 @@ def test_retry_only_fetches_missing_regions_instead_of_repeating_fresh_pages(mon
monkeypatch.setattr(gold, 'create_redis', lambda: redis)
calls = []
async def fetch(session, region):
async def fetch(session, region, *, started_at=None):
calls.append(region)
return cached_item(gold.REGIONS[region])
@@ -392,6 +421,31 @@ def test_retry_only_fetches_missing_regions_instead_of_repeating_fresh_pages(mon
assert len(asyncio.run(gold.read_cache(redis))) == 9
def test_realtime_regions_share_batch_start_even_when_fetch_crosses_slot_boundary(monkeypatch):
clock = {'now': 1793766590.0}
started_at = clock['now']
redis = FakeRedis()
redis.values[gold.LOCK_KEY] = 'owner'
monkeypatch.setattr(gold, 'create_redis', lambda: redis)
monkeypatch.setattr(gold, 'REGIONS', {'kua1': '跨1', 'kua8': '跨8'})
monkeypatch.setattr(gold, 'REGION_INTERVAL', 0)
monkeypatch.setattr(gold.time, 'time', lambda: clock['now'])
starts = []
async def fetch(session, region, *, started_at):
starts.append(started_at)
clock['now'] += 1800
return {**cached_item(gold.REGIONS[region]), 'fetchedAt': started_at,
'updataTime': refresh_slot(started_at)}
monkeypatch.setattr(gold, 'fetch_region', fetch)
asyncio.run(gold.refresh_cache('owner'))
assert starts == [started_at, started_at]
timestamps = [json.loads(redis.values[f'{gold.CACHE_PREFIX}:{region}'])['updataTime']
for region in gold.REGIONS]
assert timestamps == [refresh_slot(started_at)] * 2
def test_unresponsive_redis_returns_prompt_error_instead_of_hanging(monkeypatch):
async def run():
connections = []