Skip to content
Clone

feat(repo): add repo activity feature (usecase, impls, handler list, export view)

zhenyi committed
parent bdef01a commit a071d4a Browse files

5 files changed

+202 −0

File tree 5
Added lib/export/repo/activity.rs View lib/export/repo/activity.rs
@@ -0,0 +1,40 @@
1//! 仓库活动馈送:聚合 PR / Release 的时间线条目(commits 待 lib/git rewire 后接入)
2use serde::{Deserialize, Serialize};
3use utoipa::{IntoParams, ToSchema};
4
5use crate::user::basic::UserBasicInfo;
6
7/// 活动条目所属来源
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
9#[serde(rename_all = "lowercase")]
10pub enum RepoActivityKind {
11 Commit,
12 Pr,
13 Release,
14}
15
16/// 单条仓库活动
17#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
18pub struct RepoActivity {
19 pub kind: RepoActivityKind,
20 pub sha: Option<String>,
21 pub number: Option<i64>,
22 pub title: String,
23 pub author: Option<UserBasicInfo>,
24 pub created_at: String,
25}
26
27/// 仓库活动分页结果
28#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
29pub struct RepoActivityPage {
30 pub items: Vec<RepoActivity>,
31 pub total: i64,
32}
33
34/// 仓库活动列表查询
35#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, IntoParams)]
36#[into_params(parameter_in = Query)]
37pub struct ListActivityQuery {
38 pub page: Option<u64>,
39 pub limit: Option<u64>,
40}
No newline at end of file
Added lib/repo/features/activity/impls.rs View lib/repo/features/activity/impls.rs
@@ -0,0 +1,18 @@
1use uuid::Uuid;
2
3use crate::error::RepoResult;
4use crate::features::RepoService;
5
6use super::usecase;
7use super::{ListActivityQuery, RepoActivityPage};
8
9impl RepoService {
10 pub async fn list_activity(
11 &self,
12 actor: Uuid,
13 repo_id: Uuid,
14 q: ListActivityQuery,
15 ) -> RepoResult<RepoActivityPage> {
16 usecase::list_activity(self, actor, repo_id, q).await
17 }
18}
No newline at end of file
Added lib/repo/features/activity/mod.rs View lib/repo/features/activity/mod.rs
@@ -0,0 +1,4 @@
1pub use export::repo::activity::*;
2
3pub mod impls;
4pub mod usecase;
No newline at end of file
Added lib/repo/features/activity/usecase.rs View lib/repo/features/activity/usecase.rs
@@ -0,0 +1,106 @@
1use db::conn::sea_orm::*;
2use db::pull_request::pull_requests as pr_entity;
3use db::repo::repo_release as release_entity;
4use db::user::users_loader::load_users;
5use export::repo::activity::{RepoActivity, RepoActivityKind, RepoActivityPage};
6use export::user::basic::UserBasicInfo;
7use time::OffsetDateTime;
8use time::format_description::well_known::Rfc3339;
9use uuid::Uuid;
10
11use crate::actor::RepoAccess;
12use crate::error::RepoResult;
13use crate::features::{RepoService, authz};
14use crate::metrics;
15
16use super::ListActivityQuery;
17
18fn fmt_ts(ts: OffsetDateTime) -> String {
19 ts.format(&Rfc3339).unwrap_or_default()
20}
21
22/// 聚合仓库活动时间线:PR + Release 按创建时间合并降序分页。
23/// 说明:commits 部分依赖 lib/git rewire(当前 list_commits 为 stub),暂不入列。
24pub async fn list_activity(
25 svc: &RepoService,
26 actor: Uuid,
27 repo_id: Uuid,
28 q: ListActivityQuery,
29) -> RepoResult<RepoActivityPage> {
30 let _timer = metrics::Timer::start(metrics::FEATURE_ACTIVITY_LIST);
31 let result: RepoResult<RepoActivityPage> = async {
32 let repo = authz::load_repo_by_id(svc, repo_id).await?;
33 authz::authorize_repo(svc, actor, &repo, RepoAccess::Read).await?;
34
35 let limit = q.limit.unwrap_or(svc.config.default_pagination);
36 let offset = q.page.unwrap_or(0).saturating_mul(limit);
37 let limit = std::cmp::min(limit, svc.config.max_pagination);
38 // 两个数据源各自多取 offset+limit 条,合并排序后再切片,避免跨源分页丢失
39 let fetch = offset + limit;
40 let db = svc.db.reader();
41
42 let pr_query = pr_entity::Entity::find().filter(pr_entity::Column::RepoId.eq(repo_id));
43 let pr_total = pr_query.clone().count(&db).await?;
44 let prs = pr_query
45 .order_by_desc(pr_entity::Column::AutoincrementId)
46 .limit(Some(fetch))
47 .all(&db)
48 .await?;
49
50 let release_query = release_entity::Entity::find()
51 .filter(release_entity::Column::RepoId.eq(repo_id))
52 .filter(release_entity::Column::IsTag.eq(false));
53 let release_total = release_query.clone().count(&db).await?;
54 let releases = release_query
55 .order_by_desc(release_entity::Column::CreatedAt)
56 .limit(Some(fetch))
57 .all(&db)
58 .await?;
59
60 let mut user_ids: Vec<Uuid> = Vec::with_capacity(prs.len() + releases.len());
61 user_ids.extend(prs.iter().map(|p| p.author_id));
62 user_ids.extend(releases.iter().map(|r| r.publisher_id));
63 let users = load_users(&svc.db, &user_ids).await?;
64
65 let mut items: Vec<RepoActivity> = Vec::with_capacity(prs.len() + releases.len());
66 for p in prs {
67 let author_id = p.author_id;
68 items.push(RepoActivity {
69 kind: RepoActivityKind::Pr,
70 sha: None,
71 number: Some(p.autoincrement_id as i64),
72 title: p.title,
73 author: users.get(&author_id).map(UserBasicInfo::from),
74 created_at: fmt_ts(p.created_at),
75 });
76 }
77 for r in releases {
78 let publisher_id = r.publisher_id;
79 let title = if r.title.is_empty() { r.tag_name } else { r.title };
80 items.push(RepoActivity {
81 kind: RepoActivityKind::Release,
82 sha: Some(r.sha),
83 number: None,
84 title,
85 author: users.get(&publisher_id).map(UserBasicInfo::from),
86 created_at: fmt_ts(r.created_at),
87 });
88 }
89
90 items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
91 let end = std::cmp::min(offset + limit, items.len() as u64) as usize;
92 if offset < items.len() as u64 {
93 items = items[offset as usize..end].to_vec();
94 } else {
95 items.clear();
96 }
97
98 Ok(RepoActivityPage {
99 items,
100 total: (pr_total + release_total) as i64,
101 })
102 }
103 .await;
104 metrics::record_outcome(metrics::FEATURE_ACTIVITY_LIST, &result);
105 result
106}
No newline at end of file
Added lib/repo/handler/activity/list.rs View lib/repo/handler/activity/list.rs
@@ -0,0 +1,34 @@
1//! GET /repos/{space_name}/{repo_id}/activity 仓库活动时间线
2use axum::extract::{Path, Query, State};
3use export::data::AppResponseData;
4use jwt::extractor::AuthUser;
5use uuid::Uuid;
6
7use crate::error::RepoResult;
8use crate::features::activity::{ListActivityQuery, RepoActivityPage};
9use crate::features::RepoService;
10
11#[utoipa::path(
12 get,
13 path = "/repos/{space_name}/{repo_id}/activity",
14 tag = "repo",
15 params(
16 ListActivityQuery,
17 ("space_name" = String, Path),
18 ("repo_id" = Uuid, Path),
19 ),
20 responses((status = 200, description = "List repository activity", body = AppResponseData<RepoActivityPage>)),
21 security(("bearer_auth" = []))
22)]
23pub(crate) async fn handler(
24 State(svc): State<RepoService>,
25 AuthUser { claims }: AuthUser,
26 Path((space_name, repo_id)): Path<(String, Uuid)>,
27 Query(q): Query<ListActivityQuery>,
28) -> RepoResult<AppResponseData<RepoActivityPage>> {
29 let space_id = space::features::lookup::lookup_space_by_name(&svc, &space_name).await?;
30 space::features::authz::require_active_member(&svc, claims.sub, space_id).await?;
31 let _channel_id = crate::features::lookup::channel_for_repo(&svc, repo_id).await?;
32 let page = svc.list_activity(claims.sub, repo_id, q).await?;
33 Ok(AppResponseData::ok(page))
34}
No newline at end of file