Day 08 / 共 20 天 · 第 2 周 分层与契约

Foundation:用户/空间/鉴权

前面几天多次看到 foundation 模块最先被初始化、几乎每个业务请求都要经过它检查权限。今天专门拆开这个"地基模块",看用户、空间、鉴权三件事是怎么串起来的。

📍 你在整门课的位置 · 第 2 周 分层与契约(共 4 周 · 20 天)
D6 DDD 与 Wire D7 IDL 契约 D8 Foundation D9 Prompt 读路径 D10 调试与 LLM
L01

为什么业务模块都先过 Foundation

🤔 痛点:为什么不能各模块自己管自己的用户和权限? 如果 prompt、evaluation、data 各自维护一套"用户是谁""能不能操作"的逻辑,不仅重复劳动,还极易出现"同一个用户在不同模块权限判断不一致"的安全漏洞。
💡 本质:Foundation 是全平台唯一的"身份 + 权限 + 空间"权威来源 Day 03 api.InitfoundationHandler第一个被初始化的 handler,后面 llm/prompt/data/evaluation/observability 全部要拿它的接口(AuthService/UserService/FileService)作参数传入。backend/modules/foundation/application/ 下正好四个文件对应四种职责:user.go(用户信息)、auth.go(权限校验)、space.go(工作空间)、file.go(文件服务)。

user.go

登录、注册、查用户信息——回答"当前操作的人是谁"。

auth.go

权限校验(MCheckPermission)——回答"这个人能不能做这件事"。

space.go

工作空间(Workspace)——回答"这件事发生在哪个团队隔离单位下"。

file.go

文件上传/签名 URL——多个模块共用的"存文件"能力。

L02

user.go:用户是谁

Day 04 讲的 SessionMW 里调用的 us.GetUserInfo(...),实现就在 backend/modules/foundation/application/user.go——这条链路把"Cookie 里的一个 Session ID"最终变成"一个带用户名/邮箱的完整 User 对象",供全平台使用。

和 Session 的关系:Session(会话)只是"证明你登录过"的凭证(存一个 UserID),User(用户信息)才是真正的业务数据。SessionMW 先验证 Session 合法,再用其中的 UserID 去 user.go 查完整的用户信息——这是"认证(Authentication,你是谁)"的完整过程。
L03

auth.go:能不能做这件事

backend/modules/foundation/application/auth.goMCheckPermission 是"授权(Authorization,你能干什么)"的核心:

func (a *AuthApplicationImpl) MCheckPermission(ctx context.Context, request *auth.MCheckPermissionRequest) (r *auth.MCheckPermissionResponse, err error) {
	userID, err := strconv.ParseInt(session.UserIDInCtxOrEmpty(ctx), 10, 64)   // 从 ctx 取当前用户
	if userID == 0 || err != nil {
		return nil, errorx.NewByCode(errno.CommonInvalidParamCode, ...)
	}
	spaceID := request.GetSpaceID()
	isUserSpace, err := a.userRepo.CheckUserSpaceExist(ctx, userID, spaceID)   // 查"这个用户在这个空间里吗"
	// ...
}
💡 本质:权限判断的最小单位是"用户 + 空间 + 动作"三元组 回顾 Day 09 会看到的 prompt 模块 CreatePrompt 里的这行代码:app.authRPCProvider.CheckSpacePermission(ctx, request.GetWorkspaceID(), consts.ActionWorkspaceCreateLoopPrompt)——这就是"用户是不是当前 ctx 里那个人 + 在 request.GetWorkspaceID() 这个空间下 + 有没有 ActionWorkspaceCreateLoopPrompt(创建 Prompt)这个动作的权限"。任何业务模块要做敏感操作,都要拼出这三个要素问一遍 Foundation。
⚠️ 坑:ctx 里没有用户信息时权限检查会直接报错,而不是"当匿名用户处理" auth.go:33-37 明确写了 if userID == 0 || err != nil { return nil, errorx.NewByCode(...) }——Coze Loop 里没有"匿名可访问"的降级模式,Day 04 的 SessionMW 已经在最外层挡掉了未登录请求,所以走到这里 ctx 里一定应该有用户,一旦没有就是异常情况要立刻报错,而不是静默放行。
L04

space.go:在哪个空间里

🍼 类比:公司里的部门/项目组 Workspace(工作空间)就像公司里的一个项目组——你创建的 Prompt、评测集、数据集都归属于某个空间,同一个空间下的人可以互相看到彼此的资源,跨空间则默认隔离。这是多租户 SaaS 产品几乎必备的隔离单位(Day 01 术语表提到过)。

backend/modules/foundation/application/space.go 提供空间的创建、查询、列出用户所在的所有空间等能力。几乎所有业务请求体(回顾 Day 07 IDL 里的 CreatePromptRequest)都带一个 workspace_id 字段——这是"资源归属哪个空间"的强制约束,不能省略。

L05

前端 auth-pages 简介

后端的登录/注册/权限体系,前端对应的落地是 frontend/packages/loop-pages/auth-pages/——按 ARCHITECTURE.md 的前端分层,它属于 Level-5 loop-pages(页面模块层),负责渲染登录页、注册页,并在登录成功后把 Session Cookie 落下,后续所有请求会自动带上它,被 Day 04 的 SessionMW 校验。

前后端怎么对应? 前端调用 /api/foundation/v1/users/login_by_password(Day 04 SessionMW 里特判放行的两个路径之一)完成登录,成功后浏览器带着 Session Cookie 访问其余接口。这是一条从"前端页面"到"后端 Foundation 模块"再到"每个业务请求的中间件校验"的完整闭环,前 8 天讲的所有知识点在这里汇合。
L06

今日小结 + 动手

🧠 今天你应该能回答

  • Foundation 管哪四件事?(user 用户 / auth 鉴权 / space 空间 / file 文件)
  • 认证和授权的区别?(认证=你是谁 Session→User;授权=你能干什么 MCheckPermission)
  • 权限判断的三要素是什么?(用户 + 空间 + 动作)
  • Workspace 空间的作用?(多租户资源隔离单位,几乎所有资源都挂在某个空间下)

✋ 动手 5 分钟

# 1. 看 foundation application 四件套
ls backend/modules/foundation/application/*.go | grep -v _test | grep -v wire

# 2. 看权限校验的核心函数
grep -n "func.*MCheckPermission\|func.*CheckSpacePermission" -r backend/modules/foundation/

# 3. 看 prompt 模块怎么调用 foundation 的鉴权(Day 06 讲过的"本地客户端"模式)
grep -n "authRPCProvider\|CheckSpacePermission" backend/modules/prompt/application/manage.go | head

# 4. 看前端登录页在哪
find frontend/packages/loop-pages/auth-pages -maxdepth 2 -type d
明天预告 · Day 09:从明天开始,我们要把前 8 天学到的全部知识点串成一条完整的纵切面——跟着"新建一个 Prompt"这一件事,从 Handler 一路走到 MySQL 事务写入,逐层验证今天学到的一切是怎么真实运转的。
← 上一天 · IDL 契约入门 下一天 · Prompt 读路径纵切 →