Skip to content
Clone
b868469
69 lines 16.6 KB Markdown

space-account

结论

  • 检查调用点 87 个(去重后 77 个 tag.op)/ 操作 77 个
  • 确认问题 16 条(Q 0 / B 1 / P 1 / R 4 / G 10),可疑 6 条,未实证 2 条(#6、#10 的 HTTP 层被方法级 401 掩蔽)

主线结论:space 服务有 10 条 utoipa::path 已声明、openapi.rs 已收录(因此生成客户端与 OPS 表都有),但 routes.rs 没有挂载——其中 4 条已用 curl 拿到 unknown api route,最严重的是「创建邀请」整条链路不可用。

确认问题

#类位置操作现象后端依据建议修法
1Gview/app/src/domains/space/management/model/api.ts:82-85(调用方 ui/space-header.tsx:269)space.space_invite_createPOST /api/v1/spaces/demo/invites 实测 → 404:{"code":404,"message":"unknown api route","error_code":"not_found"}。「邀请成员」对话框提交即失败,邀请令牌永远拿不到lib/service/space/src/handlers/space_admin.rs:1413-1445 声明 path="/spaces/{space_name}/invites"、operation_id="space_invite_create";lib/service/space/src/routes.rs 全文无此路径(只有 133 行 /spaces/invites/redeem);lib/service/space/src/openapi.rs:38 收录 → 客户端有、服务端无在 routes.rs::protected() 补 .route("/spaces/{space_name}/invites", post(space_admin::create_invite))
2Gdomains/space/management/model/space-admin.ts:175-176space.space_invite_create同一操作的第二个包装(createSpaceInvite),同样 404;当前无 UI 消费方,属潜伏同 #1同 #1(或删掉重复包装,只留 api.ts:82)
3Gdomains/space/management/model/space-admin.ts:162-168space.space_invite_listGET /api/v1/spaces/demo/invites 实测 → 404 unknown api route,邀请列表恒空space_admin.rs:1455-1497(path 1459 行、operation_id 1461 行);routes.rs 无该路径routes.rs 补 get(space_admin::list_invites)
4Gdomains/space/management/model/space-admin.ts:170-173space.space_invite_revokeDELETE /api/v1/spaces/demo/invites/{uuid} 实测 → 404 unknown api route,撤销邀请无效space_admin.rs:1499-1540(path 1503 行、operation_id 1505 行);routes.rs 无该路径routes.rs 补 delete(space_admin::revoke_invite)
5Gdomains/space/management/model/space-admin.ts:231-237(调用方 ui/space-settings/bans-section.tsx:48)space.space_ban_listGET /api/v1/spaces/demo/bans 实测 → 404 unknown api route,封禁列表永远为空,封禁管理面板整段失效space_admin.rs:814-829(path 816 行、operation_id 818 行);routes.rs 只挂了 /bans/{user_id} 的 DELETEroutes.rs 补 get(space_admin::list_bans)
6Gdomains/space/management/model/space-admin.ts:261-262(调用方 bans-section.tsx:192)space.space_ban_updatePATCH /spaces/{space}/bans/{user_id} 未挂载;HTTP 层被同路径 DELETE 命中的中间件 401 掩蔽(实测 401 而非 404),故记「未实证」,代码证据充分space_admin.rs:889-928(path 891 行、operation_id 893 行)声明 PATCH;routes.rs:187-191 对同一路径只注册了 delete(unban_by_user)routes.rs 补 patch(space_admin::update_ban) 到该路径
7Gdomains/space/management/model/space-admin.ts:99-100(调用方 ui/space-settings/my-membership-section.tsx:27)space.space_member_getGET /api/v1/spaces/demo/members/00000000-... 实测 → 404 unknown api route;调用方 getSpaceMember(...).catch(() => null) 把 404 吞成 null,「我的会籍」面板静默空space_admin.rs:465-502(path 467 行、operation_id 469 行);routes.rs:164-171 只挂了 .../members(GET)与 .../members/@me(PATCH),无 .../members/{member_id}routes.rs 补 get(space_admin::get_member)
8Gdomains/space/management/model/space-admin.ts:105-108space.space_member_updatePATCH /spaces/{space}/members/{member_id} 未挂载(与 #7 同路径,该路径 GET 已实测 404)space_admin.rs:541-581(path 543 行、operation_id 545 行);routes.rs 无该路由routes.rs 补 patch(space_admin::update_member)
9Gdomains/space/management/model/space-admin.ts:114-115space.space_member_removeDELETE /spaces/{space}/members/{member_id} 未挂载(同 #7 路径)space_admin.rs:621-661(path 623 行、operation_id 625 行);routes.rs 无该路由routes.rs 补 delete(space_admin::remove_member)
10Gdomains/space/management/model/space-admin.ts:102-103space.space_member_addPOST /spaces/{space}/members 未挂载;实测返回 401(同路径 GET 命中中间件),故「未实证」,代码证据充分space_admin.rs:583-619(path 585 行、operation_id 587 行);routes.rs:164-167 对该路径只注册了 GETroutes.rs 把该路径改成 get(list_members).post(add_member)
11Pdomains/space/management/model/space-admin.ts:99-100 / 105-108 / 114-115;实参来源 my-membership-section.tsx:27 的 viewerIdspace.space_member_get/update/removepathParams 键序对(space_name → 第 2 段),但第 2 段值语义错:后端该段是 Uuid 会籍 id,前端填的是 数字 user_id(getSpaceMember(spaceName, userId: number))。补上路由后仍会因 Uuid 解析失败 400space_admin.rs:16-20 SpaceMemberPath { space_name: String, member_id: Uuid };对照 space_admin.rs:28-32 SpaceBanPath { user_id: i64 }(封禁族才是数字 id)三个包装都先 findSpaceMember(spaceName, userId) 取 member.id,再填第 2 段(banSpaceUser 已经是这个写法,照抄即可)
12Rdomains/chat/model/api.ts:311-336(SpaceMemberRow.user)space.space_member_list把成员行当嵌套 { user: { user_id } } 读:row.user?.user_id ?? '' 恒为 '',随后 .filter(row => Boolean(row.user_id)) 全过滤 → 加人选择器永远没有候选(listSpaceMembers 恒 [])后端 MemberSummary 是扁平行:lib/service/space/src/services/types.rs:131-149 有 user_id: i64、username、alias_name,没有 user;生成类型 packages/apiclient/space/space-member-list.schemas.ts 的 EnvelopePageMemberSummaryDataItemsItem 同样无 user 键直接 map((row) => ({ ...row, user_id: row.user_id })),user_id 取扁平字段
13Rdomains/issues/model/api.ts:306-307 + 类型 domains/issues/model/issue.ts:84-88space.space_member_listSpaceMember { user?: UserDto } 同源误用,问题集指派人候选恒空(use-board-data.ts:52 消费)同 #12同 #12:行类型改成扁平 user_id/username/alias_name
14Rdomains/repo/model/protection.ts:148-156space.space_member_listrow.user 恒 undefined,.map(row => row.user).filter(...) 全过滤 → 保护规则白名单用户候选恒空同 #12同 #12
15Rdomains/settings/model/api.ts:105-107user.my_suspensions后端返回裸数组,前端按 { items } 读:page?.items ?? [] 恒 [],处罚历史永远空lib/service/user/src/handlers/my_suspensions.rs:14-31 body = AppResponseData<Vec<SuspensionDto>>;生成 packages/apiclient/user/my-suspensions.schemas.ts 的 EnvelopeVecSuspensionDto.data 是数组改成 unwrapApi<SuspensionRow[]>(...) 后直接返回该数组
16Bdomains/space/management/model/space-admin.ts:239-250(调用方 bans-section.tsx:83-88 传 expires_at)space.space_member_ban前端表单收集「封禁到期时间」并在 cmd 里声明 expires_at,实际 body 只发 { reason };后端 BanMemberBody 也没有该字段 → 到期时间被静默丢弃,封禁恒为永久space_admin.rs:281-284 BanMemberBody { reason: Option<String> };生成 space-member-ban.schemas.ts 的 BanMemberBody 只有 reason优先给 BanMemberBody 补 expires_at(service 侧已支持,UpdateBanBody 有 expires_at 三态);或在 banSpaceUser 里封禁成功后串一次 update_ban 带上 expires_at

可疑(需人工确认)

#位置操作疑点已排除的可能
1ui/space-header.tsx:263-274space.space_invite_create邀请表单同时收集 email 与 groups(角色),但提交的 body 只有 target_user_id / max_uses / ttl_secs;CreateInviteBody 根本没有 email、extra_role_ids 字段,handler 又固定传 email=None、extra_role_ids=&[] → 「按邮箱邀请」退化成无目标裸链接,且不能带角色不是字段名拼错:space_admin.rs:111-118 的 CreateInviteBody 里压根没有这两个键;service 层 services/invite.rs:53-64 的 create_invite_with_extras 是支持 email / extra_role_ids 的,只是没暴露到 HTTP
2domains/space/management/model/space-admin.ts:170-173space.space_invite_revoke返回类型写成 SpaceInvite | null,后端是 AppResponseData<bool>(space_admin.rs:1513-1522)当前无 UI 消费方调 revokeSpaceInvite,不会立刻炸;属类型谎报
3domains/space/management/model/space-admin.ts:42 + :162-168space.space_invite_listSpaceInvite = NonNullable<EnvelopeInviteSummary['data']>,而 EnvelopeInviteSummary 在 services/api/index.ts:208 被定义为 EnvelopeCreateInviteResponse,其 data 是 { invite, token };但 list_invites 的 items 是扁平 InviteSummary无消费方,且该端点本身 404(#3),当前不可观察
4domains/settings/model/api.ts:28-29account.update_profile声明返回 UserPublic,后端 PATCH /me 返回 AppResponseData<NoData>(handlers/update_profile.rs:18-33),实际 data: null调用方 profile-section.tsx:128、account-section.tsx:119 都是 await 后丢弃返回值,再 onMeChanged() 重拉,当前无运行时后果
5ui/space-settings/bans-section.tsx:28-32space.space_ban_update只接了 allow_speak / allow_push_repo / allow_comment_any 三个放行位,UpdateBanBody 还有 allow_agent_join 未暴露字段名一致,属覆盖缺口而非错配(allow_* 才是请求侧正确命名,ban_allow_* 是响应侧命名,此处没混用)
6domains/settings/model/api.ts:106user.my_suspensions不传 include_revoked,默认 false 只返回仍生效的处罚;而 UI 文案是「处罚历史」键名正确(IncludeRevokedQuery),只是筛选语义可能不符预期

已核对无问题

操作依据
space.explore_spacesjoin_requests.rs:75-83 ExploreQuery { q, sort, order, limit, offset }(deny_unknown_fields);前端 discovery/model/api.ts:76-81 只发这 4 个且显式省略 undefined。实测 ?limit=100 → 200,?limit=200 → 400、?offset=5001 → 400、?per_page=10 → 400
space.space_create / space_updatetypes.rs:217-263 两个 body 覆盖 allow_join / allow_join_level / allow_invite / join_policy;UI 只发 allow_join / allow_join_level / allow_invite(general-section.tsx:51-53、create-space-modal.tsx:57),均为合法键,join_policy 缺省即不改
space.space_get / space_delete / space_leave路由 routes.rs 的 /spaces/{space_name} GET/DELETE 与 /leave 均已挂载;DeleteOrgQuery 的 force/reason 前端不发(#[serde(default)]);space_leave 返回 bool,前端 unwrapApi<boolean> 一致
space.get_space_dir_tree后端 DirTreeQuery { parent_id: Option<Uuid> }(get_space_dir_tree.rs:12-14),前端传 params: {},无未知键、无分页截断
space.create_dir_node / move_dir_node / update_dir_nodemove_dir_node 前端发 { parent_dir_id, position }(api.ts:56-61),与后端 MoveDirNodeBody 对齐;路径 3 段顺序正确
space.list_groups / create_group / update_group / delete_grouplist_groups 用 PageQuery(limit 1..=100),前端 limit: ADMIN_PAGE_LIMIT = 100 合法;CreateGroupCmd/UpdateGroupCmd 字段与 UI 一致
space.assign_group / unassign_group / join_group_self / leave_group_self路径第 2 段确为 user_id: i64(unassign_group.rs:10-15 的 MemberGroupPath),前端传数字 user_id 正确;AssignGroupCmd { group_id: Uuid } 一致
space.list_join_rules / create_join_rule / update_join_rule / delete_join_rule列表返回裸数组 Vec<JoinRuleDto>(list_join_rules.rs:17-31),前端 rows<SpaceJoinRule> 按数组读,一致;CreateJoinRuleCmd { role_id, trigger, matcher }、UpdateJoinRuleCmd { enabled, matcher } 与生成类型一致
space.create_join_request / withdraw_join_request / list_join_requests / approve_join_request / reject_join_request路径段顺序正确;ListJoinRequestsQuery { status, limit, offset } 与前端可选参数一致(显式省略 undefined,避免字符串 "null")
space.space_invite_redeem路由 routes.rs:131-136 已挂载;body { token } 与 RedeemInviteBody(space_admin.rs:120-123)逐字一致;响应 { organization_id, member_id, joined },前端按 unknown 读,不误用
space.get_space_image / delete_space_image路径 2 段(space_name, kind)顺序正确;kind 枚举与 SpaceImageKind 一致
space.list_dir_group_overwrites / list_dir_user_overwrites / set_* / remove_*SetOverwriteCmd { allow_mask: i64, deny_mask: i64 }(services/dir.rs:147-150)与前端 { allow_mask, deny_mask } 一致;路径段数/顺序正确
account.me后端 MeResp { user: UserMeDto, profile: Option<ProfileDto> }(handlers/me.rs:12-15);前端 me.user.user_id / display_name / username / avatar_url、me.profile 均命中 UserMeDto(services/profile.rs:13-28)与 ProfileDto(:31-44)
account.get_preferences / account.update_preferencesUserPreferencesDto 含 diff_view_style(services/preferences.rs:11-24),repo/model/commits.ts:64 读的就是该名;UpdatePreferencesCmd 的 mute_until 是三态(deserialize_opt_opt_datetime),前端 PATCH 只发变更键,符合语义
account.list_deviceshandlers/devices.rs:53-70 用 PageQuery(limit 1..=100),前端 limit: 100 合法;响应 Page<DeviceDto>,前端按 { items } 读一致
account.update_profile(body) / account.update_usernameUpdateProfileCmd(services/profile.rs:47-65)覆盖 UI 发来的 display_name/locale/language/timezone/real_name/bio/pronouns/website/location/company/date_of_birth/show_*/visibility;update_username 的 body { username } 与 UpdateUsernameCmd 一致,返回 UserPublic 与前端声明一致
account.mark_notifications_readMarkNotificationsReadCmd { ids?: Uuid[], all: bool }(handlers/mark_notifications_read.rs:11-16);UI 发 { ids: [id] } 与 { all: true },均合法
auth.auth_ssh_key_list / add / delete路径是 /auth/ssh-keys(不是 /me/ssh-keys),前端用的是 auth.* 这三个 op,路径正确;PageQuery 上限 100,前端 limit: 100 合法;SshKeyAddRequest { name, description, public_key, expires_at } 与前端 { name, public_key } 一致(请求侧没有 algorithm/key_type 之争,algorithm 只在响应 SshKeySummary 里);sshkeys-section.tsx:95 读的正是 key.algorithm
auth.change_password / totp_status / totp_setup / totp_enable / totp_disable / recovery_codes / regenerate_recovery_codesChangePasswordCmd { old_password, new_password }、TotpEnableCmd { code, password }、TotpCodeCmd { code }、RegenerateRecoveryCodesCmd { password } 与 UI 实参逐字一致
user.list_sessions / revoke_session / revoke_all_sessionsSessionQuery { kind, include_revoked, limit, offset }(handlers/sessions.rs:41-61),前端 { limit: 25 } 合法;revoke_session 路径 1 段 session_id 顺序正确
user.my_spacesPageQuery 上限 100,前端 limit: 100 合法;响应 Page<MySpaceDto>,前端按 { items } 读一致
user.list_push_subscriptions / delete_push_subscription / upsert_push_subscriptionPageQuery 上限 100,前端 limit: 100 合法;delete 路径 1 段 push_id 顺序正确
users.get_user_by_username后端 GET /users/by-username/{username}(handlers/users.rs:42-60,路由 routes.rs:56-59),前端 space-admin.ts:87 去掉 @ 前缀后填入,路径 1 段且顺序正确
account.account_deletion_status / request_account_deletion / cancel_account_deletion三个 op 存在于 OPS,但 view/app/src 全文无任何调用点(仅 ops.ts 生成条目),无前端形状可核对,故不计入问题(不可观察)