fix(seed): 当日行情重跑必须刷新 source_updated_at(修 503 FUND_QUOTE_UNAVAILABLE)
真机实测到的缺陷:`tools/seed_sim_account_demo.py` 的 `_upsert_market_price()` 在"当天已有行"时直接 `return`,于是同一天重跑种子**不刷新 `source_updated_at`**。 行情是时效数据,过期后 `FundQuoteService` 返回 `503 FUND_QUOTE_UNAVAILABLE:产品 510300 行情已过期`, 连带 `T001` 仪表盘与 `T006` 持仓一起不可用(`T010` 权益不查行情,仍 200)。 表现极具误导性:**刚灌完种子能用,过十几分钟就 503** —— 看起来像行情适配器或缓存故障,实际根因在种子脚本。归因过程: `memory_sync` / Redis / Milvus 全部正常,`fin_market_price` 里当天那行 `source_updated_at` 停在首次灌入时刻。 修法:把列值抽成共享 `values` 字典,当天已有行时 `update` 刷新全部行情列 (含 `source_updated_at`),不存在才 `insert`。 **幂等的正确含义是"不产生重复行"(`(product_id, trade_date)` 唯一), 不是"不更新值"。** 账户/持仓的"已存在则跳过"保持不变 —— 那是业务数据,不该被种子覆盖。 验证(本机,测试客户 9001 `cust_t`): - `python -X utf8 -m tools.seed_sim_account_demo --customer-id 9001` 正常 - `GET /api/v1/users/me/account/dashboard` → 200(修前 503) - `GET /api/v1/users/me/holdings` → 200 - `GET /api/v1/users/me/entitlements` → 200 门禁:ruff `app tests tools alembic` 全过;`mypy app` 0 错(252 文件); `audit_schema.py` 90 张业务表无差异;文档编号/端点编号/RBAC 种子一致性全过。
This commit is contained in:
@@ -26,7 +26,7 @@ import sys
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import func, insert, select
|
||||
from sqlalchemy import func, insert, select, update
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import get_settings
|
||||
@@ -118,7 +118,32 @@ async def _upsert_product(session: Session, spec: dict) -> int:
|
||||
|
||||
|
||||
async def _upsert_market_price(session: Session, product_id: int, spec: dict) -> None:
|
||||
"""写入/刷新当日行情。
|
||||
|
||||
⚠️ 当天已有行时**必须刷新**,不能直接 return(2026-09-12 实测到的缺陷):
|
||||
|
||||
行情是**时效数据**,`FundQuoteService` 会按 `source_updated_at` 判新鲜度,
|
||||
过期即返回 `503 FUND_QUOTE_UNAVAILABLE`,连带 `T001` 仪表盘与 `T006` 持仓一起不可用。
|
||||
原先"已存在就跳过"会让同一天重跑**不更新 `source_updated_at`** ——
|
||||
表现为"刚灌完种子能用,过十几分钟仪表盘就 503",而这与种子无关、极难归因。
|
||||
|
||||
幂等的正确含义是**不产生重复行**(`(product_id, trade_date)` 唯一),
|
||||
**不是"不更新值"**。账户/持仓的"已存在则跳过"是另一回事 ——
|
||||
那是业务数据,不该被种子覆盖。
|
||||
"""
|
||||
today = datetime.now(UTC).date()
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
values = {
|
||||
"open_price": spec["close_price"],
|
||||
"high_price": spec["close_price"] + Decimal("0.05"),
|
||||
"low_price": spec["close_price"] - Decimal("0.05"),
|
||||
"close_price": spec["close_price"],
|
||||
"volume": Decimal("1000000"),
|
||||
"turnover_amount": spec["close_price"] * Decimal("1000000"),
|
||||
"total_fund_shares": spec["total_fund_shares"],
|
||||
"source": "eastmoney_demo_seed",
|
||||
"source_updated_at": now,
|
||||
}
|
||||
existing = (
|
||||
await session.execute(
|
||||
select(FundMarketPrice.id).where(
|
||||
@@ -128,25 +153,20 @@ async def _upsert_market_price(session: Session, product_id: int, spec: dict) ->
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if existing is not None:
|
||||
await session.execute(
|
||||
update(FundMarketPrice).where(FundMarketPrice.id == existing).values(**values)
|
||||
)
|
||||
return
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
next_id = await _next_id(session, FundMarketPrice)
|
||||
stmt = insert(FundMarketPrice).values(
|
||||
id=next_id,
|
||||
product_id=product_id,
|
||||
trade_date=today,
|
||||
open_price=spec["close_price"],
|
||||
high_price=spec["close_price"] + Decimal("0.05"),
|
||||
low_price=spec["close_price"] - Decimal("0.05"),
|
||||
close_price=spec["close_price"],
|
||||
volume=Decimal("1000000"),
|
||||
turnover_amount=spec["close_price"] * Decimal("1000000"),
|
||||
total_fund_shares=spec["total_fund_shares"],
|
||||
source="eastmoney_demo_seed",
|
||||
source_updated_at=now,
|
||||
created_at=now,
|
||||
await session.execute(
|
||||
insert(FundMarketPrice).values(
|
||||
id=next_id,
|
||||
product_id=product_id,
|
||||
trade_date=today,
|
||||
created_at=now,
|
||||
**values,
|
||||
)
|
||||
)
|
||||
await session.execute(stmt)
|
||||
|
||||
|
||||
async def _upsert_account(session: Session, customer_id: int) -> FundSimAccount:
|
||||
|
||||
Reference in New Issue
Block a user