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:
2026-09-12 17:32:24 +08:00
parent 4766e3bd98
commit 1f62aca6f7
+37 -17
View File
@@ -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: