Skip to content

refactor(backend): organize modules by architectural responsibility - #134

Merged
cg8-5712 merged 1 commit into
mainfrom
backend-layout-refactor
Jul 27, 2026
Merged

cg8-5712 merged 1 commit into
mainfrom
backend-layout-refactor

Conversation

@cg8-5712

@cg8-5712 cg8-5712 commented Jul 27, 2026 •

Copy link
Copy Markdown
Collaborator
  • move HTTP handlers, models, errors, and rate limiting into api
  • group application state and use-case services under application
  • relocate business roles and intake assessment into domain
  • isolate AI and map adapters under integrations
  • move SeaORM entities into persistence
  • preserve legacy public module paths through crate re-exports
  • update imports in configuration and server bootstrap
  • remove the obsolete architecture document

Summary by CodeRabbit

  • 新功能

    • 新增登录、登出及当前用户查询功能,并加强登录失败限流。
    • 新增案件、线索、任务、成员、地点及附件管理能力。
    • 新增信息采集会话、智能问答、画像草稿与人工确认流程。
    • 支持任务位置上报、附件上传下载及路线规划。
    • 提供统一的认证、权限控制、错误提示与健康检查接口。
  • 安全与可靠性

    • 增加访问权限校验、操作审计、敏感信息保护及附件安全验证。
    • 智能服务不可用时支持规则降级处理。
  • 文档

    • 移除架构说明文档。

- move HTTP handlers, models, errors, and rate limiting into `api`
- group application state and use-case services under `application`
- relocate business roles and intake assessment into `domain`
- isolate AI and map adapters under `integrations`
- move SeaORM entities into `persistence`
- preserve legacy public module paths through crate re-exports
- update imports in configuration and server bootstrap
- remove the obsolete architecture document
Copilot AI review requested due to automatic review settings July 27, 2026 03:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c6d9a438-2e14-4b58-ba09-8e6b9439cfdf

📥 Commits

Reviewing files that changed from the base of the PR and between 917b113 and 209519d.

📒 Files selected for processing (52)
  • docs/ARCHITECTURE.md
  • src/api/auth.rs
  • src/api/error.rs
  • src/api/mod.rs
  • src/api/models.rs
  • src/api/rate_limit.rs
  • src/api/routes/auth.rs
  • src/api/routes/cases.rs
  • src/api/routes/clues.rs
  • src/api/routes/health.rs
  • src/api/routes/intake_sessions.rs
  • src/api/routes/mod.rs
  • src/api/routes/tasks.rs
  • src/application/app_state.rs
  • src/application/mod.rs
  • src/application/services/auth_service.rs
  • src/application/services/case_resource_service.rs
  • src/application/services/case_service.rs
  • src/application/services/intake_session_service.rs
  • src/application/services/mod.rs
  • src/application/services/task_service.rs
  • src/config.rs
  • src/domain/intake_assessment.rs
  • src/domain/mod.rs
  • src/domain/roles.rs
  • src/integrations/ai_gateway.rs
  • src/integrations/amap_service.rs
  • src/integrations/mod.rs
  • src/lib.rs
  • src/main.rs
  • src/persistence/entities/ai_prompt_templates.rs
  • src/persistence/entities/audit_events.rs
  • src/persistence/entities/auth_sessions.rs
  • src/persistence/entities/case_attachments.rs
  • src/persistence/entities/case_memberships.rs
  • src/persistence/entities/case_places.rs
  • src/persistence/entities/cases.rs
  • src/persistence/entities/clue_attachment_links.rs
  • src/persistence/entities/clue_attributions.rs
  • src/persistence/entities/clues.rs
  • src/persistence/entities/elder_profiles.rs
  • src/persistence/entities/intake_answer_revisions.rs
  • src/persistence/entities/intake_question_definitions.rs
  • src/persistence/entities/intake_session_answers.rs
  • src/persistence/entities/intake_sessions.rs
  • src/persistence/entities/mod.rs
  • src/persistence/entities/task_assignments.rs
  • src/persistence/entities/task_location_reports.rs
  • src/persistence/entities/tasks.rs
  • src/persistence/entities/user_global_capabilities.rs
  • src/persistence/entities/users.rs
  • src/persistence/mod.rs
💤 Files with no reviewable changes (1)
  • docs/ARCHITECTURE.md

📝 Walkthrough

Walkthrough

Changes

本次变更新增分层后端架构,覆盖认证、案件与线索、任务、摄入会话、附件资源、AI 网关、地图服务、统一错误处理、API 路由及 SeaORM 持久化实体,并保留原有模块路径的公开重导出。

案件与服务流程

Layer / File(s) Summary
领域模型与持久化基础
src/domain/*, src/persistence/*, src/application/app_state.rs
新增角色权限模型、应用状态及认证、案件、线索、摄入、任务、附件和审计实体。
外部集成与摄入评估
src/integrations/*, src/domain/intake_assessment.rs
新增 AI 网关、AMap 路线估算和结构化摄入事实评估。
认证与摄入会话服务
src/application/services/auth_service.rs, src/application/services/intake_session_service.rs
新增登录会话、密码验证、登出、审计、摄入答题、画像草稿和确认生成案件流程。
案件、资源与任务服务
src/application/services/case_service.rs, src/application/services/case_resource_service.rs, src/application/services/task_service.rs
新增案件与线索权限控制、地点和图片附件处理、任务状态及位置回报流程。
API 契约与路由
src/api/*, src/lib.rs, src/main.rs
新增 API 数据模型、统一错误响应、Bearer 认证中间件、登录限流及各业务路由。

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ApiSessionAuthentication
  participant IntakeSessionService
  participant AmapService
  participant Database
  Client->>ApiSessionAuthentication: Bearer request
  ApiSessionAuthentication->>IntakeSessionService: submit_intake_answer
  IntakeSessionService->>AmapService: estimate_route
  AmapService-->>IntakeSessionService: RouteEstimate
  IntakeSessionService->>Database: persist answers and assessment
  Database-->>Client: intake response
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.49% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了此次按架构职责重组后端模块的主要变更。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backend-layout-refactor

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-134-209519d80a53.angui.cg8.site.

Temporary demo accounts (all use the password a9ac2361420ff9d6748e124735016fb49ea7de38fb93f37a):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (15)
src/integrations/amap_service.rs (1)

31-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

UnsupportedMode 变体从未被构造。

estimate_route 对 RouteMode 的匹配已覆盖全部变体,该原因值目前是死代码。若无近期计划支持更多出行方式,建议删除以免调用方为不可达分支写处理逻辑。

🤖 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 `@src/integrations/amap_service.rs` around lines 31 - 39, 删除
RouteUnavailableReason::UnsupportedMode 变体,并清理所有相关的匹配分支、引用及调用方处理逻辑;保持
estimate_route 对现有 RouteMode 变体的完整匹配和其他不可用原因行为不变。
src/integrations/ai_gateway.rs (1)

427-466: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

weight 目前只作排序次键,并非加权分流。

字段名与校验(Line 574 要求非零)暗示按权重分配流量,但实现里它只是 priority 相同时的第二排序键,同权重下始终选中 provider_id 字典序最小者,不会分散负载。若这是有意的确定性选择,建议补一行注释说明;若计划做加权路由,宜留 TODO 以免被误读。

🤖 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 `@src/integrations/ai_gateway.rs` around lines 427 - 466, Clarify the intended
semantics of weight in route: if routing is intentionally deterministic, add a
concise comment near the candidate sorting or selection explaining that weight
only breaks priority ties and does not perform weighted load distribution;
otherwise add a TODO at the same location documenting the planned
weighted-routing behavior. Preserve the existing priority, weight, and
provider_id ordering.
src/application/app_state.rs (1)

5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

建议改用规范模块路径导入。

此处仍走 crate::{ai_gateway, amap_service, rate_limit} 兼容层 re-export,而 src/main.rs 已改用 integrations::/api:: 新路径。新代码继续依赖过渡层会让后续移除 lib.rs 中的 pub use 变难。

♻️ 建议改动
-use crate::{ai_gateway::AiGateway, amap_service::AmapService, rate_limit::LoginRateLimiter};
+use crate::{
+    api::rate_limit::LoginRateLimiter,
+    integrations::{ai_gateway::AiGateway, amap_service::AmapService},
+};
🤖 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 `@src/application/app_state.rs` at line 5, 将 app_state 中对 AiGateway、AmapService
和 LoginRateLimiter 的导入改为对应的 integrations:: 或 api:: 规范模块路径,避免继续依赖 crate 的兼容层
re-export。保留现有类型名称和使用方式不变,仅替换 use 声明。
src/application/services/task_service.rs (4)

672-693: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

本模块缺少测试。

volunteer_transition_allowed 状态机、Commander 取消规则、update_task_status 的 rows_affected 并发守卫,以及位置回报的时间窗校验都是关键路径,但本文件没有测试模块(case_service.rs、auth_service.rs 均有)。建议补充覆盖。是否需要我生成一组基于内存 SQLite 的测试用例?

🤖 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 `@src/application/services/task_service.rs` around lines 672 - 693, Add a test
module to task_service.rs covering volunteer_transition_allowed state
transitions, Commander cancellation rules, update_task_status rows_affected
concurrency protection, and location-report time-window validation. Follow the
existing in-memory SQLite testing patterns from case_service.rs and
auth_service.rs, including valid, invalid, and boundary cases.

45-63: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

后台清理任务:建议使用结构化日志并对删除分批。

eprintln! 绕过了应用日志体系,生产环境难以采集与告警;另外 delete_many 无 LIMIT,如果保留期任务曾停摆,一次删除可能长时间持锁并阻塞写入。建议改用 log/tracing 记录错误,并按批(例如每轮上限若干行、循环直到删完)执行删除。

🤖 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 `@src/application/services/task_service.rs` around lines 45 - 63, Update
start_location_report_retention_purger to use the application’s structured
log/tracing facility instead of eprintln! when purge_expired_location_reports
fails. Refactor purge_expired_location_reports to delete expired reports in
bounded batches, looping until no rows remain, and return the total rows
affected while preserving the existing retention filter and error propagation.

600-631: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

分页校验逻辑与 case_service::ValidatedClueTimelineQuery 重复。

offset()、page_size_usize() 与 TryFrom 中的 page/page_size 边界校验在两个服务中几乎逐行相同(src/application/services/case_service.rs Line 513-542)。建议抽出一个共享的 Pagination 类型(携带各自的 MAX_PAGE_SIZE),避免规则后续漂移。

🤖 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 `@src/application/services/task_service.rs` around lines 600 - 631, Extract the
duplicated pagination logic from ValidatedTaskListQuery and
case_service::ValidatedClueTimelineQuery into a shared Pagination type. Have it
own page/page_size validation, offset calculation, and usize conversion while
accepting each service’s maximum page-size limit, then update both TryFrom
implementations to reuse it and preserve their existing validation behavior and
error messages.

205-232: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

任务列表同样是内存分页:先全量加载再截取。

tasks::Entity::find().all(db) 拉取该案件所有任务,随后在内存中按角色过滤并 skip/take。可见性规则(Commander 全量 / Volunteer 仅自己被指派)可下推为 SQL JOIN 条件,从而使用数据库 LIMIT/OFFSET 与 COUNT。

🤖 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 `@src/application/services/task_service.rs` around lines 205 - 232, Update the
task listing flow around tasks::Entity::find and assignments_for_tasks to push
role-based visibility into the database query, using the appropriate assignment
join and Commander/Volunteer conditions. Compute the total with a database COUNT
query and apply query.offset() and query.page_size_usize() as SQL OFFSET/LIMIT
before loading results; preserve due-date and ID ordering and TaskResponse
assignment data.
src/application/services/auth_service.rs (2)

393-425: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

建议补充认证关键分支的测试。

当前仅覆盖注销路径。建议补充:错误密码返回 Unauthorized、会话过期后 authenticate 失败、status != "active" 用户无法登录、upsert_user 重新引导后旧会话被撤销。这些都是权限边界,值得回归保护。

🤖 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 `@src/application/services/auth_service.rs` around lines 393 - 425, Expand the
authentication tests alongside logout_revokes_the_server_side_session to cover
incorrect-password login returning ApiError::Unauthorized, expired sessions
being rejected by authenticate, users whose status is not "active" being unable
to log in, and upsert_user rebootstrap revoking an existing session. Reuse the
existing database, migration, bootstrap, login, authenticate, logout, and
upsert_user helpers and assert each permission boundary explicitly.

124-127: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

每次鉴权都写库更新 last_used_at,会带来明显写放大。

authenticate 在每个受保护请求上都执行一次 UPDATE,SQLite/单写入连接下容易成为吞吐瓶颈与锁竞争源。建议按节流阈值(例如距上次更新超过若干分钟才写)更新。

♻️ 建议的节流思路
-    let session_id = session.id.clone();
-    let mut active = session.into_active_model();
-    active.last_used_at = Set(now());
-    active.update(db).await?;
+    let session_id = session.id.clone();
+    let refreshed_at = now();
+    // 仅在超过节流间隔时写库,避免每个请求一次 UPDATE。
+    if should_refresh_last_used_at(&session.last_used_at, &refreshed_at) {
+        let mut active = session.into_active_model();
+        active.last_used_at = Set(refreshed_at);
+        active.update(db).await?;
+    }
🤖 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 `@src/application/services/auth_service.rs` around lines 124 - 127, 在
authenticate 流程中调整 last_used_at 的更新逻辑:仅当当前时间与会话已有的 last_used_at
相隔超过节流阈值(例如数分钟)时,才通过 active.update(db) 写回数据库;未达到阈值时跳过 UPDATE,同时保留 session_id
提取和鉴权结果行为不变。
src/api/rate_limit.rs (1)

12-15: 🔒 Security & Privacy | 🔵 Trivial

进程内限流在多副本部署下失效。

计数保存在单进程内存中,横向扩容后实际阈值变为 MAX_FAILURES × 副本数,且滚动重启会清空所有计数。若后续需要多实例部署,建议改为共享存储(如 Redis)或在入口网关层做登录限流。

🤖 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 `@src/api/rate_limit.rs` around lines 12 - 15, Update LoginRateLimiter so
attempt counts are stored in shared infrastructure rather than the process-local
attempts Mutex<HashMap<...>>, ensuring limits remain consistent across replicas
and restarts. Use the project’s existing Redis/shared-storage or gateway
rate-limiting mechanism if available, and preserve the current login-threshold
behavior.
src/application/services/case_service.rs (1)

347-375: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

列表接口统一采用「全量加载 + 内存分页」。 共同根因是基于角色的可见性规则没有下推到 SQL,导致必须先取回案件下的全部行、在应用层过滤后再 skip/take,total 也在内存中统计。随着案件生命周期变长,单次请求的内存与 I/O 成本会线性增长,而 page_size 上限(100)完全不起保护作用。

  • src/application/services/case_service.rs#L347-L375:把可见性条件下推为 SQL(Volunteer → status = 'confirmed';Family → status = 'confirmed' OR clue_attributions.submitted_by_user_id = ?),改用数据库 LIMIT/OFFSET + COUNT,并为 clues(case_id, created_at) 建复合索引。
  • src/application/services/task_service.rs#L205-L232:把「Commander 全量 / Volunteer 仅自己被指派」改为与 task_assignments 的 JOIN 过滤条件,同样使用数据库分页与计数。
🤖 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 `@src/application/services/case_service.rs` around lines 347 - 375,
将可见性过滤和分页下推到数据库,避免全量加载后在内存中筛选与统计。对于
src/application/services/case_service.rs:347-375,更新 clue 查询:Volunteer 仅查询 status
为 confirmed 的记录,Family 查询 confirmed 或由当前用户提交归因的记录,并使用数据库 COUNT、LIMIT/OFFSET;同时为
clues(case_id, created_at) 添加复合索引。对于
src/application/services/task_service.rs:205-232,将 Commander 全量与 Volunteer
仅本人被指派的规则通过 task_assignments JOIN 过滤实现,并改用数据库分页和计数。
src/application/services/case_resource_service.rs (1)

105-126: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚖️ Poor tradeoff

附件数量上限存在 TOCTOU,且文件先落盘再入库会遗留孤儿文件。

count 与后续 insert 之间没有互斥,在非序列化隔离级别下并发上传可超过 max_attachments_per_case。另外文件在事务提交前就写入磁盘,进程崩溃时 remove_file_best_effort 无法执行,会残留孤儿文件。

建议:上限改为数据库侧约束或在提交前重新 count 校验;同时增加一个定期扫描存储目录、清理无对应 case_attachments 记录的后台任务(与 task_service 的保留期清理类似)。

🤖 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 `@src/application/services/case_resource_service.rs` around lines 105 - 126,
Update the attachment creation flow around current_count and the subsequent
insert to enforce max_attachments_per_case atomically at the database level, or
revalidate the count immediately before commit so concurrent uploads cannot
exceed the limit. Avoid leaving files written by web::block without a committed
case_attachments record, and add a periodic storage-directory cleanup task,
following task_service’s retention-cleanup pattern, to remove orphaned files.
src/api/routes/cases.rs (1)

102-126: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

上传路径存在多次全量内存拷贝,缺少并发上传的整体内存约束。

单次请求中图片会被完整驻留内存多次:这里的 bytes、store_image_attachment 中的 upload.bytes.to_vec()、normalize_image 解码后的位图、以及重编码输出。并发上传数 × 单张上限即为内存峰值,目前没有任何全局约束。

建议:把 bytes 以 web::Bytes/Arc<[u8]> 传递以消除一次拷贝,并在 main.rs 中配置 PayloadConfig/MultipartFormConfig 与并发上传信号量以限定整体内存占用。

🤖 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 `@src/api/routes/cases.rs` around lines 102 - 126, 调整文件上传链路,避免在 cases 路由与
store_image_attachment 之间通过 to_vec 产生额外全量拷贝,改用 web::Bytes 或 Arc<[u8]>
传递上传内容。并在应用启动配置中设置 PayloadConfig/MultipartFormConfig
的单请求限制,同时为图片上传增加全局并发信号量,确保并发请求的总内存占用受 attachment_max_image_bytes 约束。
src/application/services/intake_session_service.rs (1)

670-817: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

normalize_answers 与 normalize_single_answer 存在重复的校验逻辑。

两者都实现了"取 max_answer_chars/answer_hard_max 较小值为上限 → trim → 校验长度范围"的逻辑,仅错误文案不同。建议抽取共用的私有辅助函数,避免未来只修改一处导致两条提交路径(初始创建 vs 单字段提交)行为分叉。

♻️ 建议抽取共用校验函数
+fn normalize_answer_text(
+    raw: &str,
+    configured_limit: i32,
+    answer_hard_max: usize,
+    error_label: &str,
+) -> Result<String, ApiError> {
+    let answer_limit = usize::try_from(configured_limit)
+        .map_err(|_| ApiError::Internal)?
+        .min(answer_hard_max);
+    let trimmed = raw.trim();
+    if trimmed.is_empty() || trimmed.chars().count() > answer_limit {
+        return Err(ApiError::Validation(format!(
+            "{error_label} must contain between 1 and {answer_limit} characters"
+        )));
+    }
+    Ok(trimmed.to_owned())
+}
🤖 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 `@src/application/services/intake_session_service.rs` around lines 670 - 817,
Extract the shared limit calculation, trimming, and length validation from
normalize_answers and normalize_single_answer into one private helper that
accepts the answer, configured limit, hard maximum, and appropriate
error-context data. Update both functions to call this helper while preserving
their existing validation behavior and error messages, so initial creation and
single-field submission use the same rules.
src/persistence/entities/ai_prompt_templates.rs (1)

6-20: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

移除该持久化实体上的 Serialize
system_instruction 是内部提示词字段,没必要让 Model 直接可序列化;这样更容易在后续被误直接返回。对外输出时改用专用 DTO,只暴露需要的字段。

🤖 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 `@src/persistence/entities/ai_prompt_templates.rs` around lines 6 - 20, Remove
the Serialize derive from the ai_prompt_templates Model entity, leaving its
other derives unchanged. Ensure external responses use a dedicated DTO with only
explicitly exposed fields rather than serializing Model directly.
🤖 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 `@src/api/models.rs`:
- Around line 182-183: 将响应模型中 guidance_mode 和 privacy_notice 的硬编码英文文案抽取为统一常量或
i18n 资源,并在两个出现位置复用同一来源,避免文案漂移;同时确认并调整为面向中文家属用户的本地化隐私告知文本,保持现有响应字段结构不变。
- Around line 746-753: Update the assigned_at field construction in the
surrounding model mapping so it follows the same include_assignee visibility
gate as assigned_volunteer_user_id, hiding the timestamp when include_assignee
is false. Keep the existing assignment timestamp value unchanged when assignee
data is included.

In `@src/api/rate_limit.rs`:
- Around line 39-48: 在 RateLimiter 的 record_failure 方法中为 attempts
映射增加明确的容量上限:清理过期条目后,若新 key 不存在且已达到上限,应拒绝插入并返回现有的合适错误,保持已有 key
的失败计数逻辑不变。使用集中定义的容量常量,并确保并发锁定下不会绕过该限制。

In `@src/api/routes/auth.rs`:
- Around line 29-32: Update the account-level limiter key in the authentication
route around client_key and account_key so it is derived only from the
normalized, trimmed, lowercased request email, without including client or IP
data. Keep client_key unchanged for source-level limiting and continue checking
both limiters.

In `@src/application/services/case_resource_service.rs`:
- Around line 362-381: Update safe_filename to preserve Unicode alphanumeric
characters, including Chinese names, while still removing path separators and
control characters; avoid rejecting valid filenames solely because they contain
no ASCII characters. Also update download_attachment to emit non-ASCII filenames
using RFC 5987 filename*=UTF-8'' percent-encoding in Content-Disposition.
- Around line 335-344: Replace the `image::load_from_memory_with_format` call
with an `image::ImageReader` configured with `Limits` before decoding, setting
`max_image_width`, `max_image_height`, and `max_alloc` to enforce the existing
8,000-pixel and 20M-pixel memory limits pre-decode. Preserve the current
`ApiError::Validation` mapping and post-decode dimension validation in the
surrounding image-processing flow.

In `@src/application/services/case_service.rs`:
- Around line 424-445: 在更新 active 之前补充反向校验:当 next_status 不是 duplicate 或
conflicting 时,若 related_clue_id 存在则必须同时提供 relationship_type,否则返回
ApiError::Validation。确保 active.related_clue_id 与 active.relationship_type
不会写入缺少关系类型的关联。
- Around line 179-190: 在 `case_service` 的状态更新流程中对齐
`task_service::update_task_status`:不要直接调用 `active.update(&transaction)`,改用带有主键和
`previous_status` 条件的 `update_many` 条件更新,并检查 `rows_affected` 必须为
1;否则返回并发冲突错误,避免覆盖其他请求已提交的状态。按需要引入
`sea_orm::sea_query::Expr`,保持现有状态转移校验和更新时间更新逻辑。

In `@src/application/services/intake_session_service.rs`:
- Around line 125-277: 在 submit_intake_answer_with_map 中读取 intake_sessions
会话记录的查询链上加入排他行锁,确保事务内基于 answers_json、状态及结构化数据计算的整行更新不会并发覆盖;参照同文件
confirm_intake_session 的 lock_exclusive 用法,保持后续校验、答案写入和聚合更新逻辑不变。

In `@src/integrations/ai_gateway.rs`:
- Line 240: Replace the unsalted Sha256 computation assigned to input_hash in
the audit-record construction with HMAC-SHA256 using a runtime-injected secret
from an environment variable, managed like the provider credentials. Reuse the
configured key consistently for hashing request.input and preserve the existing
hex-encoded audit value without storing the original content.
- Around line 344-363: Update the AnthropicMessages request construction to omit
the system field when request.system_instruction is None or empty, while
preserving the existing value when present. Follow the conditional-field pattern
used by the Gemini protocol and add coverage for both None and empty-string
system instructions.

In `@src/integrations/amap_service.rs`:
- Around line 87-91: Update RouteUnavailableReason in
src/integrations/amap_service.rs#L31-L39 by removing the unused UnsupportedMode
variant and adding InvalidRequest. In the coordinate validation branch at
src/integrations/amap_service.rs#L87-L91, return InvalidRequest instead of
InvalidResponse so invalid caller coordinates are classified as request
failures.

In `@src/persistence/entities/users.rs`:
- Around line 4-14: Update the users::Model Debug implementation so
password_hash is never exposed when instances are logged. Remove the derived
Debug and provide a manual redacted Debug implementation, or otherwise ensure
Debug omits or masks password_hash while preserving the existing Clone,
PartialEq, DeriveEntityModel, Serialize, and serde behavior.

---

Nitpick comments:
In `@src/api/rate_limit.rs`:
- Around line 12-15: Update LoginRateLimiter so attempt counts are stored in
shared infrastructure rather than the process-local attempts
Mutex<HashMap<...>>, ensuring limits remain consistent across replicas and
restarts. Use the project’s existing Redis/shared-storage or gateway
rate-limiting mechanism if available, and preserve the current login-threshold
behavior.

In `@src/api/routes/cases.rs`:
- Around line 102-126: 调整文件上传链路,避免在 cases 路由与 store_image_attachment 之间通过 to_vec
产生额外全量拷贝,改用 web::Bytes 或 Arc<[u8]> 传递上传内容。并在应用启动配置中设置
PayloadConfig/MultipartFormConfig 的单请求限制,同时为图片上传增加全局并发信号量,确保并发请求的总内存占用受
attachment_max_image_bytes 约束。

In `@src/application/app_state.rs`:
- Line 5: 将 app_state 中对 AiGateway、AmapService 和 LoginRateLimiter 的导入改为对应的
integrations:: 或 api:: 规范模块路径,避免继续依赖 crate 的兼容层 re-export。保留现有类型名称和使用方式不变,仅替换
use 声明。

In `@src/application/services/auth_service.rs`:
- Around line 393-425: Expand the authentication tests alongside
logout_revokes_the_server_side_session to cover incorrect-password login
returning ApiError::Unauthorized, expired sessions being rejected by
authenticate, users whose status is not "active" being unable to log in, and
upsert_user rebootstrap revoking an existing session. Reuse the existing
database, migration, bootstrap, login, authenticate, logout, and upsert_user
helpers and assert each permission boundary explicitly.
- Around line 124-127: 在 authenticate 流程中调整 last_used_at 的更新逻辑:仅当当前时间与会话已有的
last_used_at 相隔超过节流阈值(例如数分钟)时,才通过 active.update(db) 写回数据库;未达到阈值时跳过 UPDATE,同时保留
session_id 提取和鉴权结果行为不变。

In `@src/application/services/case_resource_service.rs`:
- Around line 105-126: Update the attachment creation flow around current_count
and the subsequent insert to enforce max_attachments_per_case atomically at the
database level, or revalidate the count immediately before commit so concurrent
uploads cannot exceed the limit. Avoid leaving files written by web::block
without a committed case_attachments record, and add a periodic
storage-directory cleanup task, following task_service’s retention-cleanup
pattern, to remove orphaned files.

In `@src/application/services/case_service.rs`:
- Around line 347-375: 将可见性过滤和分页下推到数据库,避免全量加载后在内存中筛选与统计。对于
src/application/services/case_service.rs:347-375,更新 clue 查询:Volunteer 仅查询 status
为 confirmed 的记录,Family 查询 confirmed 或由当前用户提交归因的记录,并使用数据库 COUNT、LIMIT/OFFSET;同时为
clues(case_id, created_at) 添加复合索引。对于
src/application/services/task_service.rs:205-232,将 Commander 全量与 Volunteer
仅本人被指派的规则通过 task_assignments JOIN 过滤实现,并改用数据库分页和计数。

In `@src/application/services/intake_session_service.rs`:
- Around line 670-817: Extract the shared limit calculation, trimming, and
length validation from normalize_answers and normalize_single_answer into one
private helper that accepts the answer, configured limit, hard maximum, and
appropriate error-context data. Update both functions to call this helper while
preserving their existing validation behavior and error messages, so initial
creation and single-field submission use the same rules.

In `@src/application/services/task_service.rs`:
- Around line 672-693: Add a test module to task_service.rs covering
volunteer_transition_allowed state transitions, Commander cancellation rules,
update_task_status rows_affected concurrency protection, and location-report
time-window validation. Follow the existing in-memory SQLite testing patterns
from case_service.rs and auth_service.rs, including valid, invalid, and boundary
cases.
- Around line 45-63: Update start_location_report_retention_purger to use the
application’s structured log/tracing facility instead of eprintln! when
purge_expired_location_reports fails. Refactor purge_expired_location_reports to
delete expired reports in bounded batches, looping until no rows remain, and
return the total rows affected while preserving the existing retention filter
and error propagation.
- Around line 600-631: Extract the duplicated pagination logic from
ValidatedTaskListQuery and case_service::ValidatedClueTimelineQuery into a
shared Pagination type. Have it own page/page_size validation, offset
calculation, and usize conversion while accepting each service’s maximum
page-size limit, then update both TryFrom implementations to reuse it and
preserve their existing validation behavior and error messages.
- Around line 205-232: Update the task listing flow around tasks::Entity::find
and assignments_for_tasks to push role-based visibility into the database query,
using the appropriate assignment join and Commander/Volunteer conditions.
Compute the total with a database COUNT query and apply query.offset() and
query.page_size_usize() as SQL OFFSET/LIMIT before loading results; preserve
due-date and ID ordering and TaskResponse assignment data.

In `@src/integrations/ai_gateway.rs`:
- Around line 427-466: Clarify the intended semantics of weight in route: if
routing is intentionally deterministic, add a concise comment near the candidate
sorting or selection explaining that weight only breaks priority ties and does
not perform weighted load distribution; otherwise add a TODO at the same
location documenting the planned weighted-routing behavior. Preserve the
existing priority, weight, and provider_id ordering.

In `@src/integrations/amap_service.rs`:
- Around line 31-39: 删除 RouteUnavailableReason::UnsupportedMode
变体,并清理所有相关的匹配分支、引用及调用方处理逻辑;保持 estimate_route 对现有 RouteMode 变体的完整匹配和其他不可用原因行为不变。

In `@src/persistence/entities/ai_prompt_templates.rs`:
- Around line 6-20: Remove the Serialize derive from the ai_prompt_templates
Model entity, leaving its other derives unchanged. Ensure external responses use
a dedicated DTO with only explicitly exposed fields rather than serializing
Model directly.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c6d9a438-2e14-4b58-ba09-8e6b9439cfdf

📥 Commits

Reviewing files that changed from the base of the PR and between 917b113 and 209519d.

📒 Files selected for processing (52)
  • docs/ARCHITECTURE.md
  • src/api/auth.rs
  • src/api/error.rs
  • src/api/mod.rs
  • src/api/models.rs
  • src/api/rate_limit.rs
  • src/api/routes/auth.rs
  • src/api/routes/cases.rs
  • src/api/routes/clues.rs
  • src/api/routes/health.rs
  • src/api/routes/intake_sessions.rs
  • src/api/routes/mod.rs
  • src/api/routes/tasks.rs
  • src/application/app_state.rs
  • src/application/mod.rs
  • src/application/services/auth_service.rs
  • src/application/services/case_resource_service.rs
  • src/application/services/case_service.rs
  • src/application/services/intake_session_service.rs
  • src/application/services/mod.rs
  • src/application/services/task_service.rs
  • src/config.rs
  • src/domain/intake_assessment.rs
  • src/domain/mod.rs
  • src/domain/roles.rs
  • src/integrations/ai_gateway.rs
  • src/integrations/amap_service.rs
  • src/integrations/mod.rs
  • src/lib.rs
  • src/main.rs
  • src/persistence/entities/ai_prompt_templates.rs
  • src/persistence/entities/audit_events.rs
  • src/persistence/entities/auth_sessions.rs
  • src/persistence/entities/case_attachments.rs
  • src/persistence/entities/case_memberships.rs
  • src/persistence/entities/case_places.rs
  • src/persistence/entities/cases.rs
  • src/persistence/entities/clue_attachment_links.rs
  • src/persistence/entities/clue_attributions.rs
  • src/persistence/entities/clues.rs
  • src/persistence/entities/elder_profiles.rs
  • src/persistence/entities/intake_answer_revisions.rs
  • src/persistence/entities/intake_question_definitions.rs
  • src/persistence/entities/intake_session_answers.rs
  • src/persistence/entities/intake_sessions.rs
  • src/persistence/entities/mod.rs
  • src/persistence/entities/task_assignments.rs
  • src/persistence/entities/task_location_reports.rs
  • src/persistence/entities/tasks.rs
  • src/persistence/entities/user_global_capabilities.rs
  • src/persistence/entities/users.rs
  • src/persistence/mod.rs
💤 Files with no reviewable changes (1)
  • docs/ARCHITECTURE.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 13

🧹 Nitpick comments (15)
src/integrations/amap_service.rs (1)

31-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

UnsupportedMode 变体从未被构造。

estimate_route 对 RouteMode 的匹配已覆盖全部变体,该原因值目前是死代码。若无近期计划支持更多出行方式,建议删除以免调用方为不可达分支写处理逻辑。

🤖 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 `@src/integrations/amap_service.rs` around lines 31 - 39, 删除
RouteUnavailableReason::UnsupportedMode 变体,并清理所有相关的匹配分支、引用及调用方处理逻辑;保持
estimate_route 对现有 RouteMode 变体的完整匹配和其他不可用原因行为不变。
src/integrations/ai_gateway.rs (1)

427-466: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

weight 目前只作排序次键,并非加权分流。

字段名与校验(Line 574 要求非零)暗示按权重分配流量,但实现里它只是 priority 相同时的第二排序键,同权重下始终选中 provider_id 字典序最小者,不会分散负载。若这是有意的确定性选择,建议补一行注释说明;若计划做加权路由,宜留 TODO 以免被误读。

🤖 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 `@src/integrations/ai_gateway.rs` around lines 427 - 466, Clarify the intended
semantics of weight in route: if routing is intentionally deterministic, add a
concise comment near the candidate sorting or selection explaining that weight
only breaks priority ties and does not perform weighted load distribution;
otherwise add a TODO at the same location documenting the planned
weighted-routing behavior. Preserve the existing priority, weight, and
provider_id ordering.
src/application/app_state.rs (1)

5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

建议改用规范模块路径导入。

此处仍走 crate::{ai_gateway, amap_service, rate_limit} 兼容层 re-export,而 src/main.rs 已改用 integrations::/api:: 新路径。新代码继续依赖过渡层会让后续移除 lib.rs 中的 pub use 变难。

♻️ 建议改动
-use crate::{ai_gateway::AiGateway, amap_service::AmapService, rate_limit::LoginRateLimiter};
+use crate::{
+    api::rate_limit::LoginRateLimiter,
+    integrations::{ai_gateway::AiGateway, amap_service::AmapService},
+};
🤖 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 `@src/application/app_state.rs` at line 5, 将 app_state 中对 AiGateway、AmapService
和 LoginRateLimiter 的导入改为对应的 integrations:: 或 api:: 规范模块路径,避免继续依赖 crate 的兼容层
re-export。保留现有类型名称和使用方式不变,仅替换 use 声明。
src/application/services/task_service.rs (4)

672-693: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

本模块缺少测试。

volunteer_transition_allowed 状态机、Commander 取消规则、update_task_status 的 rows_affected 并发守卫,以及位置回报的时间窗校验都是关键路径,但本文件没有测试模块(case_service.rs、auth_service.rs 均有)。建议补充覆盖。是否需要我生成一组基于内存 SQLite 的测试用例?

🤖 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 `@src/application/services/task_service.rs` around lines 672 - 693, Add a test
module to task_service.rs covering volunteer_transition_allowed state
transitions, Commander cancellation rules, update_task_status rows_affected
concurrency protection, and location-report time-window validation. Follow the
existing in-memory SQLite testing patterns from case_service.rs and
auth_service.rs, including valid, invalid, and boundary cases.

45-63: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

后台清理任务:建议使用结构化日志并对删除分批。

eprintln! 绕过了应用日志体系,生产环境难以采集与告警;另外 delete_many 无 LIMIT,如果保留期任务曾停摆,一次删除可能长时间持锁并阻塞写入。建议改用 log/tracing 记录错误,并按批(例如每轮上限若干行、循环直到删完)执行删除。

🤖 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 `@src/application/services/task_service.rs` around lines 45 - 63, Update
start_location_report_retention_purger to use the application’s structured
log/tracing facility instead of eprintln! when purge_expired_location_reports
fails. Refactor purge_expired_location_reports to delete expired reports in
bounded batches, looping until no rows remain, and return the total rows
affected while preserving the existing retention filter and error propagation.

600-631: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

分页校验逻辑与 case_service::ValidatedClueTimelineQuery 重复。

offset()、page_size_usize() 与 TryFrom 中的 page/page_size 边界校验在两个服务中几乎逐行相同(src/application/services/case_service.rs Line 513-542)。建议抽出一个共享的 Pagination 类型(携带各自的 MAX_PAGE_SIZE),避免规则后续漂移。

🤖 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 `@src/application/services/task_service.rs` around lines 600 - 631, Extract the
duplicated pagination logic from ValidatedTaskListQuery and
case_service::ValidatedClueTimelineQuery into a shared Pagination type. Have it
own page/page_size validation, offset calculation, and usize conversion while
accepting each service’s maximum page-size limit, then update both TryFrom
implementations to reuse it and preserve their existing validation behavior and
error messages.

205-232: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

任务列表同样是内存分页:先全量加载再截取。

tasks::Entity::find().all(db) 拉取该案件所有任务,随后在内存中按角色过滤并 skip/take。可见性规则(Commander 全量 / Volunteer 仅自己被指派)可下推为 SQL JOIN 条件,从而使用数据库 LIMIT/OFFSET 与 COUNT。

🤖 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 `@src/application/services/task_service.rs` around lines 205 - 232, Update the
task listing flow around tasks::Entity::find and assignments_for_tasks to push
role-based visibility into the database query, using the appropriate assignment
join and Commander/Volunteer conditions. Compute the total with a database COUNT
query and apply query.offset() and query.page_size_usize() as SQL OFFSET/LIMIT
before loading results; preserve due-date and ID ordering and TaskResponse
assignment data.
src/application/services/auth_service.rs (2)

393-425: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

建议补充认证关键分支的测试。

当前仅覆盖注销路径。建议补充:错误密码返回 Unauthorized、会话过期后 authenticate 失败、status != "active" 用户无法登录、upsert_user 重新引导后旧会话被撤销。这些都是权限边界,值得回归保护。

🤖 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 `@src/application/services/auth_service.rs` around lines 393 - 425, Expand the
authentication tests alongside logout_revokes_the_server_side_session to cover
incorrect-password login returning ApiError::Unauthorized, expired sessions
being rejected by authenticate, users whose status is not "active" being unable
to log in, and upsert_user rebootstrap revoking an existing session. Reuse the
existing database, migration, bootstrap, login, authenticate, logout, and
upsert_user helpers and assert each permission boundary explicitly.

124-127: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

每次鉴权都写库更新 last_used_at,会带来明显写放大。

authenticate 在每个受保护请求上都执行一次 UPDATE,SQLite/单写入连接下容易成为吞吐瓶颈与锁竞争源。建议按节流阈值(例如距上次更新超过若干分钟才写)更新。

♻️ 建议的节流思路
-    let session_id = session.id.clone();
-    let mut active = session.into_active_model();
-    active.last_used_at = Set(now());
-    active.update(db).await?;
+    let session_id = session.id.clone();
+    let refreshed_at = now();
+    // 仅在超过节流间隔时写库,避免每个请求一次 UPDATE。
+    if should_refresh_last_used_at(&session.last_used_at, &refreshed_at) {
+        let mut active = session.into_active_model();
+        active.last_used_at = Set(refreshed_at);
+        active.update(db).await?;
+    }
🤖 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 `@src/application/services/auth_service.rs` around lines 124 - 127, 在
authenticate 流程中调整 last_used_at 的更新逻辑:仅当当前时间与会话已有的 last_used_at
相隔超过节流阈值(例如数分钟)时,才通过 active.update(db) 写回数据库;未达到阈值时跳过 UPDATE,同时保留 session_id
提取和鉴权结果行为不变。
src/api/rate_limit.rs (1)

12-15: 🔒 Security & Privacy | 🔵 Trivial

进程内限流在多副本部署下失效。

计数保存在单进程内存中,横向扩容后实际阈值变为 MAX_FAILURES × 副本数,且滚动重启会清空所有计数。若后续需要多实例部署,建议改为共享存储(如 Redis)或在入口网关层做登录限流。

🤖 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 `@src/api/rate_limit.rs` around lines 12 - 15, Update LoginRateLimiter so
attempt counts are stored in shared infrastructure rather than the process-local
attempts Mutex<HashMap<...>>, ensuring limits remain consistent across replicas
and restarts. Use the project’s existing Redis/shared-storage or gateway
rate-limiting mechanism if available, and preserve the current login-threshold
behavior.
src/application/services/case_service.rs (1)

347-375: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

列表接口统一采用「全量加载 + 内存分页」。 共同根因是基于角色的可见性规则没有下推到 SQL,导致必须先取回案件下的全部行、在应用层过滤后再 skip/take,total 也在内存中统计。随着案件生命周期变长,单次请求的内存与 I/O 成本会线性增长,而 page_size 上限(100)完全不起保护作用。

  • src/application/services/case_service.rs#L347-L375:把可见性条件下推为 SQL(Volunteer → status = 'confirmed';Family → status = 'confirmed' OR clue_attributions.submitted_by_user_id = ?),改用数据库 LIMIT/OFFSET + COUNT,并为 clues(case_id, created_at) 建复合索引。
  • src/application/services/task_service.rs#L205-L232:把「Commander 全量 / Volunteer 仅自己被指派」改为与 task_assignments 的 JOIN 过滤条件,同样使用数据库分页与计数。
🤖 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 `@src/application/services/case_service.rs` around lines 347 - 375,
将可见性过滤和分页下推到数据库,避免全量加载后在内存中筛选与统计。对于
src/application/services/case_service.rs:347-375,更新 clue 查询:Volunteer 仅查询 status
为 confirmed 的记录,Family 查询 confirmed 或由当前用户提交归因的记录,并使用数据库 COUNT、LIMIT/OFFSET;同时为
clues(case_id, created_at) 添加复合索引。对于
src/application/services/task_service.rs:205-232,将 Commander 全量与 Volunteer
仅本人被指派的规则通过 task_assignments JOIN 过滤实现,并改用数据库分页和计数。
src/application/services/case_resource_service.rs (1)

105-126: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚖️ Poor tradeoff

附件数量上限存在 TOCTOU,且文件先落盘再入库会遗留孤儿文件。

count 与后续 insert 之间没有互斥,在非序列化隔离级别下并发上传可超过 max_attachments_per_case。另外文件在事务提交前就写入磁盘,进程崩溃时 remove_file_best_effort 无法执行,会残留孤儿文件。

建议:上限改为数据库侧约束或在提交前重新 count 校验;同时增加一个定期扫描存储目录、清理无对应 case_attachments 记录的后台任务(与 task_service 的保留期清理类似)。

🤖 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 `@src/application/services/case_resource_service.rs` around lines 105 - 126,
Update the attachment creation flow around current_count and the subsequent
insert to enforce max_attachments_per_case atomically at the database level, or
revalidate the count immediately before commit so concurrent uploads cannot
exceed the limit. Avoid leaving files written by web::block without a committed
case_attachments record, and add a periodic storage-directory cleanup task,
following task_service’s retention-cleanup pattern, to remove orphaned files.
src/api/routes/cases.rs (1)

102-126: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

上传路径存在多次全量内存拷贝,缺少并发上传的整体内存约束。

单次请求中图片会被完整驻留内存多次:这里的 bytes、store_image_attachment 中的 upload.bytes.to_vec()、normalize_image 解码后的位图、以及重编码输出。并发上传数 × 单张上限即为内存峰值,目前没有任何全局约束。

建议:把 bytes 以 web::Bytes/Arc<[u8]> 传递以消除一次拷贝,并在 main.rs 中配置 PayloadConfig/MultipartFormConfig 与并发上传信号量以限定整体内存占用。

🤖 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 `@src/api/routes/cases.rs` around lines 102 - 126, 调整文件上传链路,避免在 cases 路由与
store_image_attachment 之间通过 to_vec 产生额外全量拷贝,改用 web::Bytes 或 Arc<[u8]>
传递上传内容。并在应用启动配置中设置 PayloadConfig/MultipartFormConfig
的单请求限制,同时为图片上传增加全局并发信号量,确保并发请求的总内存占用受 attachment_max_image_bytes 约束。
src/application/services/intake_session_service.rs (1)

670-817: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

normalize_answers 与 normalize_single_answer 存在重复的校验逻辑。

两者都实现了"取 max_answer_chars/answer_hard_max 较小值为上限 → trim → 校验长度范围"的逻辑,仅错误文案不同。建议抽取共用的私有辅助函数,避免未来只修改一处导致两条提交路径(初始创建 vs 单字段提交)行为分叉。

♻️ 建议抽取共用校验函数
+fn normalize_answer_text(
+    raw: &str,
+    configured_limit: i32,
+    answer_hard_max: usize,
+    error_label: &str,
+) -> Result<String, ApiError> {
+    let answer_limit = usize::try_from(configured_limit)
+        .map_err(|_| ApiError::Internal)?
+        .min(answer_hard_max);
+    let trimmed = raw.trim();
+    if trimmed.is_empty() || trimmed.chars().count() > answer_limit {
+        return Err(ApiError::Validation(format!(
+            "{error_label} must contain between 1 and {answer_limit} characters"
+        )));
+    }
+    Ok(trimmed.to_owned())
+}
🤖 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 `@src/application/services/intake_session_service.rs` around lines 670 - 817,
Extract the shared limit calculation, trimming, and length validation from
normalize_answers and normalize_single_answer into one private helper that
accepts the answer, configured limit, hard maximum, and appropriate
error-context data. Update both functions to call this helper while preserving
their existing validation behavior and error messages, so initial creation and
single-field submission use the same rules.
src/persistence/entities/ai_prompt_templates.rs (1)

6-20: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

移除该持久化实体上的 Serialize
system_instruction 是内部提示词字段,没必要让 Model 直接可序列化;这样更容易在后续被误直接返回。对外输出时改用专用 DTO,只暴露需要的字段。

🤖 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 `@src/persistence/entities/ai_prompt_templates.rs` around lines 6 - 20, Remove
the Serialize derive from the ai_prompt_templates Model entity, leaving its
other derives unchanged. Ensure external responses use a dedicated DTO with only
explicitly exposed fields rather than serializing Model directly.
🤖 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 `@src/api/models.rs`:
- Around line 182-183: 将响应模型中 guidance_mode 和 privacy_notice 的硬编码英文文案抽取为统一常量或
i18n 资源,并在两个出现位置复用同一来源,避免文案漂移;同时确认并调整为面向中文家属用户的本地化隐私告知文本,保持现有响应字段结构不变。
- Around line 746-753: Update the assigned_at field construction in the
surrounding model mapping so it follows the same include_assignee visibility
gate as assigned_volunteer_user_id, hiding the timestamp when include_assignee
is false. Keep the existing assignment timestamp value unchanged when assignee
data is included.

In `@src/api/rate_limit.rs`:
- Around line 39-48: 在 RateLimiter 的 record_failure 方法中为 attempts
映射增加明确的容量上限:清理过期条目后,若新 key 不存在且已达到上限,应拒绝插入并返回现有的合适错误,保持已有 key
的失败计数逻辑不变。使用集中定义的容量常量,并确保并发锁定下不会绕过该限制。

In `@src/api/routes/auth.rs`:
- Around line 29-32: Update the account-level limiter key in the authentication
route around client_key and account_key so it is derived only from the
normalized, trimmed, lowercased request email, without including client or IP
data. Keep client_key unchanged for source-level limiting and continue checking
both limiters.

In `@src/application/services/case_resource_service.rs`:
- Around line 362-381: Update safe_filename to preserve Unicode alphanumeric
characters, including Chinese names, while still removing path separators and
control characters; avoid rejecting valid filenames solely because they contain
no ASCII characters. Also update download_attachment to emit non-ASCII filenames
using RFC 5987 filename*=UTF-8'' percent-encoding in Content-Disposition.
- Around line 335-344: Replace the `image::load_from_memory_with_format` call
with an `image::ImageReader` configured with `Limits` before decoding, setting
`max_image_width`, `max_image_height`, and `max_alloc` to enforce the existing
8,000-pixel and 20M-pixel memory limits pre-decode. Preserve the current
`ApiError::Validation` mapping and post-decode dimension validation in the
surrounding image-processing flow.

In `@src/application/services/case_service.rs`:
- Around line 424-445: 在更新 active 之前补充反向校验:当 next_status 不是 duplicate 或
conflicting 时,若 related_clue_id 存在则必须同时提供 relationship_type,否则返回
ApiError::Validation。确保 active.related_clue_id 与 active.relationship_type
不会写入缺少关系类型的关联。
- Around line 179-190: 在 `case_service` 的状态更新流程中对齐
`task_service::update_task_status`:不要直接调用 `active.update(&transaction)`,改用带有主键和
`previous_status` 条件的 `update_many` 条件更新,并检查 `rows_affected` 必须为
1;否则返回并发冲突错误,避免覆盖其他请求已提交的状态。按需要引入
`sea_orm::sea_query::Expr`,保持现有状态转移校验和更新时间更新逻辑。

In `@src/application/services/intake_session_service.rs`:
- Around line 125-277: 在 submit_intake_answer_with_map 中读取 intake_sessions
会话记录的查询链上加入排他行锁,确保事务内基于 answers_json、状态及结构化数据计算的整行更新不会并发覆盖;参照同文件
confirm_intake_session 的 lock_exclusive 用法,保持后续校验、答案写入和聚合更新逻辑不变。

In `@src/integrations/ai_gateway.rs`:
- Line 240: Replace the unsalted Sha256 computation assigned to input_hash in
the audit-record construction with HMAC-SHA256 using a runtime-injected secret
from an environment variable, managed like the provider credentials. Reuse the
configured key consistently for hashing request.input and preserve the existing
hex-encoded audit value without storing the original content.
- Around line 344-363: Update the AnthropicMessages request construction to omit
the system field when request.system_instruction is None or empty, while
preserving the existing value when present. Follow the conditional-field pattern
used by the Gemini protocol and add coverage for both None and empty-string
system instructions.

In `@src/integrations/amap_service.rs`:
- Around line 87-91: Update RouteUnavailableReason in
src/integrations/amap_service.rs#L31-L39 by removing the unused UnsupportedMode
variant and adding InvalidRequest. In the coordinate validation branch at
src/integrations/amap_service.rs#L87-L91, return InvalidRequest instead of
InvalidResponse so invalid caller coordinates are classified as request
failures.

In `@src/persistence/entities/users.rs`:
- Around line 4-14: Update the users::Model Debug implementation so
password_hash is never exposed when instances are logged. Remove the derived
Debug and provide a manual redacted Debug implementation, or otherwise ensure
Debug omits or masks password_hash while preserving the existing Clone,
PartialEq, DeriveEntityModel, Serialize, and serde behavior.

---

Nitpick comments:
In `@src/api/rate_limit.rs`:
- Around line 12-15: Update LoginRateLimiter so attempt counts are stored in
shared infrastructure rather than the process-local attempts
Mutex<HashMap<...>>, ensuring limits remain consistent across replicas and
restarts. Use the project’s existing Redis/shared-storage or gateway
rate-limiting mechanism if available, and preserve the current login-threshold
behavior.

In `@src/api/routes/cases.rs`:
- Around line 102-126: 调整文件上传链路,避免在 cases 路由与 store_image_attachment 之间通过 to_vec
产生额外全量拷贝,改用 web::Bytes 或 Arc<[u8]> 传递上传内容。并在应用启动配置中设置
PayloadConfig/MultipartFormConfig 的单请求限制,同时为图片上传增加全局并发信号量,确保并发请求的总内存占用受
attachment_max_image_bytes 约束。

In `@src/application/app_state.rs`:
- Line 5: 将 app_state 中对 AiGateway、AmapService 和 LoginRateLimiter 的导入改为对应的
integrations:: 或 api:: 规范模块路径,避免继续依赖 crate 的兼容层 re-export。保留现有类型名称和使用方式不变,仅替换
use 声明。

In `@src/application/services/auth_service.rs`:
- Around line 393-425: Expand the authentication tests alongside
logout_revokes_the_server_side_session to cover incorrect-password login
returning ApiError::Unauthorized, expired sessions being rejected by
authenticate, users whose status is not "active" being unable to log in, and
upsert_user rebootstrap revoking an existing session. Reuse the existing
database, migration, bootstrap, login, authenticate, logout, and upsert_user
helpers and assert each permission boundary explicitly.
- Around line 124-127: 在 authenticate 流程中调整 last_used_at 的更新逻辑:仅当当前时间与会话已有的
last_used_at 相隔超过节流阈值(例如数分钟)时,才通过 active.update(db) 写回数据库;未达到阈值时跳过 UPDATE,同时保留
session_id 提取和鉴权结果行为不变。

In `@src/application/services/case_resource_service.rs`:
- Around line 105-126: Update the attachment creation flow around current_count
and the subsequent insert to enforce max_attachments_per_case atomically at the
database level, or revalidate the count immediately before commit so concurrent
uploads cannot exceed the limit. Avoid leaving files written by web::block
without a committed case_attachments record, and add a periodic
storage-directory cleanup task, following task_service’s retention-cleanup
pattern, to remove orphaned files.

In `@src/application/services/case_service.rs`:
- Around line 347-375: 将可见性过滤和分页下推到数据库,避免全量加载后在内存中筛选与统计。对于
src/application/services/case_service.rs:347-375,更新 clue 查询:Volunteer 仅查询 status
为 confirmed 的记录,Family 查询 confirmed 或由当前用户提交归因的记录,并使用数据库 COUNT、LIMIT/OFFSET;同时为
clues(case_id, created_at) 添加复合索引。对于
src/application/services/task_service.rs:205-232,将 Commander 全量与 Volunteer
仅本人被指派的规则通过 task_assignments JOIN 过滤实现,并改用数据库分页和计数。

In `@src/application/services/intake_session_service.rs`:
- Around line 670-817: Extract the shared limit calculation, trimming, and
length validation from normalize_answers and normalize_single_answer into one
private helper that accepts the answer, configured limit, hard maximum, and
appropriate error-context data. Update both functions to call this helper while
preserving their existing validation behavior and error messages, so initial
creation and single-field submission use the same rules.

In `@src/application/services/task_service.rs`:
- Around line 672-693: Add a test module to task_service.rs covering
volunteer_transition_allowed state transitions, Commander cancellation rules,
update_task_status rows_affected concurrency protection, and location-report
time-window validation. Follow the existing in-memory SQLite testing patterns
from case_service.rs and auth_service.rs, including valid, invalid, and boundary
cases.
- Around line 45-63: Update start_location_report_retention_purger to use the
application’s structured log/tracing facility instead of eprintln! when
purge_expired_location_reports fails. Refactor purge_expired_location_reports to
delete expired reports in bounded batches, looping until no rows remain, and
return the total rows affected while preserving the existing retention filter
and error propagation.
- Around line 600-631: Extract the duplicated pagination logic from
ValidatedTaskListQuery and case_service::ValidatedClueTimelineQuery into a
shared Pagination type. Have it own page/page_size validation, offset
calculation, and usize conversion while accepting each service’s maximum
page-size limit, then update both TryFrom implementations to reuse it and
preserve their existing validation behavior and error messages.
- Around line 205-232: Update the task listing flow around tasks::Entity::find
and assignments_for_tasks to push role-based visibility into the database query,
using the appropriate assignment join and Commander/Volunteer conditions.
Compute the total with a database COUNT query and apply query.offset() and
query.page_size_usize() as SQL OFFSET/LIMIT before loading results; preserve
due-date and ID ordering and TaskResponse assignment data.

In `@src/integrations/ai_gateway.rs`:
- Around line 427-466: Clarify the intended semantics of weight in route: if
routing is intentionally deterministic, add a concise comment near the candidate
sorting or selection explaining that weight only breaks priority ties and does
not perform weighted load distribution; otherwise add a TODO at the same
location documenting the planned weighted-routing behavior. Preserve the
existing priority, weight, and provider_id ordering.

In `@src/integrations/amap_service.rs`:
- Around line 31-39: 删除 RouteUnavailableReason::UnsupportedMode
变体,并清理所有相关的匹配分支、引用及调用方处理逻辑;保持 estimate_route 对现有 RouteMode 变体的完整匹配和其他不可用原因行为不变。

In `@src/persistence/entities/ai_prompt_templates.rs`:
- Around line 6-20: Remove the Serialize derive from the ai_prompt_templates
Model entity, leaving its other derives unchanged. Ensure external responses use
a dedicated DTO with only explicitly exposed fields rather than serializing
Model directly.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c6d9a438-2e14-4b58-ba09-8e6b9439cfdf

📥 Commits

Reviewing files that changed from the base of the PR and between 917b113 and 209519d.

📒 Files selected for processing (52)
  • docs/ARCHITECTURE.md
  • src/api/auth.rs
  • src/api/error.rs
  • src/api/mod.rs
  • src/api/models.rs
  • src/api/rate_limit.rs
  • src/api/routes/auth.rs
  • src/api/routes/cases.rs
  • src/api/routes/clues.rs
  • src/api/routes/health.rs
  • src/api/routes/intake_sessions.rs
  • src/api/routes/mod.rs
  • src/api/routes/tasks.rs
  • src/application/app_state.rs
  • src/application/mod.rs
  • src/application/services/auth_service.rs
  • src/application/services/case_resource_service.rs
  • src/application/services/case_service.rs
  • src/application/services/intake_session_service.rs
  • src/application/services/mod.rs
  • src/application/services/task_service.rs
  • src/config.rs
  • src/domain/intake_assessment.rs
  • src/domain/mod.rs
  • src/domain/roles.rs
  • src/integrations/ai_gateway.rs
  • src/integrations/amap_service.rs
  • src/integrations/mod.rs
  • src/lib.rs
  • src/main.rs
  • src/persistence/entities/ai_prompt_templates.rs
  • src/persistence/entities/audit_events.rs
  • src/persistence/entities/auth_sessions.rs
  • src/persistence/entities/case_attachments.rs
  • src/persistence/entities/case_memberships.rs
  • src/persistence/entities/case_places.rs
  • src/persistence/entities/cases.rs
  • src/persistence/entities/clue_attachment_links.rs
  • src/persistence/entities/clue_attributions.rs
  • src/persistence/entities/clues.rs
  • src/persistence/entities/elder_profiles.rs
  • src/persistence/entities/intake_answer_revisions.rs
  • src/persistence/entities/intake_question_definitions.rs
  • src/persistence/entities/intake_session_answers.rs
  • src/persistence/entities/intake_sessions.rs
  • src/persistence/entities/mod.rs
  • src/persistence/entities/task_assignments.rs
  • src/persistence/entities/task_location_reports.rs
  • src/persistence/entities/tasks.rs
  • src/persistence/entities/user_global_capabilities.rs
  • src/persistence/entities/users.rs
  • src/persistence/mod.rs
💤 Files with no reviewable changes (1)
  • docs/ARCHITECTURE.md
🛑 Comments failed to post (13)
src/api/models.rs (2)

182-183: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

privacy_notice / guidance_mode 为硬编码英文长文案,且两处近似重复。

这些是直接展示给(中文)家属用户的隐私告知文本,硬编码在响应模型里既无法本地化,也容易在两处漂移不一致。建议抽为常量(或走 i18n 资源),并确认面向终端用户的语言。

Also applies to: 328-329

🤖 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 `@src/api/models.rs` around lines 182 - 183, 将响应模型中 guidance_mode 和
privacy_notice 的硬编码英文文案抽取为统一常量或 i18n
资源,并在两个出现位置复用同一来源,避免文案漂移;同时确认并调整为面向中文家属用户的本地化隐私告知文本,保持现有响应字段结构不变。

746-753: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

assigned_at 未受 include_assignee 控制,与相邻字段的可见性策略不一致。

assigned_volunteer_user_id 在 include_assignee == false 时被隐藏,但紧邻的 assigned_at 无条件返回。要么两者一起隐藏,要么在注释中说明 assigned_at 是有意公开的。

🐛 建议对齐
-            assigned_at: assignment.map(|value| value.assigned_at),
+            assigned_at: include_assignee
+                .then(|| assignment.map(|value| value.assigned_at))
+                .flatten(),
📝 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.

            assigned_volunteer_user_id: include_assignee
                .then(|| {
                    assignment
                        .as_ref()
                        .map(|value| value.volunteer_user_id.clone())
                })
                .flatten(),
            assigned_at: include_assignee
                .then(|| assignment.map(|value| value.assigned_at))
                .flatten(),
🤖 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 `@src/api/models.rs` around lines 746 - 753, Update the assigned_at field
construction in the surrounding model mapping so it follows the same
include_assignee visibility gate as assigned_volunteer_user_id, hiding the
timestamp when include_assignee is false. Keep the existing assignment timestamp
value unchanged when assignee data is included.
src/api/rate_limit.rs (1)

39-48: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

限流表无容量上限,攻击者可用大量不同 key 造成内存耗尽。

prune_expired 只按时间清理,条目数完全由请求量决定。若 key 包含邮箱(测试中即 "127.0.0.1:family@demo.invalid"),攻击者只需在 15 分钟窗口内用海量随机邮箱发起失败登录,就能让 attempts 无界膨胀直至 OOM;限流器本身反而成了放大器。

建议在 record_failure 中对 map 设置容量上限(超过上限时拒绝插入新 key 或整体降级为按 IP 限流),并考虑把 key 收敛为仅 IP 维度。

🛡️ 建议的容量上限
+const MAX_TRACKED_KEYS: usize = 100_000;
+
     pub fn record_failure(&self, key: String) -> Result<(), ApiError> {
         let mut attempts = self.attempts.lock().map_err(|_| ApiError::Internal)?;
         prune_expired(&mut attempts);
+        if attempts.len() >= MAX_TRACKED_KEYS && !attempts.contains_key(&key) {
+            // 表已满:不再追踪新 key,避免内存被放大攻击撑爆。
+            return Ok(());
+        }
         let attempt = attempts.entry(key).or_insert(AttemptWindow {
             started_at: Instant::now(),
             failures: 0,
         });
         attempt.failures += 1;
         Ok(())
     }

以上依据 path instructions 对 src/** 的要求:“Review Rust/Actix code for authentication, authorization, privacy, error handling, concurrency...”。

Also applies to: 59-61

🤖 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 `@src/api/rate_limit.rs` around lines 39 - 48, 在 RateLimiter 的 record_failure
方法中为 attempts 映射增加明确的容量上限:清理过期条目后,若新 key 不存在且已达到上限,应拒绝插入并返回现有的合适错误,保持已有 key
的失败计数逻辑不变。使用集中定义的容量常量,并确保并发锁定下不会绕过该限制。

Source: Path instructions

src/api/routes/auth.rs (1)

29-32: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

账户限流键不应包含客户端 IP。

account_key 同时包含 IP 和邮箱,因此与 client_key 一样只能限制单个来源。攻击者轮换 IP 即可绕过针对同一账户的密码猜测限制。账户键应仅基于规范化邮箱,IP 键保留用于来源级限流。

建议修改
 let client_key = format!("client:{client}");
-let account_key = format!("account:{client}:{}", request.email.trim().to_lowercase());
+let account_key = format!("account:{}", request.email.trim().to_lowercase());

As per path instructions, src/** requires authentication-risk review.

📝 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.

    let client_key = format!("client:{client}");
    let account_key = format!("account:{}", request.email.trim().to_lowercase());
    state.login_limiter.check(&client_key)?;
    state.login_limiter.check(&account_key)?;
🤖 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 `@src/api/routes/auth.rs` around lines 29 - 32, Update the account-level
limiter key in the authentication route around client_key and account_key so it
is derived only from the normalized, trimmed, lowercased request email, without
including client or IP data. Keep client_key unchanged for source-level limiting
and continue checking both limiters.

Source: Path instructions

src/application/services/case_resource_service.rs (2)

335-344: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

image crate 0.25 ImageReader limits max_image_width max_alloc

💡 Result:

In the image crate version 0.25, decoding limits are managed via the Limits struct, which can be configured on an ImageReader [1][2]. The Limits struct defines several parameters to control memory usage and prevent potential denial-of-service vulnerabilities during image decoding [2]. The relevant fields for your query are: max_image_width: An Option field that sets the maximum allowed image width [3][2]. This is a strict limit; if an image exceeds this width, the decoder will fail [2]. By default, there is no limit [3]. max_alloc: An Option field that sets the maximum allowed sum of bytes allocated by the decoder at any one time (excluding allocator overhead) [4][3]. The bytes required to store the output image are included in this count [4][2]. This is a non-strict limit by default, meaning some decoders may choose to ignore it, although exceeding it is still considered a bug [2]. The default value for max_alloc is 512 MiB [4][3]. To apply these limits to an ImageReader, you can use the limits method to provide a customized Limits instance: let mut reader = ImageReader::open("image.png")?; let mut limits = Limits::default; limits.max_image_width = Some(1024); limits.max_alloc = Some(1024 * 1024 * 64); // 64 MiB reader.limits(limits); let img = reader.decode?; Additionally, you can use the no_limits method on an ImageReader to disable all decoding limits if needed [1][5]. Note that max_image_height is also available as a strict limit similar to max_image_width [3][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and the image crate version/API usage in the repo.
printf '\n== file outline ==\n'
ast-grep outline src/application/services/case_resource_service.rs --view expanded || true

printf '\n== relevant lines around 335-344 ==\n'
sed -n '320,360p' src/application/services/case_resource_service.rs

printf '\n== image dependency versions ==\n'
rg -n '^\s*image\s*=' Cargo.toml Cargo.lock **/Cargo.toml **/Cargo.lock 2>/dev/null || true

