Skip to content

refactor: session_lifecycle_payload 自查最近活动,删除 5 处重复调用 - #67

Merged
XiaoCow666 merged 2 commits into
XiaoCow666:mainfrom
linxi123-A:refactor/payload-self-fetch-activity
Sep 22, 2026
Merged

XiaoCow666 merged 2 commits into
XiaoCow666:mainfrom
linxi123-A:refactor/payload-self-fetch-activity

Conversation

@linxi123-A

Copy link
Copy Markdown
Contributor

维护痛点

session_lifecycle_payload 需要last_activity_at 才能正确计算会话的 idle/active 状态和activity_age_seconds 。但这个参数的获取方式是一个 隐含假设 :每个单 session 调用者都必须手写

Python

1

last_activity_at=latest_session_activity([session.id]).get(session.id)

在routes/thinking.py 中这行重复了 5 次。问题是:

  1. 重复 :同样的查询+取值逻辑散落 5 处,改一次要改 5 个地方。
  2. 隐含假设 :如果调用者忘了传,payload 会退化成用started_at 当活动时间——一个刚提交过答案的会话,因为started_at 很早,会被误判为idle 。这是静默 bug,没有任何报错。
  3. 批量场景是另一套写法 :activity_by_session = latest_session_activity([...]) 然后循环传last_activity_at=activity_by_session.get(id) ,目的是避免 N+1。单 session 和批量场景的写法不一致,维护时需要分辨。

改动

services/session_lifecycle.py :session_lifecycle_payload 在last_activity_at 为None 时,基于session.id 自查一次最近活动时间:

Python

1
2
3
4

if last_activity_at is None: session_id = getattr(session, "id", None) if session_id is not None: last_activity_at = latest_session_activity([session_id]).get(session_id)

routes/thinking.py :删除 5 处单 session 调用中的last_activity_at=... 参数,调用简化为session_lifecycle_payload(session) 。

改动边界 :

  • 4 处批量场景(thinking.py 教师概览 ×2、main.py 首页、action_center.py ) 保持不变 ——它们先批量查出activity_by_session 再循环传入,避免 N+1,这是有意的性能优化。
  • session_lifecycle_payload 的返回结构和所有字段不变。
  • 显式传last_activity_at 时行为完全不变(不触发自查)。

行为保持情况

场景 改前 改后 单 session,不传last_activity_at 用started_at 判 idle(可能误判) 自查最近活动,正确判断 单 session,传last_activity_at 用传入值 用传入值(不变) 批量场景,传last_activity_at 用传入值 用传入值(不变)

唯一的行为变化是"不传"的单 session 场景从"错误地用 started_at"变为"正确地自查"——这正是修复隐含假设。

测试结果

新增 2 个测试(tests/test_session_lifecycle.py ):

  • test_payload_self_fetches_latest_activity_when_omitted :不传last_activity_at 时,payload 用session.id 自查,返回的last_activity_at 和activity_age_seconds 基于日志时间。
  • test_payload_keeps_provided_activity_without_extra_query :显式传last_activity_at 时不触发自查(保护批量场景的 N+1 约束)。
    Bash

运行

1

pytest tests/test_session_lifecycle.py tests/test_session_lifecycle_routes.py tests/test_demo_guided_learning.py -q

结果: 30 passed (覆盖生命周期投影、路由、引导学习全流程)。

回退

单提交,改动集中在session_lifecycle_payload 一个函数和 5 处调用点,revert 即可。

@XiaoCow666 XiaoCow666 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeSense 自动评审

当前事件未提供可审查的代码 diff,无法判断该改动是否引入阻塞性回归,因此不能批准。请先提供完整且可访问的变更内容后再复审。

需要先处理的问题

  • GitHub 返回 503,PR diff 不可用;缺少实际代码变更,无法核验自查逻辑、5 处调用删除及测试覆盖是否正确。

请按下面的步骤修改

  • 位置:PR #67 的完整 diff;现在:事件中仅有 PR 描述和不可用的 diff,无法审查代码实现;改成:提供可访问的完整 patch 或逐文件变更内容,包含 services/session_lifecycle.py、routes/thinking.py 及测试文件;交给 AI:请重新获取并提交完整 diff,确认所有变更可审查后运行 PR 描述中的相关 pytest 命令并提供实际检查输出。

已有验证信息

  • PR 描述声称运行了 3 个测试文件并得到 30 passed,但事件未提供可独立核验的 CI 或命令输出。

评审事件:669d8140-b63b-11f1-89ca-8dafac991334

@linxi123-A

Copy link
Copy Markdown
Contributor Author

完整 diff(3 个文件,git diff origin/main...HEAD ):

diff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

