diff --git a/DEVLOG.md b/DEVLOG.md index c49eb5f..60478fb 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1900,3 +1900,12 @@ PMS 功能仍 a56dac5,本轮未重建 PMS 容器。07:27 三票历史修复数 五分钟任务已于 07:35 正常调度且回调成功。未采纳提议或制造成交。真实消费、提议重核案例及自然新成交仍待判收。 PMS 页面需要重新登录;基座页面版本与研究字段已实机核对。automation-2 下一次为今日 08:55,核对正常消费及区制修订。 详见四仓同步《交接_每日选股计划完善_2026-10-15.md》最后一节。 + + +## 2026-10-16 · 计划版本页面比对兼容修复 + +浏览器验收发现,新版收到记录已使用 decision 与查询范围指纹,变化与预览仍以旧名册指纹比较。 +因此落库成功也被误报为未落快照。写入和两个比对出口统一使用收到计划的身份。 +旧数据继续按旧名册口径,不改变消费记录、排序或提议行为。页面删去实时重装配及永远按分数排序的过期说明。 +新增离线往返回归:相同版本与范围通过,新版本或范围变更不误认;落新记录后恢复一致。 +部署与实机判收另记到四仓同步交接。用户原有未提交台账内容保留。 diff --git a/app/services/plan_feed.py b/app/services/plan_feed.py index 7f9cec9..2503322 100644 --- a/app/services/plan_feed.py +++ b/app/services/plan_feed.py @@ -929,6 +929,15 @@ def _sync_themes_quiet(plan: dict) -> dict: # ================================================================ 名册快照与榜单变化 +def _received_digest(plan: dict, roster=None) -> str: + """写入与页面比对共用收到计划的身份,旧数据继续使用名册指纹。""" + if plan.get('decision'): + from app.core.plan_revision import fingerprint + return fingerprint({'received': plan['decision'], 'requested': plan.get('requested')})[:40] + from app.core import plan_diff as pdf + return pdf.digest_of(pdf.roster_of(plan) if roster is None else roster) + + def snapshot(plan: dict, *, keep: int = 200, rank_mode=None) -> dict: """把这一份计划的名册落进 pms_plan_snapshot。 @@ -938,10 +947,7 @@ def snapshot(plan: dict, *, keep: int = 200, rank_mode=None) -> dict: from app.core import plan_diff as pdf from app.repo import pms_repo roster = pdf.roster_of(plan) - digest = pdf.digest_of(roster) - if plan.get('decision'): - from app.core.plan_revision import fingerprint - digest = fingerprint({'received': plan['decision'], 'requested': plan.get('requested')})[:40] + digest = _received_digest(plan, roster) meta = {k: plan.get(k) for k in ("counts", "returned", "requested", "funnel", "theme_cap", "heat_date", "market_snapshot_days", "age_tdays", "url", @@ -1048,7 +1054,7 @@ def preview_changes(plan: dict, *, held=None, rank_jump=None, tail_guard=None) - curr_roster = pdf.roster_of(plan) out["stored"] = ({k: prev.get(k) for k in ("id", "plan_date", "digest", "fetched_at")} if prev else None) - out["same_as_stored"] = bool(prev and prev["digest"] == pdf.digest_of(curr_roster)) + out["same_as_stored"] = bool(prev and prev["digest"] == _received_digest(plan, curr_roster)) try: tr = plan.get("truncated") or {} out["diff"] = _diff_against(prev, curr_roster, curr_date=plan.get("date"), @@ -1084,7 +1090,7 @@ def changes(*, held=None, rank_jump=None, tail_guard=None) -> dict: curr_digest = None try: # 先确保手上这份已落库 (缓存命中时是个空操作) plan = get_plan() - curr_digest = pdf.digest_of(pdf.roster_of(plan)) + curr_digest = _received_digest(plan) except PlanFeedError as e: out["hint"] = f"上游计划取不到, 只能拿库里的历史快照比: {e}" except Exception as e: diff --git a/app/web/static/index.html b/app/web/static/index.html index d18f048..f61d296 100644 --- a/app/web/static/index.html +++ b/app/web/static/index.html @@ -1991,7 +1991,9 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;} - + @@ -2418,7 +2420,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
- 正在读上游选股计划…… 上游装配这份计划要读论断与催化事件,慢的时候要几十秒。 + 正在读上游选股计划…… 正在读取已发布的完整版本。 这段时间抽屉是空的,属正常;真读不到会在这里显示原因。
@@ -2458,8 +2460,8 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;} + :title="'同一计划日 ' + chg.curr_date + ' 的完整修订版本:' + + '此前收到的计划与当前版本不同,请查看版本及研究变化'">
「预期空间」= 券商目标价相对现价的空间 (上游 upside 字段, 2.12 → +212%)。噪音大, - 所以永不参与排序 —— 排序始终按 score; 只在 PMS_PLAN_MIN_UPSIDE 上做下限过滤。 + 所以永不参与排序 —— 排序按当前有效模式与稳定次级键; 只在 PMS_PLAN_MIN_UPSIDE 上做下限过滤。 筛选参数在「参数设置」里: PMS_PLAN_TOP_N / PMS_PLAN_TIERS / PMS_PLAN_INCLUDE_OBSERVE / PMS_PLAN_MIN_SCORE / PMS_PLAN_MIN_SOURCES / PMS_PLAN_MIN_UPSIDE / PMS_PLAN_STALE_TDAYS。
发给上游的三个: PMS_PLAN_TOP / PMS_PLAN_OBS_TOP / PMS_PLAN_THEME_CAP (0=不传, 用上游默认 @@ -4453,7 +4455,7 @@ createApp({ const src = p.plan_source === 'snapshot' ? '快照' : (p.plan_source === 'live' ? '现算' : (p.plan_source || '')); const gate = p.gate_on === true ? '赛道闸开' : (p.gate_on === false ? '赛道闸关' : ''); const head = ['选股计划数据至 ' + (p.date || '-') + '(数据日)', planFetchedText.value, src, gate].filter(Boolean).join(' · '); - return head + '。日期是数据日:上游每个交易日早上 07:10 用上一交易日收盘后的证据出计划,持仓系统 08:40 拉取,所以正常显示的是上一交易日。计划来源:快照=盘前落盘那份,毫秒返回、可复现;现算=盘中实时装配,较慢,多半是快照过旧回落。赛道闸=上游是否只在十条赛道内选票。'; + return head + '。日期是数据日:上游每个交易日早上 07:10 用上一交易日收盘后的证据出计划,持仓系统 08:40 拉取,所以正常显示的是上一交易日。计划来源:快照=已发布的完整版本,包含晨版及盘中修订;旧接口的现算来源不具备同等追溯能力。赛道闸=上游是否只在十条赛道内选票。'; }); const hdMenuLabel = computed(() => { const who = authed.value ? (me.value.username || me.value.phone || '') : ''; diff --git a/scripts/test_batch8_units.py b/scripts/test_batch8_units.py index f7a4d4f..af890fe 100644 --- a/scripts/test_batch8_units.py +++ b/scripts/test_batch8_units.py @@ -632,6 +632,33 @@ def t_j4(): restore() +@case("[J4b] 新版收到身份写读一致,查询范围变化不能误认同版") +def t_j4b(): + from app.services import plan_feed as pf + from unittest.mock import patch + store, restore = _with_fake_repo({"PMS_PLAN_SNAPSHOT": True}) + try: + p = plan(main=[_r("600418.SH", 1, 242.24)], date="2026-07-30") + p.update(decision={'id': 'decision-a', 'input_hash': 'a'}, requested={'top': 300}) + pf.snapshot(p) + before = len(store['rows']) + assert pf.preview_changes(p)['same_as_stored'] is True + with patch.object(pf, 'get_plan', return_value=p): + assert pf.changes()['in_sync'] is True + assert len(store['rows']) == before + q = dict(p, requested={'top': 100}) + assert pf.preview_changes(q)['same_as_stored'] is False + q = dict(p, decision={'id': 'decision-b', 'input_hash': 'b'}) + with patch.object(pf, 'get_plan', return_value=q): + assert pf.changes()['in_sync'] is False + assert pf.snapshot(q)['stored'] is True + with patch.object(pf, 'get_plan', return_value=q): + assert pf.changes()['in_sync'] is True + assert pf.preview_changes(q)['same_as_stored'] is True + finally: + restore() + + @case("[J5] 只读预览能比出手上这份与库里那份的差异") def t_j5(): from app.services import plan_feed as pf