printf '\n== any ImageReader/Limits usage in repo ==\n'
rg -n 'ImageReader|Limits|max_image_width|max_image_height|max_alloc|load_from_memory_with_format' src Cargo.toml Cargo.lock 2>/dev/null || true

Repository: AG-Angui/Angui

Length of output: 3100


在解码前加尺寸/内存上限,避免图片炸弹触发 OOM。
load_from_memory_with_format 会先把图像完整解码到内存,之后才检查 8000×8000 / 20M 像素上限;压缩后很小但像素数很大的 PNG/JPEG 仍可能在这里分配过多内存。改用 image::ImageReader 配合 Limits(max_image_width / max_image_height / max_alloc)在解码前拒绝超限输入。

🤖 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 `@src/application/services/case_resource_service.rs` around lines 335 - 344,
Replace the `image::load_from_memory_with_format` call with an
`image::ImageReader` configured with `Limits` before decoding, setting
`max_image_width`, `max_image_height`, and `max_alloc` to enforce the existing
8,000-pixel and 20M-pixel memory limits pre-decode. Preserve the current
`ApiError::Validation` mapping and post-decode dimension validation in the
surrounding image-processing flow.

Source: Path instructions


362-381: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

纯中文文件名会被过滤为空并直接报错,属于 i18n 缺陷。