`diff --git a/routes/thinking.py b/routes/thinking.py
@@ -472,10 +472,7 @@
payload['forum_state'] = _stage3_forum_state(ts.id)

  • payload['session_lifecycle'] = session_lifecycle_payload(
  •    ts,
    
  •    last_activity_at=latest_session_activity([ts.id]).get(ts.id),
    
  • )
  • payload['session_lifecycle'] = session_lifecycle_payload(ts)
    return ts, target_role, payload

@@ -665,10 +662,7 @@
result['user_goal'] = ...

  • result['session_lifecycle'] = session_lifecycle_payload(
  •    thinking_session,
    
  •    last_activity_at=latest_session_activity([thinking_session.id]).get(thinking_session.id),
    
  • )
  • result['session_lifecycle'] = session_lifecycle_payload(thinking_session)
    return result

@@ -979,10 +973,7 @@
if existing:

  •        lifecycle = session_lifecycle_payload(
    
  •            existing,
    
  •            last_activity_at=latest_session_activity([existing.id]).get(existing.id),
    
  •        )
    
  •        lifecycle = session_lifecycle_payload(existing)
    

@@ -1111,10 +1102,7 @@
_log_event(new_session.id, 1, 'session_start', 'student', '开始引导式学习')

  •    lifecycle = session_lifecycle_payload(
    
  •        new_session,
    
  •        last_activity_at=new_session.started_at,
    
  •    )
    
  •    lifecycle = session_lifecycle_payload(new_session)
    

@@ -2348,10 +2336,7 @@
return jsonify({'error': '会话不存在或无权访问'}), 403

  • lifecycle = session_lifecycle_payload(
  •    thinking_session,
    
  •    last_activity_at=latest_session_activity([thinking_session.id]).get(thinking_session.id),
    
  • )
  • lifecycle = session_lifecycle_payload(thinking_session)
    return jsonify({'success': True, 'session': lifecycle})`

diff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

`diff --git a/services/session_lifecycle.py b/services/session_lifecycle.py
@@ -187,8 +187,19 @@ def _stage_progress(session, lifecycle_status):

def session_lifecycle_payload(session, *, now=None, last_activity_at=None):

  • """Build a safe, content-free lifecycle payload for UI and teacher views."""
  • """...(新增文档:单 session 可不传 last_activity_at,内部自查)"""
    current_time = _utc_now(now)
  • if last_activity_at is None:
  •    session_id = getattr(session, "id", None)
    
  •    if session_id is not None:
    
  •        last_activity_at = latest_session_activity([session_id]).get(session_id)
    
    lifecycle_status = session_lifecycle_status(
    session,
    now=current_time,
    last_activity_at=last_activity_at,
    )`

diff

1
2
3
4
5
6
7
8
9
10
11
12

`diff --git a/tests/test_session_lifecycle.py b/tests/test_session_lifecycle.py
@@ -197,3 +197,62 @@
assert can_view_session(other_teacher, session) is False
+
+
+def test_payload_self_fetches_latest_activity_when_omitted(monkeypatch):

  • 不传 last_activity_at 时,payload 用 session.id 自查一次

  • 断言 calls == [[7]],last_activity_at 取日志时间,activity_age ≈ 120s

+def test_payload_keeps_provided_activity_without_extra_query(monkeypatch):

  • 显式传 last_activity_at 时不触发自查(保护批量场景 N+1 约束)

  • 断言 calls == []`

实际测试输出(命令与摘要):

1

pytest tests/test_session_lifecycle.py tests/test_session_lifecycle_routes.py tests/test_demo_guided_learning.py -v

1
2
3
4
5

tests/test_session_lifecycle.py::test_payload_self_fetches_latest_activity_when_omitted PASSED tests/test_session_lifecycle.py::test_payload_keeps_provided_activity_without_extra_query PASSED tests/test_session_lifecycle_routes.py::test_start_session_resume_returns_the_same_lifecycle_projection PASSED ... (共 30 项) ================= 30 passed, 165 warnings in 70.30s (0:01:10) =================

全部 30 项通过,0 failed。两个新增测试分别验证"不传则自查"和"传了则不查",覆盖自查逻辑和 N+1 保护边界。

@XiaoCow666 XiaoCow666 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论:需要修改。当前实现会在批量调用场景中引入 N+1 查询,和 PR 声明的性能边界不一致。

  1. 高优先级:区分“省略参数”和“显式传入 None”

请打开 services/session_lifecycle.py 的 session_lifecycle_payload。当前 if last_activity_at is None: 会把两种情况合并处理:单会话调用者省略参数,以及批量调用者通过 activity_by_session.get(session.id) 显式传入 None。当某个会话没有活动记录时,批量调用仍会再次执行 latest_session_activity([session_id]),教师概览等列表会因此产生额外查询。

请使用独立 sentinel 作为 last_activity_at 的默认值,仅在调用者省略该参数时查询最近活动;调用者显式传入 None 时,应保留现有回退到 started_at 的行为且不执行查询。

请同时打开 tests/test_session_lifecycle.py,增加一个覆盖显式传入 last_activity_at=None 的测试,验证 latest_session_activity 没有被调用,并验证 payload 按现有规则使用 started_at。现有 test_payload_keeps_provided_activity_without_extra_query 只覆盖非空时间,无法发现该问题。

修改后运行:

pytest tests/test_session_lifecycle.py tests/test_session_lifecycle_routes.py tests/test_demo_guided_learning.py -q

已检查范围:routes/thinking.py 删除的 5 处调用、services/session_lifecycle.py 的默认查询逻辑,以及新增的两个生命周期测试。权限检查和返回字段在 diff 中没有发生变化。

@XiaoCow666 XiaoCow666 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeSense 自动评审

核心目标已完成:省略 last_activity_at 时会自行查询最近活动,显式传值(包括 None)仍避免额外查询,批量调用的 N+1 约束得到保留。diff 中未见安全、数据损坏、明显回归或无法运行的阻塞问题。

已有验证信息

  • PR 描述报告运行 pytest tests/test_session_lifecycle.py tests/test_session_lifecycle_routes.py tests/test_demo_guided_learning.py -q,结果为 30 passed。
  • diff 新增测试覆盖了省略参数时的自查、显式活动时间时不查询,以及显式 None 时保留回退且不触发查询。

评审事件:4578bdd0-b691-11f1-9229-9041db19ed4e

@XiaoCow666
XiaoCow666 merged commit 9da0a18 into XiaoCow666:main Sep 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants