群排行功能 - #6
Conversation
Reviewer's Guide新增“今天玩什么”随机游戏推荐图片功能和群游玩时长排行榜功能,这些功能基于新的 SteamPlayRecord 表实现,并与轮询流水线和管理后台完成集成,同时更新 README 和帮助文档。 新增群游玩时长排行榜流程的时序图sequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
新增随机游戏推荐图片流程的时序图sequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your Experience访问你的 dashboard 来:
Getting HelpOriginal review guide in EnglishReviewer's GuideAdds a "今天玩什么" random game recommendation image feature and a group playtime ranking feature backed by a new SteamPlayRecord table and integrated with the polling pipeline and admin console, plus README/help updates. Sequence diagram for the new group playtime ranking flowsequenceDiagram
actor User
participant Bot
participant ranking_sv
participant ranking_service
participant SteamBind
participant SteamPlayRecord
User->>Bot: "steam群排行" / "steam群排名"
Bot->>ranking_sv: group_ranking(ev)
ranking_sv->>ranking_service: get_group_ranking_list(ev.group_id)
ranking_service->>SteamBind: get_binds_by_group(group_id)
SteamBind-->>ranking_service: binds
ranking_service->>SteamPlayRecord: get_records_by_steamids(steamid64s)
SteamPlayRecord-->>ranking_service: records
ranking_service-->>ranking_sv: ranking_list
ranking_sv->>Bot: send(text)
Bot-->>User: 群游戏时长排行榜文本
Sequence diagram for the new random game recommendation image flowsequenceDiagram
actor User
participant Bot
participant library_SV
participant library_service
participant SteamAPI
participant draw_what_to_play
User->>Bot: "steam玩什么"
Bot->>library_SV: get_my_steamlibrary_image(ev)
library_SV->>library_service: build_random_pick(steamid64)
library_service->>SteamConfig: get_config("SteamWebAPIKey")
library_service->>get_steamlibrary_by_steamid64: (api_key, steamid64)
get_steamlibrary_by_steamid64-->>library_service: library.games
library_service->>SteamAPI: GetGameCoverImageURL(appid, variant)
SteamAPI-->>library_service: cover_url
library_service->>draw_what_to_play: draw_what_to_play(picks)
draw_what_to_play-->>library_service: Image
library_service-->>library_SV: img_bytes
library_SV->>Bot: send(MessageSegment.image(img_bytes))
Bot-->>User: 推荐图片
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Warning Review limit reached
Next review available in:41 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. 📝 WalkthroughWalkthrough新增“玩什么”随机推荐命令、游戏游玩记录存储与群游玩时长排行榜,并更新帮助菜单、README、图片渲染及后台管理配置。 ChangesSteam 功能扩展
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant LibraryCommand
participant LibraryService
participant ImageRenderer
User->>LibraryCommand: 玩什么
LibraryCommand->>LibraryService: resolve steamid64
LibraryService->>ImageRenderer: selected games
ImageRenderer-->>LibraryService: recommendation image
LibraryService-->>LibraryCommand: JPEG bytes
LibraryCommand-->>User: image message
sequenceDiagram
participant Poller
participant RecordService
participant PlayRecord
participant RankingCommand
Poller->>RecordService: changed game status
RecordService->>PlayRecord: write start/end timestamps
RankingCommand->>PlayRecord: query ended records
PlayRecord-->>RankingCommand: aggregate ranking data
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Hey - 我发现了 3 个问题,并给出了一些整体性反馈:
- 在
build_random_pick中,game_data未定义,同时循环末尾存在多余的}) }),这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如picks_data),并移除多余的闭合括号。 - 同样在
build_random_pick中,pic_quality在当前作用域中被引用但从未定义,而在build_library_wall中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现NameError。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- 在 `build_random_pick` 中,`game_data` 未定义,同时循环末尾存在多余的 `}) })`,这会导致运行时错误;请在函数内初始化并使用一个本地列表(例如 `picks_data`),并移除多余的闭合括号。
- 同样在 `build_random_pick` 中,`pic_quality` 在当前作用域中被引用但从未定义,而在 `build_library_wall` 中它是从配置中获取的;你应该以类似的方式获取或传递质量设置,以避免出现 `NameError`。
## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick 包含语法问题和未定义变量,并会在运行时失败
在 `build_random_pick` 中,`game_data` 被追加但从未定义,结尾的 `}) })` 会导致语法错误,同时 `pic_quality` 在该函数中使用时没有被加载(与 `build_library_wall` 不同)。请在使用前在本地定义 `game_data`,移除重复的闭合语法,并在这里加载 `pic_quality`(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算
Docstring 中说明在开始游戏时 `start_ts` 是必填的,但 `upsert_record` 在 `end_ts` 为 None 时可以插入 `start_ts=None` 的记录。之后,`get_group_ranking_list` 会执行 `record.end_ts - record.start_ts`,如果两者之一为 None 就会抛出异常。
请在 `end_ts` 为 None 时强制 `start_ts` 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 `end_ts < start_ts` 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. 如果 `upsert_record` 属于对外公开的 API 并会将错误返回给调用方,你可能希望将 `ValueError` 替换为领域特定的异常类型(例如 `InvalidPlayRecordError`),或者转换为你现有的错误/响应模式。
2. 确保任何此前依赖插入 `start_ts=None` 且 `end_ts=None` 的调用方都被更新为始终提供合法的 `start_ts`,或者在能够提供时再调用 `upsert_record`。
3. 如果 `upsert_record` 内部存在基于其他信息推导 `start_ts`/`end_ts` 的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃
在 `get_group_ranking_list` 中,`duration = record.end_ts - record.start_ts` 如果 `start_ts` 为 None 就会抛出异常。`get_records_by_steamids` 只过滤了 `end_ts`,因此不合法的记录会导致这个循环崩溃。
请在计算 `duration` 之前处理缺失或不一致的时间戳,例如:
- 跳过 `start_ts` 或 `end_ts` 为 None,或者 `end_ts < start_ts` 的记录;
- 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
这样可以在存在坏数据时保持排名功能的健壮性。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进评审质量。
Original comment in English
Hey - I've found 3 issues, and left some high level feedback:
- In
build_random_pick,game_datais not defined and there is an extra}) })at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g.picks_data) and clean up the stray closing braces. - Also in
build_random_pick,pic_qualityis referenced but never defined in this scope, unlikebuild_library_wallwhere it is obtained from config; you should retrieve or pass the quality setting similarly to avoid aNameError.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `build_random_pick`, `game_data` is not defined and there is an extra `}) })` at the end of the loop, which will cause runtime errors; please initialize and use a local list (e.g. `picks_data`) and clean up the stray closing braces.
- Also in `build_random_pick`, `pic_quality` is referenced but never defined in this scope, unlike `build_library_wall` where it is obtained from config; you should retrieve or pass the quality setting similarly to avoid a `NameError`.
## Individual Comments### Comment 1
<locationpath="SteamUID/SteamLibarary/library_service.py"line_range="57-66" />
<code_context>
+async def build_random_pick(steamid64: str) -> bytes:
</code_context>
<issue_to_address>
**issue (bug_risk):** build_random_pick contains syntax issues, undefined variables, and will fail at runtime
In `build_random_pick`, `game_data` is appended to but never defined, the trailing `}) })` will cause a syntax error, and `pic_quality` is used without being loaded in this function (unlike `build_library_wall`). Please define `game_data` locally before use, remove the duplicate closing syntax, and load `pic_quality` here (or via a shared helper) so this path doesn’t throw before generating the image.
</issue_to_address>
### Comment 2
<locationpath="SteamUID/utils/database/models.py"line_range="608-617" />
<code_context>
+ async def upsert_record(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations
The docstring says `start_ts` is required when starting a game, but `upsert_record` can insert rows with `start_ts=None` when `end_ts` is None. Later, `get_group_ranking_list` does `record.end_ts - record.start_ts`, which will raise if either is None.
Please enforce that `start_ts` is non-None when `end_ts` is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where `end_ts < start_ts` so all stored play records remain well-formed and ranking stays robust.
Suggested implementation:
```python@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) -> int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。"""# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全if end_ts isNone:
# 开始一段新的游玩记录,必须有 start_tsif start_ts isNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入if start_ts isnotNoneand end_ts < start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)
```1. If `upsert_record` is part of a public API that surfaces errors to callers, you may want to replace `ValueError` with a domain-specific exception type (e.g. `InvalidPlayRecordError`) or convert it to your existing error/response pattern.
2. Ensure any callers that relied on inserting `start_ts=None` with `end_ts=None` are updated to always supply a valid `start_ts`, or to avoid calling `upsert_record` until they can.
3. If there is logic inside `upsert_record` that derives `start_ts`/`end_ts` from other sources, verify that the new validation runs before any database writes and does not conflict with that logic.
</issue_to_address>
### Comment 3
<locationpath="SteamUID/SteamRanking/ranking_service.py"line_range="47-51" />
<code_context>
++# 第3步:计算时长并按 user_id 累加+ user_durations: dict[str, int] = {}
+ for record in records:
+ uid = steamid_to_user.get(record.steamid64)+ if uid is None:+ continue+ duration = record.end_ts - record.start_ts # type: ignore+ user_durations[uid] = user_durations.get(uid, 0) + duration+
</code_context>
<issue_to_address>
**issue (bug_risk):** Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash
In `get_group_ranking_list`, `duration = record.end_ts - record.start_ts` will raise if `start_ts` is None. `get_records_by_steamids` only filters `end_ts`, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing `duration`, e.g.:
- Skip records where `start_ts` or `end_ts` is None, or `end_ts < start_ts`.
- Optionally log or count these cases to detect upstream data issues.
This keeps the ranking robust when bad data is present.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| async def build_random_pick(steamid64: str) -> bytes: | ||
| """从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。""" | ||
| api_key = SteamConfig.get_config("SteamWebAPIKey").data | ||
| if not api_key: | ||
| raise SteamConfigError("请先配置 steam web api key") | ||
| library = await get_steamlibrary_by_steamid64(api_key, steamid64) | ||
| games = library.get("games") | ||
| if games is None: | ||
| raise SteamValidationError("获取 steam 游戏库列表失败") |
There was a problem hiding this comment.
issue (bug_risk): build_random_pick 包含语法问题和未定义变量,并会在运行时失败
在 build_random_pick 中,game_data 被追加但从未定义,结尾的 }) }) 会导致语法错误,同时 pic_quality 在该函数中使用时没有被加载(与 build_library_wall 不同)。请在使用前在本地定义 game_data,移除重复的闭合语法,并在这里加载 pic_quality(或通过共享的辅助函数),以避免在生成图片之前就抛出异常。
Original comment in English
issue (bug_risk): build_random_pick contains syntax issues, undefined variables, and will fail at runtime
In build_random_pick, game_data is appended to but never defined, the trailing }) }) will cause a syntax error, and pic_quality is used without being loaded in this function (unlike build_library_wall). Please define game_data locally before use, remove the duplicate closing syntax, and load pic_quality here (or via a shared helper) so this path doesn’t throw before generating the image.
| async def upsert_record( | ||
| cls: Type[T_SteamPlayRecord], | ||
| session: AsyncSession, | ||
| steamid64: str, | ||
| appid: str, | ||
| start_ts: Optional[int] = None, | ||
| end_ts: Optional[int] = None, | ||
| ) -> int: | ||
| """ | ||
| 写入游玩记录(upsert)。 |
There was a problem hiding this comment.
suggestion (bug_risk): SteamPlayRecord.upsert_record 允许 start_ts 为 None,这会破坏后续时长计算
Docstring 中说明在开始游戏时 start_ts 是必填的,但 upsert_record 在 end_ts 为 None 时可以插入 start_ts=None 的记录。之后,get_group_ranking_list 会执行 record.end_ts - record.start_ts,如果两者之一为 None 就会抛出异常。
请在 end_ts 为 None 时强制 start_ts 必须非 None(例如,直接让 upsert 失败,而不是插入无效记录),并考虑拒绝或规范化 end_ts < start_ts 的情况,以确保所有存储的游玩记录都是有效的、排名计算更加稳定可靠。
Suggested implementation:
@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)- 如果
upsert_record属于对外公开的 API 并会将错误返回给调用方,你可能希望将ValueError替换为领域特定的异常类型(例如InvalidPlayRecordError),或者转换为你现有的错误/响应模式。 - 确保任何此前依赖插入
start_ts=None且end_ts=None的调用方都被更新为始终提供合法的start_ts,或者在能够提供时再调用upsert_record。 - 如果
upsert_record内部存在基于其他信息推导start_ts/end_ts的逻辑,请确认新的校验在任何数据库写入之前执行,并且不会与这些逻辑产生冲突。
Original comment in English
suggestion (bug_risk): SteamPlayRecord.upsert_record allows start_ts to be None, which can break downstream duration calculations
The docstring says start_ts is required when starting a game, but upsert_record can insert rows with start_ts=None when end_ts is None. Later, get_group_ranking_list does record.end_ts - record.start_ts, which will raise if either is None.
Please enforce that start_ts is non-None when end_ts is None (e.g. fail the upsert instead of inserting an invalid record), and consider rejecting or normalizing cases where end_ts < start_ts so all stored play records remain well-formed and ranking stays robust.
Suggested implementation:
@classmethod@with_sessionasyncdefupsert_record(
cls: Type[T_SteamPlayRecord],
session: AsyncSession,
steamid64: str,
appid: str,
start_ts: Optional[int] =None,
end_ts: Optional[int] =None,
) ->int:
""" 写入游玩记录(upsert)。 - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填(否则抛出异常)。 - 当同时提供 start_ts 和 end_ts 时,必须满足 end_ts >= start_ts,否则抛出异常。 """# 数据有效性检查:避免插入无效的游玩记录,保证后续时长计算安全ifend_tsisNone:
# 开始一段新的游玩记录,必须有 start_tsifstart_tsisNone:
raiseValueError("SteamPlayRecord.upsert_record: start_ts is required when end_ts is None")
else:
# 结束时间早于开始时间的记录属于异常数据,拒绝写入ifstart_tsisnotNoneandend_ts<start_ts:
raiseValueError(
f"SteamPlayRecord.upsert_record: end_ts ({end_ts}) "f"cannot be earlier than start_ts ({start_ts})"
)- If
upsert_recordis part of a public API that surfaces errors to callers, you may want to replaceValueErrorwith a domain-specific exception type (e.g.InvalidPlayRecordError) or convert it to your existing error/response pattern. - Ensure any callers that relied on inserting
start_ts=Nonewithend_ts=Noneare updated to always supply a validstart_ts, or to avoid callingupsert_recorduntil they can. - If there is logic inside
upsert_recordthat derivesstart_ts/end_tsfrom other sources, verify that the new validation runs before any database writes and does not conflict with that logic.
| for record in records: | ||
| uid = steamid_to_user.get(record.steamid64) | ||
| if uid is None: | ||
| continue | ||
| duration = record.end_ts - record.start_ts # type: ignore |
There was a problem hiding this comment.
issue (bug_risk): 群组排名中的时长汇总假定时间戳非 None,可能表现异常或崩溃
在 get_group_ranking_list 中,duration = record.end_ts - record.start_ts 如果 start_ts 为 None 就会抛出异常。get_records_by_steamids 只过滤了 end_ts,因此不合法的记录会导致这个循环崩溃。
请在计算 duration 之前处理缺失或不一致的时间戳,例如:
- 跳过
start_ts或end_ts为 None,或者end_ts < start_ts的记录; - 可选地对这些情况进行日志记录或计数,以便发现上游数据问题。
这样可以在存在坏数据时保持排名功能的健壮性。
Original comment in English
issue (bug_risk): Duration aggregation in group ranking assumes non-None timestamps and can misbehave or crash
In get_group_ranking_list, duration = record.end_ts - record.start_ts will raise if start_ts is None. get_records_by_steamids only filters end_ts, so malformed rows can crash this loop.
Please handle missing or inconsistent timestamps before computing duration, e.g.:
- Skip records where
start_tsorend_tsis None, orend_ts < start_ts. - Optionally log or count these cases to detect upstream data issues.
This keeps the ranking robust when bad data is present.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
SteamUID/SteamPoll/poll_service.py (1)
184-223: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win先写游玩记录,再落盘轮询基线
SteamUID/SteamPoll/poll_service.py:221-222现在先flush_status_updates(update_list),再update_game_record(push_list)。一旦任一条upsert_record失败或抛错,这次gameid变化已经写回SteamIDInfo,下一轮就不会再被检测到,游玩记录会直接丢失;同一轮里后续条目也会被跳过。建议把游玩记录写入前置,或至少按条目隔离异常并记录失败。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@SteamUID/SteamPoll/poll_service.py` around lines 184 - 223, 调整 poll_and_push_game_status 中 flush_status_updates 与 update_game_record 的执行顺序,先完成 update_game_record,再落盘 flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在 update_game_record 中按条目隔离 upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
🧹 Nitpick comments (4)
SteamUID/utils/database/models.py (1)
385-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win建议为
group_id补充索引以支撑排行榜查询。
get_binds_by_group按group_id过滤,但SteamBind.group_id字段定义为Optional[str] = Field(default=None, title="群ID"),未设置index=True(对比steamid64/user_id均有索引)。随着绑定数据增长,"群排行"命令每次调用都会触发全表扫描。♻️ 建议为 group_id 添加索引(位于 SteamBind 字段定义处,非本次选中行范围)
group_id: Optional[str] =Field(default=None, index=True, title="群ID")请确认项目是否有迁移机制(如 Alembic)来同步已部署数据库的索引变更,否则仅修改模型定义对已有数据库无效。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@SteamUID/utils/database/models.py` around lines 385 - 396, 为 SteamBind.group_id 字段补充 index=True,使 get_binds_by_group 的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。README.md (1)
105-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win新增图片缺少 alt 文本。
markdownlint 提示该图片缺少替代文本(MD045)。
♿ 建议补充 alt
## steam玩什么 -<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160">+<img src="https://dlink.host/1drv/aHR0cHM6Ly8xZHJ2Lm1zL2kvYy8xYmIyNTkxODI4ZDcyZTIzL0lRQjcxemRIckRxTlI0ZGYwVmhpQm5YUkFkR0lzZjAzVktic2x2ZTQ3VUNSYnNRP2U9WXhMVUhP.jpg" width="160" alt="steam玩什么效果图">🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 105 - 107, 为 README.md 中该图片的 img 标签添加简洁、准确的 alt 属性,保留现有图片地址和 width 设置不变。Source: Linters/SAST tools
SteamUID/SteamLibarary/library_service.py (1)
75-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winplaytime 计算逻辑与
build_library_wall(32-38 行)完全重复。同一段
playtime_forever or (windows_forever + mac_forever + linux_forever + deck_forever)逻辑在两个函数中原样复制,建议提炼为公共辅助函数,避免未来两处逻辑走偏。♻️ 建议:提炼公共函数
def_get_total_playtime(game: dict) ->int: return ( game.get("playtime_forever", 0) orgame.get("playtime_windows_forever", 0) +game.get("playtime_mac_forever", 0) +game.get("playtime_linux_forever", 0) +game.get("playtime_deck_forever", 0) )随后在
build_library_wall与build_random_pick中分别调用_get_total_playtime(game)替换重复代码块。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@SteamUID/SteamLibarary/library_service.py` around lines 75 - 81, 提取公共辅助函数 `_get_total_playtime`,集中实现 `playtime_forever` 优先、否则累加各平台游玩时长的逻辑;更新 `build_library_wall` 和 `build_random_pick` 调用该函数,移除两处重复计算代码。SteamUID/utils/PIL/draw.py (1)
678-804: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win推荐卡数量小于 3 时布局不居中。
画布宽度固定为
_PLAY_LOGICAL_W = 600(正好等于 3 张卡片 + 间距的总宽度),但卡片绘制时cx始终从SIDE_PAD开始向右排列。当picks长度为 1 或 2(库存游戏不足 3 款时会发生,见library_service.py中pick_count = min(3, len(games))),卡片会靠左排列,右侧出现大片空白,视觉效果不佳。♻️ 建议:按实际卡片数量居中
# —— 绘制卡片 —— cards_top = A(TOP_PAD + TITLE_H + TITLE_GAP + SUBTITLE_H + SECTION_GAP) + n = len(picks)+ content_w = n * CARD_W + (n - 1) * CARD_GAP+ start_x = (_PLAY_LOGICAL_W - content_w) / 2+ for i, game in enumerate(picks): - cx = A(SIDE_PAD) + A(i * (CARD_W + CARD_GAP))+ cx = A(start_x) + A(i * (CARD_W + CARD_GAP))Also applies to: 754-770
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@SteamUID/utils/PIL/draw.py` around lines 678 - 804, Update the card positioning in draw_what_to_play so the rendered cards are horizontally centered based on len(picks), while preserving the existing card width and gap. Compute a centered starting x-coordinate from the fixed canvas width and actual card count, then use it when calculating each card’s cx instead of always starting at SIDE_PAD.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 79-88: Update the `steam加好友` example in the README command table
to remove the `123456` argument, using the `@xx steam加好友` form while preserving
the existing description.
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 55-95: 修复 build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化
game_data 为空列表,确保后续 game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。
In `@SteamUID/SteamRanking/__init__.py`:
- Line 28: Update the text assignment in the ranking initialization flow to
remove the unnecessary f-string prefix from the literal, preserving the existing
string content and newline while resolving Ruff F541.
In `@SteamUID/SteamRanking/ranking_service.py`:
- Around line 20-23: Update the binds handling in the ranking service to
distinguish a failed query from a valid empty result: raise SteamError when
get_binds_by_group returns None, while continuing to return an empty list when
it returns an empty list. Align this behavior with the existing records handling
and preserve the group_ranking error path.
In `@SteamUID/utils/database/models.py`:
- Around line 597-652: 在 SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is
None)中先校验 start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。
In `@SteamUID/utils/utils.py`:
- Line 80: Update the docstring near the duration-conversion utility to replace
the full-width comma with ASCII punctuation or parentheses, preserving the
existing description and satisfying Ruff RUF002.
---
Outside diff comments:
In `@SteamUID/SteamPoll/poll_service.py`:
- Around line 184-223: 调整 poll_and_push_game_status 中 flush_status_updates 与
update_game_record 的执行顺序,先完成 update_game_record,再落盘
flush_status_updates,确保游玩记录写入失败时不会提前更新轮询基线;同时在 update_game_record 中按条目隔离
upsert_record 异常并记录失败,避免单条记录异常中断后续条目处理。
---
Nitpick comments:
In `@README.md`:
- Around line 105-107: 为 README.md 中该图片的 img 标签添加简洁、准确的 alt 属性,保留现有图片地址和 width
设置不变。
In `@SteamUID/SteamLibarary/library_service.py`:
- Around line 75-81: 提取公共辅助函数 `_get_total_playtime`,集中实现 `playtime_forever`
优先、否则累加各平台游玩时长的逻辑;更新 `build_library_wall` 和 `build_random_pick`
调用该函数,移除两处重复计算代码。
In `@SteamUID/utils/database/models.py`:
- Around line 385-396: 为 SteamBind.group_id 字段补充 index=True,使 get_binds_by_group
的过滤查询使用索引;同时检查项目现有的数据库迁移机制,并通过对应迁移为已部署数据库创建该索引,避免仅修改模型定义而未同步实际数据库。
In `@SteamUID/utils/PIL/draw.py`:
- Around line 678-804: Update the card positioning in draw_what_to_play so the
rendered cards are horizontally centered based on len(picks), while preserving
the existing card width and gap. Compute a centered starting x-coordinate from
the fixed canvas width and actual card count, then use it when calculating each
card’s cx instead of always starting at SIDE_PAD.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4c992a5c-c784-424f-a939-eaec03caba02
📒 Files selected for processing (13)
CHANGELOG.mdREADME.mdSteamUID/SteamHelp/help.jsonSteamUID/SteamLibarary/__init__.pySteamUID/SteamLibarary/library_service.pySteamUID/SteamPoll/poll_service.pySteamUID/SteamRanking/__init__.pySteamUID/SteamRanking/ranking_service.pySteamUID/utils/PIL/draw.pySteamUID/utils/database/__init__.pySteamUID/utils/database/admin.pySteamUID/utils/database/models.pySteamUID/utils/utils.py
💤 Files with no reviewable changes (1)
- CHANGELOG.md
| #### 社交相关 | ||
| | 命令 | 说明 | | ||
| |------|:------:| | ||
| | `@xx steam加好友123456` | 获取被 @ 用户的好友码 | | ||
| | `steam群排行` | 查看群游玩时长排行榜 | | ||
| #### 其他服务 | ||
| | 命令 | 说明 | | ||
| |------|:------:| | ||
| | `steam帮助` | 呼出本插件帮助菜单 | |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash# 确认“加好友”命令实际是否需要数字参数
rg -n '加好友' -g '*.py' -A 15Repository: sssysy/SteamUID
Length of output: 153
steam加好友 示例去掉多余参数。
这里应写成 @xx steam加好友;123456 与“获取被 @ 用户的好友码”的语义不符,也会误导用户。
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 80-80: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
[warning] 86-86: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 79 - 88, Update the `steam加好友` example in the README
command table to remove the `123456` argument, using the `@xx steam加好友` form
while preserving the existing description.
| async def build_random_pick(steamid64: str) -> bytes: | ||
| """从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。""" | ||
| api_key = SteamConfig.get_config("SteamWebAPIKey").data | ||
| if not api_key: | ||
| raise SteamConfigError("请先配置 steam web api key") | ||
| library = await get_steamlibrary_by_steamid64(api_key, steamid64) | ||
| games = library.get("games") | ||
| if games is None: | ||
| raise SteamValidationError("获取 steam 游戏库列表失败") | ||
| if not games: | ||
| raise SteamValidationError("该 steam 账号暂无游戏库存") | ||
| pick_count = min(3, len(games)) | ||
| picks = random.sample(games, pick_count) | ||
| for game in picks: | ||
| appid = str(game.get("appid", "")) | ||
| name = game.get("name", "未知游戏") | ||
| playtime = ( | ||
| game.get("playtime_forever", 0) or | ||
| game.get("playtime_windows_forever", 0) + | ||
| game.get("playtime_mac_forever", 0) + | ||
| game.get("playtime_linux_forever", 0) + | ||
| game.get("playtime_deck_forever", 0) | ||
| ) | ||
| cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900') | ||
| game_data.append({ | ||
| "appid": appid, | ||
| "name": name, | ||
| "playtime": playtime, | ||
| "cover_url": cover_url, | ||
| }) }) | ||
| img = await draw_what_to_play(game_data) | ||
| img = img.convert("RGB") | ||
| buf = BytesIO() | ||
| img.save(buf, format="JPEG", quality=pic_quality, subsampling=0) | ||
| return buf.getvalue() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
阻断性问题:语法错误 + game_data 未定义,将导致整个模块无法导入。
第 88 行存在多余的右括号 }) })(Ruff 已报 invalid-syntax),会直接导致该文件解析失败;即便修正括号,函数体内也从未初始化 game_data,第 83 行 game_data.append(...) 会抛出 NameError。
由于 SteamLibarary/__init__.py 在模块级 from .library_service import build_library_wall, build_random_pick,此错误会使整个 SteamLibarary 模块加载失败,波及模块内所有已注册命令(包括原有 游戏墙 命令),而不只是新增的“玩什么”。
🐛 修复语法错误与变量初始化
pick_count = min(3, len(games))
picks = random.sample(games, pick_count)
+ game_data = []
for game in picks:
appid = str(game.get("appid", ""))
name = game.get("name", "未知游戏")
playtime = (
game.get("playtime_forever", 0) or
game.get("playtime_windows_forever", 0) +
game.get("playtime_mac_forever", 0) +
game.get("playtime_linux_forever", 0) +
game.get("playtime_deck_forever", 0)
)
cover_url = SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900')
game_data.append({
"appid": appid,
"name": name,
"playtime": playtime,
"cover_url": cover_url,
- }) })+ })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| asyncdefbuild_random_pick(steamid64: str) ->bytes: | |
| """从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。""" | |
| api_key=SteamConfig.get_config("SteamWebAPIKey").data | |
| ifnotapi_key: | |
| raiseSteamConfigError("请先配置 steam web api key") | |
| library=awaitget_steamlibrary_by_steamid64(api_key, steamid64) | |
| games=library.get("games") | |
| ifgamesisNone: | |
| raiseSteamValidationError("获取 steam 游戏库列表失败") | |
| ifnotgames: | |
| raiseSteamValidationError("该 steam 账号暂无游戏库存") | |
| pick_count=min(3, len(games)) | |
| picks=random.sample(games, pick_count) | |
| forgameinpicks: | |
| appid=str(game.get("appid", "")) | |
| name=game.get("name", "未知游戏") | |
| playtime= ( | |
| game.get("playtime_forever", 0) or | |
| game.get("playtime_windows_forever", 0) + | |
| game.get("playtime_mac_forever", 0) + | |
| game.get("playtime_linux_forever", 0) + | |
| game.get("playtime_deck_forever", 0) | |
| ) | |
| cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900') | |
| game_data.append({ | |
| "appid": appid, | |
| "name": name, | |
| "playtime": playtime, | |
| "cover_url": cover_url, | |
| }) }) | |
| img=awaitdraw_what_to_play(game_data) | |
| img=img.convert("RGB") | |
| buf=BytesIO() | |
| img.save(buf, format="JPEG", quality=pic_quality, subsampling=0) | |
| returnbuf.getvalue() | |
| asyncdefbuild_random_pick(steamid64: str) ->bytes: | |
| """从用户 Steam 游戏库中随机选取 3 款游戏,生成推荐图片。""" | |
| api_key=SteamConfig.get_config("SteamWebAPIKey").data | |
| ifnotapi_key: | |
| raiseSteamConfigError("请先配置 steam web api key") | |
| library=awaitget_steamlibrary_by_steamid64(api_key, steamid64) | |
| games=library.get("games") | |
| ifgamesisNone: | |
| raiseSteamValidationError("获取 steam 游戏库列表失败") | |
| ifnotgames: | |
| raiseSteamValidationError("该 steam 账号暂无游戏库存") | |
| pick_count=min(3, len(games)) | |
| picks=random.sample(games, pick_count) | |
| game_data= [] | |
| forgameinpicks: | |
| appid=str(game.get("appid", "")) | |
| name=game.get("name", "未知游戏") | |
| playtime= ( | |
| game.get("playtime_forever", 0) or | |
| game.get("playtime_windows_forever", 0) + | |
| game.get("playtime_mac_forever", 0) + | |
| game.get("playtime_linux_forever", 0) + | |
| game.get("playtime_deck_forever", 0) | |
| ) | |
| cover_url=SteamAPI.GetGameCoverImageURL(appid, variant='library_600x900') | |
| game_data.append({ | |
| "appid": appid, | |
| "name": name, | |
| "playtime": playtime, | |
| "cover_url": cover_url, | |
| }) | |
| img=awaitdraw_what_to_play(game_data) | |
| img=img.convert("RGB") | |
| buf=BytesIO() | |
| img.save(buf, format="JPEG", quality=pic_quality, subsampling=0) | |
| returnbuf.getvalue() |
🧰 Tools
🪛 ast-grep (0.44.1)
[info] 69-69: use secrets package over random package
Context: random.sample(games, pick_count)
Note: [CWE-330] Use of Insufficiently Random Values.
(avoid-random-python)
🪛 Ruff (0.15.20)
[warning] 88-88: Expected a statement
(invalid-syntax)
[warning] 88-88: Expected a statement
(invalid-syntax)
[warning] 88-89: Expected a statement
(invalid-syntax)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SteamUID/SteamLibarary/library_service.py` around lines 55 - 95, 修复
build_random_pick 中导致模块无法解析的多余右括号,并在遍历 picks 前初始化 game_data 为空列表,确保后续
game_data.append 调用可正常执行;保持现有游戏选择、数据组装和图片生成流程不变。
Source: Linters/SAST tools
| await bot.send("本群暂无游戏时长排行数据") | ||
| return | ||
| text = f"本群游戏时长排行:\n" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
移除多余的 f-string 前缀。
text = f"本群游戏时长排行:\n" 不含任何占位符,Ruff 已将其标记为 F541(error 级别),建议去掉 f 前缀以通过 lint 检查。
🧹 建议修复
- text = f"本群游戏时长排行:\n"+ text = "本群游戏时长排行:\n"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| text=f"本群游戏时长排行:\n" | |
| text="本群游戏时长排行:\n" |
🧰 Tools
🪛 Ruff (0.15.20)
[error] 28-28: f-string without any placeholders
Remove extraneous f prefix
(F541)
[warning] 28-28: String contains ambiguous : (FULLWIDTH COLON). Did you mean : (COLON)?
(RUF001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SteamUID/SteamRanking/__init__.py` at line 28, Update the text assignment in
the ranking initialization flow to remove the unnecessary f-string prefix from
the literal, preserving the existing string content and newline while resolving
Ruff F541.
Source: Linters/SAST tools
| # 第1步:取群绑定列表,构建映射 | ||
| binds = await SteamBind.get_binds_by_group(group_id) | ||
| if not binds: | ||
| return [] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
binds 的 None 与空列表未区分,可能把查询失败误报为"暂无数据"。
对比下方第 41-43 行对 records 的处理(显式区分 None 与空列表,查询失败时 raise SteamError),此处 if not binds: return [] 会把 get_binds_by_group 因 with_session 重试耗尽而返回的 None 与"本群确实没有绑定"的正常空结果混为一谈,导致真实的数据库故障被静默呈现为"本群暂无游戏时长排行数据"(见 SteamRanking/__init__.py 中 group_ranking 的分支逻辑),不利于故障排查。
🐛 建议修复
binds = await SteamBind.get_binds_by_group(group_id)
+ if binds is None:+ raise SteamError("查询群绑定信息失败,请稍后重试")
if not binds:
return []📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # 第1步:取群绑定列表,构建映射 | |
| binds=awaitSteamBind.get_binds_by_group(group_id) | |
| ifnotbinds: | |
| return [] | |
| # 第1步:取群绑定列表,构建映射 | |
| binds=awaitSteamBind.get_binds_by_group(group_id) | |
| ifbindsisNone: | |
| raiseSteamError("查询群绑定信息失败,请稍后重试") | |
| ifnotbinds: | |
| return [] |
🧰 Tools
🪛 Ruff (0.15.20)
[warning] 20-20: Comment contains ambiguous : (FULLWIDTH COLON). Did you mean : (COLON)?
(RUF003)
[warning] 20-20: Comment contains ambiguous , (FULLWIDTH COMMA). Did you mean , (COMMA)?
(RUF003)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SteamUID/SteamRanking/ranking_service.py` around lines 20 - 23, Update the
binds handling in the ranking service to distinguish a failed query from a valid
empty result: raise SteamError when get_binds_by_group returns None, while
continuing to return an empty list when it returns an empty list. Align this
behavior with the existing records handling and preserve the group_ranking error
path.
| class SteamPlayRecord(BaseIDModel, table=True): | ||
| """Steam游戏游玩记录表(用于游戏排行榜及衍生功能)""" | ||
| __table_args__: Dict[str, Any] = {"extend_existing": True} | ||
| steamid64: str = Field(default=None, index=True, title="SteamID64") | ||
| appid: str = Field(default=None, index=True, title="游戏AppID") | ||
| start_ts: int = Field(default=None, title="开始游戏时间(Unix时间戳)") | ||
| end_ts: Optional[int] = Field(default=None, title="结束游戏时间(Unix时间戳,NULL=进行中)") | ||
| @classmethod | ||
| @with_session | ||
| async def upsert_record( | ||
| cls: Type[T_SteamPlayRecord], | ||
| session: AsyncSession, | ||
| steamid64: str, | ||
| appid: str, | ||
| start_ts: Optional[int] = None, | ||
| end_ts: Optional[int] = None, | ||
| ) -> int: | ||
| """ | ||
| 写入游玩记录(upsert)。 | ||
| - 不传 end_ts(开始游戏):新增一条记录,start_ts 必填。 | ||
| - 传了 end_ts(结束游戏):按 steamid64 + appid + end_ts IS NULL | ||
| 定位进行中的记录并写入 end_ts,start_ts 此时忽略。 | ||
| 一个玩家同一时间在同一个 appid 只会有一条 end_ts 为 NULL 的记录, | ||
| 因此该定位方式不会产生歧义。 | ||
| 返回 0 表示成功,-1 表示结束游戏时未找到进行中的记录。 | ||
| """ | ||
| if end_ts is None: | ||
| session.add( | ||
| cls( | ||
| steamid64=steamid64, # type: ignore | ||
| appid=appid, # type: ignore | ||
| start_ts=start_ts, # type: ignore | ||
| end_ts=None, # type: ignore | ||
| ) | ||
| ) | ||
| return 0 | ||
| # 结束游戏:查找进行中的记录 | ||
| stmt = ( | ||
| select(cls) | ||
| .where( | ||
| cls.steamid64 == steamid64, | ||
| cls.appid == appid, | ||
| cls.end_ts.is_(None), # type: ignore | ||
| ) | ||
| .order_by(cls.id.desc()) # type: ignore | ||
| ) | ||
| result = await session.execute(stmt) | ||
| existing = result.scalars().first() | ||
| if existing is None: | ||
| return -1 | ||
| existing.end_ts = end_ts | ||
| session.add(existing) | ||
| return 0 |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bashset -euo pipefail
printf'\n== models.py around SteamPlayRecord ==\n'
sed -n '560,700p' SteamUID/utils/database/models.py
printf'\n== ranking_service.py references ==\n'
rg -n "duration\s*=|start_ts|end_ts" SteamUID/SteamRanking/ranking_service.py
printf'\n== poll_service.py references ==\n'
rg -n "upsert_record|start_ts|end_ts" SteamUID -g '!**/__pycache__/**'printf'\n== locate SteamPlayRecord usages ==\n'
rg -n "SteamPlayRecord|upsert_record\(" SteamUIDRepository: sssysy/SteamUID
Length of output: 8887
🏁 Script executed:
#!/bin/bashset -euo pipefail
printf'\n== model base classes ==\n'
sed -n '1,220p' SteamUID/utils/database/models.py
printf'\n== search for similar Field(default=None) patterns on non-Optional annotations ==\n'
rg -n ':\s*(int|str|float|bool|datetime|date)\s*=\s*Field\(default=None' SteamUID/utils/database/models.py
printf'\n== search for model config / nullable hints ==\n'
rg -n 'model_config|Config|nullable|sa_column|Field\(' SteamUID/utils/database/models.py SteamUID/utils/database -g '!**/__pycache__/**'Repository: sssysy/SteamUID
Length of output: 13449
🌐 Web query:
SQLModel Field(default=None) non-Optional nullable column behavior default None int annotation
💡 Result:
In SQLModel, the behavior of a column defined with a non-Optional type annotation (e.g., int) combined with Field(default=None) depends on how SQLModel maps these attributes to both Pydantic (for validation) and SQLAlchemy (for database schema generation) [1][2]. When you define a field as my_field: int = Field(default=None), SQLModel interprets this as follows: 1. Pydantic Validation: Because the field has a default value of None, Pydantic treats the field as not required [2][3]. During validation, if no value is provided, it will default to None [2]. 2. SQLAlchemy Nullability: SQLModel automatically determines the nullability of the database column based on the type annotation and the presence of a default value [1]. Historically, early versions of SQLModel might have treated such fields as nullable in the database [4][5]. However, as of modern versions, SQLModel generally infers nullable=False for non-Optional types unless specified otherwise [5][1]. Crucially, Field(default=None) explicitly tells SQLModel to use None as the Python-side default value [2]. If you intend for the database column to be nullable (allowing NULL values), it is best practice to use an Optional type annotation (e.g., int | None or Optional[int]) [2][3]. This ensures that the type checker, Pydantic validation, and the SQL schema generation are all consistent regarding the field's ability to hold a NULL value [2]. If you use a non-Optional annotation with Field(default=None), you may encounter inconsistencies where the Python type checker expects an int but the runtime value is None, or where the database schema might be configured as NOT NULL while you are attempting to insert None [4][2]. To ensure a column is nullable in the database, always use an Optional type annotation [2][3]. If you need to force specific nullability, you can explicitly pass nullable=True or nullable=False within the Field function [1].
Citations:
- 1: https://github.com/tiangolo/sqlmodel/blob/main/sqlmodel/main.py
- 2: https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/?h=optional
- 3: https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/?h=create+engine
- 4: Non-optional property with default value is not translated to a non-nullable field in sqlalchemy fastapi/sqlmodel#76
- 5: 🐛 Fix setting nullable property of Fields that don't accept
Nonefastapi/sqlmodel#79
开始游戏分支补上 start_ts 非空校验upsert_record() 的“开始游戏”分支现在会直接接收并写入 start_ts=None,和文档里的“必填”不一致;而 SteamRanking/ranking_service.py 里又直接做 end_ts - start_ts,一旦落入空值记录就会把排行计算打断。建议在 end_ts is None 分支里显式拒绝空 start_ts。
🧰 Tools
🪛 Ruff (0.15.20)
[warning] 598-598: Docstring contains ambiguous ( (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?
(RUF002)
[warning] 598-598: Docstring contains ambiguous ) (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?
(RUF002)
[warning] 599-599: Mutable default value for class attribute
(RUF012)
[warning] 617-617: Docstring contains ambiguous ( (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?
(RUF002)
[warning] 617-617: Docstring contains ambiguous ) (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?
(RUF002)
[warning] 618-618: Docstring contains ambiguous ( (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?
(RUF002)
[warning] 618-618: Docstring contains ambiguous ) (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?
(RUF002)
[warning] 618-618: Docstring contains ambiguous : (FULLWIDTH COLON). Did you mean : (COLON)?
(RUF002)
[warning] 618-618: Docstring contains ambiguous , (FULLWIDTH COMMA). Did you mean , (COMMA)?
(RUF002)
[warning] 619-619: Docstring contains ambiguous ( (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?
(RUF002)
[warning] 619-619: Docstring contains ambiguous ) (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?
(RUF002)
[warning] 619-619: Docstring contains ambiguous : (FULLWIDTH COLON). Did you mean : (COLON)?
(RUF002)
[warning] 620-620: Docstring contains ambiguous , (FULLWIDTH COMMA). Did you mean , (COMMA)?
(RUF002)
[warning] 621-621: Docstring contains ambiguous , (FULLWIDTH COMMA). Did you mean , (COMMA)?
(RUF002)
[warning] 623-623: Docstring contains ambiguous , (FULLWIDTH COMMA). Did you mean , (COMMA)?
(RUF002)
[warning] 636-636: Comment contains ambiguous : (FULLWIDTH COLON). Did you mean : (COLON)?
(RUF003)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SteamUID/utils/database/models.py` around lines 597 - 652, 在
SteamPlayRecord.upsert_record 的开始游戏分支(end_ts is None)中先校验
start_ts,发现为空时显式拒绝并返回失败结果,禁止创建 start_ts=None 的记录;保留有效 start_ts
的新增逻辑,并确保调用方能区分该失败情况。
| def time_convert_s(seconds: int) -> str: | ||
| """将秒数转换为人类可读的时长,如 1天2小时30分45秒""" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
修正文档字符串中的全角逗号
Ruff RUF002 已报告该字符可能导致 lint 检查失败。请改用括号或 ASCII 标点。
建议修改
- """将秒数转换为人类可读的时长,如 1天2小时30分45秒"""+ """将秒数转换为人类可读的时长(如 1天2小时30分45秒)"""📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| """将秒数转换为人类可读的时长,如 1天2小时30分45秒""" | |
| """将秒数转换为人类可读的时长(如 1天2小时30分45秒)""" |
🧰 Tools
🪛 Ruff (0.15.20)
[warning] 80-80: Docstring contains ambiguous , (FULLWIDTH COMMA). Did you mean , (COMMA)?
(RUF002)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SteamUID/utils/utils.py` at line 80, Update the docstring near the
duration-conversion utility to replace the full-width comma with ASCII
punctuation or parentheses, preserving the existing description and satisfying
Ruff RUF002.
Source: Linters/SAST tools
Summary by Sourcery
添加游戏游玩记录和群组排名功能,以及从用户的 Steam 库中随机推荐游戏的图片。
New Features:
Enhancements:
Documentation:
Original summary in English
Summary by Sourcery
Add game play recording and group ranking features, plus a random game recommendation image from a user's Steam library.
New Features:
Enhancements:
Documentation:
Summary by CodeRabbit