filter_map 只保留 ASCII 字母数字与少量符号,照片.jpg 之类的文件名会被清空并触发 filename is invalid。本项目面向中文用户,这会阻断正常上传。建议保留 char::is_alphanumeric()(覆盖 Unicode 字母)并仅剔除路径分隔符与控制字符,或在清洗为空时回退到默认名而非报错。

🐛 建议修复
     let normalized: String = stem
         .chars()
         .filter_map(|character| match character {
-            'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' => Some(character),
             ' ' => Some('_'),
-            _ => None,
+            '.' | '-' | '_' => Some(character),
+            _ if character.is_alphanumeric() && !character.is_control() => Some(character),
+            _ => None,
         })
         .take(100)
         .collect();
-    if normalized.is_empty() {
-        return Err(ApiError::Validation("filename is invalid".to_owned()));
-    }
+    let normalized = if normalized.is_empty() {
+        "image".to_owned()
+    } else {
+        normalized
+    };

注意:放开非 ASCII 后,download_attachment 中 Content-Disposition: attachment; filename="..." 需改用 filename*=UTF-8''<percent-encoded> 形式以符合 RFC 5987。

📝 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.

fn safe_filename(filename: &str, extension: &str) -> Result<String, ApiError> {
    let name = Path::new(filename)
        .file_name()
        .and_then(|value| value.to_str())
        .unwrap_or("image");
    let stem = name.rsplit_once('.').map_or(name, |(stem, _)| stem);
    let normalized: String = stem
        .chars()
        .filter_map(|character| match character {
            ' ' => Some('_'),
            '.' | '-' | '_' => Some(character),
            _ if character.is_alphanumeric() && !character.is_control() => Some(character),
            _ => None,
        })
        .take(100)
        .collect();
    let normalized = if normalized.is_empty() {
        "image".to_owned()
    } else {
        normalized
    };
    Ok(format!("{normalized}.{extension}"))
}
🤖 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 `@src/application/services/case_resource_service.rs` around lines 362 - 381,
Update safe_filename to preserve Unicode alphanumeric characters, including
Chinese names, while still removing path separators and control characters;
avoid rejecting valid filenames solely because they contain no ASCII characters.
Also update download_attachment to emit non-ASCII filenames using RFC 5987
filename*=UTF-8'' percent-encoding in Content-Disposition.
src/application/services/case_service.rs (2)

179-190: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

案件状态更新缺少乐观并发守卫,与 task_service 的做法不一致。

这里读取 case_model.status 校验转移后直接整行更新,两个并发的 commander 请求可能都通过校验并双双写入(丢失更新,例如 closed 被覆盖回 active)。task_service::update_task_status(Line 344-355)已采用 update_many().filter(Status.eq(previous_status)) + rows_affected != 1 的守卫,建议在此对齐。

🔒️ 建议的条件更新
-    let previous_status = case_model.status.clone();
-    let mut active = case_model.into_active_model();
-    active.status = Set(next_status.clone());
-    active.updated_at = Set(now());
-    active.update(&transaction).await?;
+    let previous_status = case_model.status.clone();
+    let update_result = cases::Entity::update_many()
+        .col_expr(cases::Column::Status, Expr::value(next_status.clone()))
+        .col_expr(cases::Column::UpdatedAt, Expr::value(now()))
+        .filter(cases::Column::Id.eq(case_id))
+        .filter(cases::Column::Status.eq(&previous_status))
+        .exec(&transaction)
+        .await?;
+    if update_result.rows_affected != 1 {
+        return Err(ApiError::Conflict(
+            "case status changed before this transition could be applied".to_owned(),
+        ));
+    }

需额外引入 use sea_orm::sea_query::Expr;。

以上依据 path instructions 对 src/** 的要求:“Review Rust/Actix code for ... concurrency ... and data-loss risks.”

📝 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.

    if !case_transition_allowed(&case_model.status, &next_status) {
        return Err(ApiError::Conflict(format!(
            "case status cannot change from {:?} to {:?}",
            case_model.status, next_status
        )));
    }

    let previous_status = case_model.status.clone();
    let update_result = cases::Entity::update_many()
        .col_expr(cases::Column::Status, Expr::value(next_status.clone()))
        .col_expr(cases::Column::UpdatedAt, Expr::value(now()))
        .filter(cases::Column::Id.eq(case_id))
        .filter(cases::Column::Status.eq(&previous_status))
        .exec(&transaction)
        .await?;
    if update_result.rows_affected != 1 {
        return Err(ApiError::Conflict(
            "case status changed before this transition could be applied".to_owned(),
        ));
    }
🤖 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 `@src/application/services/case_service.rs` around lines 179 - 190, 在
`case_service` 的状态更新流程中对齐 `task_service::update_task_status`:不要直接调用
`active.update(&transaction)`,改用带有主键和 `previous_status` 条件的 `update_many`
条件更新,并检查 `rows_affected` 必须为 1;否则返回并发冲突错误,避免覆盖其他请求已提交的状态。按需要引入
`sea_orm::sea_query::Expr`,保持现有状态转移校验和更新时间更新逻辑。

Source: Path instructions


424-445: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

related_clue_id 在非 duplicate/conflicting 状态下仍会被写入,且可能没有对应的 relationship_type。

约束只做了单向:duplicate/conflicting 必须带关联;但 status = "confirmed" 且只传 related_clue_id、不传 relationship_type 时,第 443 行仍会写入关联,产生 relationship_type IS NULL 的悬空关系。建议补上反向约束。

🐛 建议补充校验
     if matches!(next_status.as_str(), "duplicate" | "conflicting")
         && trim_optional(request.relationship_type.clone()).is_none()
     {
         return Err(ApiError::Validation(
             "duplicate and conflicting reviews require relationship_type".to_owned(),
         ));
     }
+    if !matches!(next_status.as_str(), "duplicate" | "conflicting") && related_clue_id.is_some() {
+        return Err(ApiError::Validation(
+            "related_clue_id is only allowed for duplicate or conflicting reviews".to_owned(),
+        ));
+    }
📝 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.

    if matches!(next_status.as_str(), "duplicate" | "conflicting") && related_clue_id.is_none() {
        return Err(ApiError::Validation(
            "duplicate and conflicting reviews require related_clue_id".to_owned(),
        ));
    }
    if matches!(next_status.as_str(), "duplicate" | "conflicting")
        && trim_optional(request.relationship_type.clone()).is_none()
    {
        return Err(ApiError::Validation(
            "duplicate and conflicting reviews require relationship_type".to_owned(),
        ));
    }
    if !matches!(next_status.as_str(), "duplicate" | "conflicting") && related_clue_id.is_some() {
        return Err(ApiError::Validation(
            "related_clue_id is only allowed for duplicate or conflicting reviews".to_owned(),
        ));
    }
    let mut active = clue_model.into_active_model();
    active.status = Set(next_status.clone());
    if next_status == "confirmed" {
        active.confirmed_at = Set(Some(now()));
    } else {
        active.confirmed_at = Set(None);
    }
    active.related_clue_id = Set(related_clue_id.clone());
    let relationship_type = trim_optional(request.relationship_type);
    active.relationship_type = Set(relationship_type.clone());
🤖 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 `@src/application/services/case_service.rs` around lines 424 - 445, 在更新 active
之前补充反向校验:当 next_status 不是 duplicate 或 conflicting 时,若 related_clue_id 存在则必须同时提供
relationship_type,否则返回 ApiError::Validation。确保 active.related_clue_id 与
active.relationship_type 不会写入缺少关系类型的关联。
src/application/services/intake_session_service.rs (1)

125-277: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

submit_intake_answer_with_map 缺少行锁,存在并发丢失更新风险。

第 126-129 行读取 intake_sessions 行时未加锁,而第 243-252 行随后对整行做覆盖式更新(answers_json/status/structured_answers_json/assessment_json)。若同一会话的两个不同字段在短时间内并发提交(例如前端自动保存多个表单字段),两个事务都会基于各自读到的旧快照计算完整的聚合 JSON 并写回,后提交的事务会覆盖先提交事务写入的字段变更,造成聚合状态(answers_json/phase/missing_fields)悄然丢失,即便单条答案已正确落库到 intake_session_answers。

对比同文件的 confirm_intake_session(第 400-404 行)对同一张表的读取已显式使用 .lock_exclusive(),此处应保持一致处理。

🔒 建议为会话读取加排他锁
     let transaction = db.begin().await?;
-    let session = intake_sessions::Entity::find_by_id(session_id)
-        .one(&transaction)
-        .await?
-        .ok_or_else(|| ApiError::NotFound("intake session was not found".to_owned()))?;
+    let session = intake_sessions::Entity::find_by_id(session_id)
+        .lock_exclusive()
+        .one(&transaction)
+        .await?
+        .ok_or_else(|| ApiError::NotFound("intake session was not found".to_owned()))?;
📝 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.

    let transaction = db.begin().await?;
    let session = intake_sessions::Entity::find_by_id(session_id)
        .lock_exclusive()
        .one(&transaction)
        .await?
        .ok_or_else(|| ApiError::NotFound("intake session was not found".to_owned()))?;
🤖 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 `@src/application/services/intake_session_service.rs` around lines 125 - 277, 在
submit_intake_answer_with_map 中读取 intake_sessions 会话记录的查询链上加入排他行锁,确保事务内基于
answers_json、状态及结构化数据计算的整行更新不会并发覆盖;参照同文件 confirm_intake_session 的 lock_exclusive
用法,保持后续校验、答案写入和聚合更新逻辑不变。
src/integrations/ai_gateway.rs (2)

240-240: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

无盐 SHA256 对低熵输入可被反推。

input 是摄入回答文本,可能包含姓名、手机号、住址等低熵 PII。未加密钥的 SHA256 摘要写入审计表后,攻击者(含具备审计表读权限的内部人员)可通过字典/枚举还原原文,这削弱了此处“审计不含原始内容”的设计意图。建议改用带运行时密钥的 HMAC-SHA256(密钥从环境变量注入,与 provider 凭据同等管理)。

🔒 建议方向
-            input_hash: hex::encode(Sha256::digest(request.input.as_bytes())),
+            // 使用运行时注入的 pepper 做 HMAC,避免低熵输入被字典还原
+            input_hash: hex::encode(hmac_sha256(pepper, request.input.as_bytes())),
🤖 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 `@src/integrations/ai_gateway.rs` at line 240, Replace the unsalted Sha256
computation assigned to input_hash in the audit-record construction with
HMAC-SHA256 using a runtime-injected secret from an environment variable,
managed like the provider credentials. Reuse the configured key consistently for
hashing request.input and preserve the existing hex-encoded audit value without
storing the original content.

344-363: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Anthropic Messages API empty string system parameter accepted or 400 error

💡 Result:

The Anthropic Messages API does not support an empty string as a value for the top-level system parameter, which often triggers an invalid_request_error (HTTP 400) [1][2][3]. Official documentation and common implementation patterns indicate that while the system parameter is optional, if it is provided, it must contain valid content (a non-empty string or an array of text blocks) [4][5][6][7]. Many third-party libraries (such as LangChain) handle this by explicitly omitting the parameter entirely if the content is empty, or replacing an empty string with a non-empty placeholder like a space to avoid API validation errors [1]. It is important to distinguish this from two other common sources of 400 errors related to "system" prompts: 1. Attempting to use "role": "system" inside the messages array: The Messages API only permits "user" and "assistant" roles within the messages array [8][2][9]. System instructions must be provided via the top-level system parameter [4][5]. 2. Mid-conversation system messages: On specific models (e.g., Claude Fable 5, Mythos, and Opus 4.8+), you can include messages with "role": "system" after a user turn, but this is a distinct feature from the top-level system parameter and is subject to strict placement rules [10]. If you are receiving a 400 error, ensure you are not passing an empty string to the top-level system parameter and confirm that you are not incorrectly placing system instructions inside the messages list [2][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect relevant locations in the target file and nearby tests.
file="src/integrations/ai_gateway.rs"

echo "== line count =="
wc -l "$file"

echo "== outline =="
ast-grep outline "$file" --view expanded || true

echo "== relevant slices =="
sed -n '300,390p' "$file"
echo
sed -n '730,780p' "$file"

Repository: AG-Angui/Angui

Length of output: 11195


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== system_instruction references =="
rg -n "system_instruction" src | sed -n '1,200p'

echo
echo "== request fixture and any request builders around it =="
sed -n '630,710p' src/integrations/ai_gateway.rs

echo
echo "== any Anthropic-specific tests near protocol adapters =="
sed -n '725,790p' src/integrations/ai_gateway.rs

Repository: AG-Angui/Angui

Length of output: 7141


Anthropic 的 system 为空时应省略字段 当前 request.system_instruction 为 None 时会落成空字符串并写入 "system": "",这会让部分 Anthropic Messages 请求直接返回 400;改成像 Gemini 一样在为空时不发送该字段,并补一条 None/空串用例。

🤖 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 `@src/integrations/ai_gateway.rs` around lines 344 - 363, Update the
AnthropicMessages request construction to omit the system field when
request.system_instruction is None or empty, while preserving the existing value
when present. Follow the conditional-field pattern used by the Gemini protocol
and add coverage for both None and empty-string system instructions.
src/integrations/amap_service.rs (1)

87-91: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

RouteUnavailableReason 的取值未与实际失败语义对齐。 枚举缺少“调用方入参非法”这一类别,因此输入侧坐标校验失败被错报为 InvalidResponse;与此同时 UnsupportedMode 因 RouteMode 已被穷尽匹配而永不产生,成为死变体。

  • src/integrations/amap_service.rs#L87-L91:坐标校验失败改为返回新的 RouteUnavailableReason::InvalidRequest,避免把入参错误统计成上游故障。
  • src/integrations/amap_service.rs#L31-L39:删除永不构造的 UnsupportedMode,并新增 InvalidRequest 变体。
📍 Affects 1 file
  • src/integrations/amap_service.rs#L87-L91 (this comment)
  • src/integrations/amap_service.rs#L31-L39
🤖 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 `@src/integrations/amap_service.rs` around lines 87 - 91, Update
RouteUnavailableReason in src/integrations/amap_service.rs#L31-L39 by removing
the unused UnsupportedMode variant and adding InvalidRequest. In the coordinate
validation branch at src/integrations/amap_service.rs#L87-L91, return
InvalidRequest instead of InvalidResponse so invalid caller coordinates are
classified as request failures.
src/persistence/entities/users.rs (1)

4-14: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

避免通过 Debug 泄露密码哈希。

#[serde(skip_serializing)] 不会影响派生的 Debug;记录 users::Model 时仍会输出 password_hash。移除派生 Debug,或实现会脱敏该字段的手写 Debug。

🤖 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 `@src/persistence/entities/users.rs` around lines 4 - 14, Update the
users::Model Debug implementation so password_hash is never exposed when
instances are logged. Remove the derived Debug and provide a manual redacted
Debug implementation, or otherwise ensure Debug omits or masks password_hash
while preserving the existing Clone, PartialEq, DeriveEntityModel, Serialize,
and serde behavior.

Source: Path instructions

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