项目:html5-examples | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
前七篇按"架构—页面—接口—数据—流程—部署"的横向视角组织,本篇起进入纵向视角:逐个模块深读。所谓"模块",指源码中一个高内聚的目录或文件簇——它有清晰的对外边界(导出的函数/组件/控制器),内部有自成体系的机制(缓存、状态机、拼装管线)。每 个模块按固定三段式展开:职责定位(它为什么存在、边界在哪)→ 关键文件与函数清单(表格,含路径与核心函数/导出)→ 机制走读(按代码执行顺序讲清"怎么做到的")。
已在前七篇充分展开的横向机制(如 401 全局联动、URL 即状态、路由自愈),本篇只给指针不重复展开;本篇补充的是函数级与文件级的细节,这些细节在横向视角里会被压缩成一句话,但排查具体问题时恰恰是它们决定成败。阅读顺序建议:先读第 1 节(前端基建三件套),它们是所有页面的地基;再读第 2 节(CSS 实验室模块族),它是"配置驱动扩展"的标本;最后读第 3 节(NestJS 后端模块),按需查阅。
职责:把一个 CodeExample(html/css/js 三段片段)拼装成可直接塞进 iframe srcdoc 属性的完整 HTML 文档字符串,并决定 iframe 的 sandbox 属性组合。它是内容域与浏览器安全模型之间唯一的接合面(安全分析详见《08-安全与权限设计》)。
关键文件与函数清单:
| 文件/函数 | 签名/职责 |
|---|---|
src/utils/sandbox.ts | 全模块唯一文件,纯函数无 Vue 依赖 |
Snippet | 接口 { label, css?, html?, js? },变体注入载体 |
deduplicateRepeatedJs(js) | 私有;检测并去除"两段完全相同的 JS 拼接"(偶数长度前后半相等快路径 + 按顶级 const/let/var/function/class/import/export 语句切分后前后半逐条比对) |
escapeHtml(str) | 私有;& < > " ' 五字符实体转义,用于文档 <title> |
buildSrcDoc(example, snippet?) | 核心导出;拼完整文档(详见走读) |
needsSameOrigin(example) | 正则探测示例是否引用同源资源,决定是否追加 allow-same-origin |
getSandboxAttributes(includeSameOrigin) | 返回 sandbox 属性数组,默认 ['allow-scripts','allow-forms','allow-modals','allow-popups'] |
机制走读(buildSrcDoc 的拼装管线,按执行序):
snippet?.css/js 以换行追加到示例对应段之后;snippet?.html 若存在则整体替换 example.html——这一非对称语义支撑详情页"变体"切换(变体通常只改一段)。topicSlug === 'html5' 时把 html 包进 <div class="example-stage"> 并注入舞台 CSS(100vh、flex 居中、子元素不收缩),补偿导入期被清洗掉的 body 布局样式。deduplicateRepeatedJs 去重,再把 </script(大小写不敏感)替换为 <\/script,防止示例 JS 字符串提前闭合宿主 script 标签。<!DOCTYPE html> + charset/viewport + <base href="{window.location.origin}/"> + reset CSS + 一组 .demo-layout/.snippet-panel/.snippet-btn/.preview-panel 辅助样式(历史导入期的侧栏预览布局遗留)+ 示例 CSS;body 内为 stagedHtml 与条件渲染的 <script> 块。<base> 使示例里的 /shared-assets/...、/static-sites/... 绝对路径解析到站点 origin,这正是 needsSameOrigin 存在的原因。消费链:usePreview.ts 的 srcDoc computed 调用 buildSrcDoc → PreviewPane.vue 以 includeSameOrigin = needsSameOrigin(example) 传入 DeviceFrame.vue → 后者用 getSandboxAttributes(includeSameOrigin).join(' ') 写入 <iframe :sandbox>。DeviceFrame 还在切换/卸载时把 srcdoc 置空以停掉示例音频(iframeRef.value.srcdoc = '')。PreviewPane 内部用 IntersectionObserver 做可视区懒渲染:isVisible 为假时只渲染占位,不创建 iframe。
职责:750 个示例(历史迁移基线 1057 个)的唯一数据通道。对外保持与原 NestJS /api/examples 完全一致的函数签名与响应结构(迁移决策见文件头注释),对内负责分组 JSON 懒加载、内存缓存、稳定排序、localStorage 覆盖层。
关键文件与函数清单:
| 导出 | 职责 |
|---|---|
PaginatedResponse<T> | { data, pagination: { total, page, limit, totalPages } },全站分页规范类型 |
GroupKey / groupLoaders | 8 个分组名联合类型 + 对应的 8 个动态 import('@/content/examples/*.json') 加载器表 |
loadAll()(私有) | 双重检查锁式加载:cache 命中即返;否则复用/创建 loadingPromise = Promise.all(8 个 import) → flatMap 合并 → 稳定排序 → 写 cache → applyOverrides;catch 中 loadingPromise = null 允许重试 |
getExamples(params?) | 七维内存过滤(topicSlug/sourceGroup/categorySlug/sourceFilePrefix/ids/tags/search)+ 分页切片;limit 默认 100,页码钳制 [1, totalPages] |
getExampleById(id) | 内存查找,未命中抛 ApiError(404, …, { message: '示例不存在或已被移除' }) |
updateExample(id, data) | 只摘取 html/css/js 三字段写入覆盖层,Object.assign 同步内存 cache,返回浅拷贝 |
| 私有覆盖层函数 | OVERRIDE_KEY = 'examples-overrides-v1';loadOverrides/persistOverrides(隐私模式写入失败静默放弃)、applyOverrides(loadAll 后合并进内存数组) |
机制走读:
Promise.all 并发拉取——首次进入示例库会一次下载全部 8 个分组(约 8.3MB 原文,gzip 后显著缩小),之后常驻内存,翻页筛选零网络成本。排序采用"数字 id 升序在前、非数字 id 按 title.localeCompare(zh-CN) 排后"的稳定序,保证任何一次加载顺序一致,前端分页才不会跨页重复/遗漏。loadingPromise ??= 使并发触发的多次 loadAll 复用同一个 Promise(与 utils/highlighter.ts 同一套单例+双重检查锁手法,见《01-系统架构》§5);失败置空是刻意设计——chunk 加载失败(如发版瞬间)不会把模块永久锁死在错误态。updateExample 是详情页"保存"按钮的落点。它只允许覆盖 html/css/js 三个内容字段(title/tags 等元数据不可改),写入 localStorage 后刷新仍生效,清缓存即还原——这是"本机沙上笔记"语义,绝不回流 JSON。职责:全站唯一允许出现裸 fetch 的地方(397 行),把超时、重试、鉴权头、401 副作用、错误归一全部收拢。三个依赖注入点(setAuthTokenProvider / setAuthFailureHandler / configureClient)让它不 import 任何 store——authStore 在 initialize() 时反向注册,属于典型的依赖反转。
关键文件与函数清单:
| 导出/私有函数 | 职责 |
|---|---|
ApiError | status/statusText/url/body 四字段错误类;body 为解析后的后端响应体 |
getApiErrorMessage(error, fallback) | 全站唯一错误文案提取器:后端 body.message(string 或 string[],数组以「;」连接)→ error.message → fallback |
request<T>(endpoint, options) | 主循环:最多 config.retry(默认 2)次重试;每次尝试新建超时 AbortController(默认 10s);401 触发 authFailureHandler 后照常抛出;重试判定走 shouldRetry |
doRequest<T> | 单次请求:注入 Bearer 头、linkSignals(timeoutSignal, options.signal) 合并取消信号;非 2xx 读 body 转 ApiError;空 body 返回 null as T;JSON 解析失败抛 ApiError(status, '响应体 JSON 解析失败') |
toApiError(error, url) | 异常归一:AbortError→408,其余 Error→0(网络错误),非 Error→0 |
linkSignals(...) | 合并多个 AbortSignal:优先原生 AbortSignal.any,否则手动 addEventListener 兜底 |
backoffDelay/shouldRetry | 指数退避 min(500·2^(n-1), 2000)ms;仅 GET 且 status=0 或 5xx 重试 |
get/post/put/patch/del | 五个谓词;get 在 disableCache=true 时附 _t 时间戳;buildQueryString 过滤 undefined 并字符串化 |
fireAndForgetKeepalive | beforeunload 场景专用:keepalive:true、不重试不抛错不读响应(64KB body 上限),返回是否成功发起 |
机制走读:重试循环里有一个容易忽略的细节——每次 attempt 都新建超时 controller(request 内注释明确说明避免上次重试 timer 影响本次),finally 里 clearTimeout;401 的处理在重试判定之前,因此 401 永不重试,且处理器自身抛错也不影响错误上抛(try/catch 包裹)。doRequest 先读错误体再抛错的设计让前端能展示后端 ValidationPipe 的字段级 message 数组——与后端约定互为配套(见《03-接口设计》§4)。配套测试 src/api/client.test.ts 覆盖重试与错误归一。
职责:通用异步资源加载 composable,设计参考 @vueuse/core 的 useAsyncState,但增加了竞态保护:并发多次加载时仅最新一次请求的结果(与错误)会写入状态。
关键文件与清单:
| 成员 | 职责 |
|---|---|
UseResourceFetchOptions<T> | immediate?(默认 true)、watch?(WatchSource 或数组,变化自动 run)、onError/onSuccess、initial? |
返回值 ResourceFetchResult<T> | { data: Ref<T|null>, loading, error, refresh, execute }(refresh 与 execute 同一函数) |
内部 requestId / abortController | 递增序号 + 每次新建 controller 并 abort 前序 |
机制走读:run() 四步——abort 前序、++requestId 快照为 currentRequestId、置 loading/清 error、await fetcher(signal)。三个出口都做序号比对:成功但过期→丢弃;失败但过期→丢弃;AbortError(DOMException 且 name 为 AbortError)视为竞态取消而非真实错误→静默返回;finally 中只有最新请求才把 loading 置 false(防止被取消的旧请求提前熄灭新请求的 loading)。data 用 shallowRef——列表场景避免深层响应式代理大数组的开销。典型用法见 ExampleDetailPage:watch: () => route.params.id 实现路由参数快速切换时旧请求被取消、旧结果被丢弃。
职责:管理列表页筛选状态,输出"实时值 filters + 300ms 防抖值 debouncedFilters"双层结构,并可选地把防抖值镜像到 URL query(replaceState,不触发路由跳转)。
| 成员 | 职责 |
|---|---|
UseListFilterOptions<T> | initial、debounce?(默认 300)、syncUrl?(默认 true)、urlKeys?(字段名→query key 映射) |
resolveInitial()(私有) | 初始化时从 window.location.search 恢复被映射字段——刷新/分享不丢状态 |
watchDebounced | 来自 @vueuse/core,监听 filters 全字段深比较,防抖后写入 debouncedFilters |
| URL 回写 watcher | debouncedFilters 变化时逐字段 set/delete 到 URLSearchParams,与当前 search 不同才 replaceState(避免无谓历史污染) |
reset/clearAll | reset 恢复全部初值;clearAll 只清 search、其余字段回初值 |
机制上值得注意的是单向写 URL:composable 只负责把状态写进 URL,不监听路由变化回写状态——外部导航(如首页主题卡带 query 跳入)由页面层自己的 route.query watcher 单向同步进 filters,两个方向被刻意分开以避免回环(ExamplesLibraryPage.vue 的注释专门解释了这一点,见《02-页面设计》§2.2)。
src/components/examples/PreviewPane.vue(与 src/components/common/DeviceFrame.vue)是沙箱的运行时外壳。PreviewPane 除懒渲染外,还管理设备视口(desktop 100%/tablet 768×1024/mobile 375×667,尺寸表在 usePreview.ts 的 DEVICE_DIMENSIONS)、全屏切换、refreshKey 自增强制 srcdoc 重算;usePreview 还封装了复制三段代码(useClipboard)与 Blob URL 新窗口打开(60 秒自动 revoke,onUnmounted 兜底清理 Map 中全部定时器与 URL)。这三个文件的分工:usePreview 管状态与纯逻辑,PreviewPane 管布局与可视区,DeviceFrame 管浏览器视口框与真实 iframe 元素(sandbox 属性在此落地)。
CSS 实验室是"配置驱动扩展"的极致形态:63 个具体生成器组件共享一条动态路由、一个基类骨架、一份目录清单。模块族由四个层次构成:目录层(config/css-lab)、路由层(router/routes/css-lab.ts)、分发层(CssGeneratorRouter + generators/routes.ts)、实现层(generators/*.vue)。
| 文件 | 导出 | 说明 |
|---|---|---|
catalog.ts(588 行) | cssTools: ToolItem[]、cssToolSubgroups | 主清单:实测 52 条 cssTools(1 条速查表 + 51 条 css-gen-* 生成器;Flex/Grid/渐变 3 个工作台走 css-lab.ts 独立路由,不在本清单);子分组映射按 8 类(layout/button/card/nav/animation/text/bg/form)各持 accent 色与 id 列表。文件内注释称"64 项子生成器"——与实际条目数存在口径差(见《13-技术债务》第 3 条) |
routes.ts | TOOL_ROUTE_PATHS(66 键常量表)、toolPath(name) | 路由路径单一事实源:66 键 = 1 个 cheatsheet + 65 个 css-gen-*;toolPath 供 catalog 派生 link,防止多处硬编码漂移 |
categories.ts | toolCategories(all/css 两项)、toolCategoryTitleMap、getCategoryAccent() | 分类极少——非 CSS 工具拆去 devtools 后只剩一个 css 分类 |
index.ts | 桶导出 | 消费方统一从 @/config/css-lab 引入 |
父路由 /css-lab 挂 CategoryLayout.vue,子路由依次为:首页('')、css redirect、css/cheatsheet 速查表、11 条旧分类 redirect(css/flexbox→gen-flex-layout 等)、3 条合并生成器 redirect(gen-radial-gradient→gen-gradient-bg 等)、3 个显式工作台路由(Flex/Grid 标 meta.noSidebar: true 全宽;渐变工作台走普通侧栏)、以及 css/gen-:kind 动态段(组件 CssGeneratorRouter.vue)。文件头注释记录了板块更名史(/tools → /css-lab,非 CSS 工具迁往独立项目),底部 LegacyToolsRedirect 把 /tools/:pathMatch(.*)* 连 query 与 hash 整体迁往 /css-lab/*。
src/views/css-lab/css/generators/routes.ts(82 行):generatorComponents: Record<string, Component>——64 个 kind 键到 defineAsyncComponent(() => import('./XxxGenerator.vue')) 的映射(其中 card 键与 slanted-card 指向同一组件,是唯一别名);generatorKinds 为键列表;isValidGeneratorKind(kind) 供 Router 校验。
src/views/css-lab/css/CssGeneratorRouter.vue(约 500 行)是分发与编排中枢,机制比"按 kind 动态渲染"丰富得多:
activeTab 默认 'showcase'(卡片示例墙),切 'generator' 才渲染生成器组件——即"先看效果库、再调参数"的交互次序。_sourceKind;styleSplit 的来源(button)再按 BUTTON_STYLES 视觉风格细分。FAMILY_TABS 提供同族子风格 tab(另有 text、progress 两族),主入口之外的成员从 catalog 侧栏移除。getCardShowcases(kind)(generators/cardShowcaseData.ts)异步懒加载该 kind 的样式变体;KIND_CLASSIFIERS + classifyItem 为配置了的 kind 生成子分类 Tab。handleApplyShowcase → 切回生成器 Tab → requestAnimationFrame 后调用生成器实例暴露的 applyCardShowcase(id, item);命中预设则恢复参数化预览,未命中则回退 customPreview(SourcePreview 直接渲染卡片真实 html/css)——保证"应用到生成器"始终有可见效果。跨来源卡片则 router.push 到对应真实生成器页。'button';页面标题从 cssTools 反查(css-gen-<kind> → 名称去"生成器"后缀)。目录实测 71 项:64 个 *Generator.vue(含 BaseGenerator.vue 基类,即 63 个具体生成器)+ 7 个支撑文件(generatorTypes.ts、index.ts、routes.ts、utils.ts、cardShowcaseData.ts、showcaseClassifiers.ts、cardShowcases/ 数据目录)。
| 文件 | 职责 |
|---|---|
BaseGenerator.vue | 骨架组件(注释自称"方案 B 布局"):Header + Action Bar(预设 pill + 查看代码按钮)+ 左预览/右配置双列 Body + 代码 Modal(Shiki 高亮,经 CodeSlidePanel)。props:title/description/icon/iconColor/config/presets/sections/previewStyles/codeOutput/customPreview;emits:update:config/reset/apply-preset/exit-custom-preview。参数控件六类:slider/color/select/text/number/toggle(text/number 独占整行,其余双列)。文件注释称"61 个生成器通过 props/sections/presets/#preview 插槽接入" |
generatorTypes.ts | CustomPreview/GeneratorPreset/GeneratorParam/GeneratorSection 四接口;文件头注释解释为何独立成 .ts——.vue 模块经 declare module '*.vue' shim 无法被 import type 其命名导出(TS2614)。BaseGenerator 另做了一次向后兼容的类型再导出 |
index.ts(435 行) | 自动生成的桶文件:"请勿手动编辑"——61 个 export { default as ... }(含 BaseGenerator)+ generatorList(60 条 { id, label, icon, category } 元数据,category 取值 structure/animation/text/utility 等) |
cardShowcaseData.ts / cardShowcases/ | 卡片墙数据入口与按 kind 组织的变体数据 |
showcaseClassifiers.ts | BUTTON_STYLES、KIND_CLASSIFIERS、classifyItem/classifyButtonStyle——卡片墙子分类器 |
utils.ts | 生成器共享工具函数 |
一个生成器的完整接入路径:组件组合 BaseGenerator 并声明 sections: GeneratorSection[](参数分组)与 presets: GeneratorPreset[](一键预设)→ generators/routes.ts 的 generatorComponents 加一行 kind 映射 → catalog.ts 加一条 ToolItem(id css-gen-xxx、link 用 toolPath('gen-xxx') 派生)→ 路由零改动。首页 Bento 分类卡、侧边栏、搜索索引全部从 catalog 派生。完整 SOP 见《11-扩展开发指南》场景二。
server/src/main.ts(60 行)的 bootstrap 顺序即安全基线顺序:全局 ValidationPipe{ whitelist, forbidNonWhitelisted, transform }(DTO 即契约,未声明字段直接 400)→ CORS(CORS_ORIGIN 逗号分隔白名单 + credentials: true)→ JWT Secret 安检(弱密钥字典四项 + < 开头模板占位符检测,生产拒绝启动/开发告警)→ 生产缺 DB_PASSWORD 拒绝启动 → createDefaultAdmin()(仅当不存在时创建;默认密码 admin123 时打警告)。端口默认 3000。
server/src/app.module.ts 装配:ConfigModule.forRoot({ isGlobal, envFilePath: '.env' });TypeOrmModule.forRoot(PostgreSQL,27 个实体登记于 entities 数组,synchronize: DB_SYNC === 'true',开发环境开 SQL logging);ServeStaticModule 把 server/admin/ 挂到 /admin(排除 /api/(.*));18 个业务模块 import;APP_GUARD 全局挂 JwtAuthGuard。全局守卫 + @Public() 放行 + 写操作叠加 AdminGuard 构成三级访问漏斗(机制详见《08-安全与权限设计》§3)。
职责:注册/登录/手机号登录/刷新令牌/当前用户/登出/改密 + 图形验证码,是后端安全机制最密集的模块。
| 文件 | 关键内容 |
|---|---|
auth.controller.ts | @Controller('api/auth');cookie 名 auth-token(httpOnly/secure(生产)/sameSite=lax/path=/);captcha GET [公开]、login/register/phone-auth POST [公开 + RateLimitGuard(5, 60_000)]、refresh POST [公开]、me GET(需鉴权)/ POST [公开,返回 { user: req.user || null }]、logout POST [公开]、password PUT(需鉴权) |
auth.service.ts | AuthResult{ accessToken, refreshToken, expiresIn, user } 统一返回结构;账号归一(邮箱/手机号/用户名三态 normalizeAccount);bcrypt cost 12;refresh token 为 64 字节随机 hex 落库(RefreshToken 实体),一次性使用——refreshAccessToken 校验后立即吊销旧 token 并签发新对;吊销后再用即判定泄露,触发 revokeAllUserRefreshTokens 全量吊销;改密后同样全量吊销;cleanupExpiredRefreshTokens 供清理过期记录 |
captcha.service.ts | 验证码 challenge 生成与校验,落 CaptchaChallenge 实体 |
strategies/jwt.strategy.ts | passport-jwt;提取器顺序 = Bearer 头优先 → cookie auth-token 兜底;payload { sub, username, role } → { userId, username, role } |
guards/jwt-auth.guard.ts | 宽容模式:无任何凭证(无 Authorization 头且 cookie 无 auth-token)时放行且 req.user 为 undefined,由业务自行处理;有凭证但验证失败才抛 401——这是"公共浏览 + 可选认证"的兼容设计 |
guards/require-auth.guard.ts | 严格模式:req.user 缺失即 401;供 knowledge 等强制登录模块叠加 |
guards/admin.guard.ts | 未认证 401、非 admin 角色 403 |
guards/rate-limit.guard.ts | 进程内 Map 按 IP 限流(默认 5 次/60 秒),超限 429 并返回 retryAfter;注意重启即清零、多实例不共享 |
decorators/public.decorator.ts | Public() = SetMetadata('isPublic', true) |
dto/auth.dto | AccountLoginDto(account/username + password)、RegisterDto(account/password/displayName/captchaId/captchaCode)、PhoneAuthDto、RefreshDto、ChangePasswordDto |
三文件标准四件套(无 dto 目录,body 直接 AddFavoriteDto)。favorites.controller.ts 类级 @UseGuards(JwtAuthGuard):GET /api/favorites(返回完整 Example 数据)、POST({ exampleId })、DELETE /:exampleId。favorites.service.ts 的 findAll 用 QueryBuilder innerJoin(Favorite) 按 favorite.created_at DESC 排序返回收藏的完整示例;add 先校验示例存在再落库(favorites 表 user_id+example_id 联合唯一索引兜底幂等);remove 影响 0 行抛 404。前端消费方 src/api/favorites.ts(get/add/remove 三函数)与 useFavorites.ts(模块级单例状态 + 乐观更新,见《05-核心流程》流程四)。
两个目录模块结构对称:controller 全 GET [公开] + 写操作 [AdminGuard]。categories 额外提供 GET /grouped(按 topic 分组的分类树)与 GET /subcategories/:sourceGroup(某分组下真实 category 分布——示例库侧边栏二级菜单的数据源)。service 的列表查询统一走 clampPage/clampLimit 归一化(server/src/common/utils/pagination.util.ts:page 非法回退 1,limit 非法回退 20、默认上限 100、可用 clampLimit(value, 1000) 放宽——全量端点即用后者)。
@Controller('api/examples'):GET 列表 [公开](七维过滤 query:topicSlug/categorySlug/search/isInteractive/sourceFilePrefix/sourceGroup/ids + 分页)、GET :id [公开](404 语义)、POST/PATCH/DELETE [AdminGuard]。examples.service.ts 的 findAll 用 QueryBuilder 拼条件并在 select 中保留 html/css/js 全文字段(注释:保留代码字段用于列表预览快照)。前端已于 2026-10-05 切换到本地 JSON,SPA 前端零调用;/admin 后台静态页(server/admin/js/examples.js)是其现存消费方(已核实,见《13-技术债务》第 2 条)。
| 模块 | 端点概貌 | 前端消费 |
|---|---|---|
| static-site-nav | GET 列表/详情 [公开](默认 limit=1000、createdAt DESC)+ POST/PATCH/DELETE [Admin] | src/api/static-sites.ts(Tailwind-sites 板块) |
| ui-elements | GET categories / 列表 / :id [公开] + PATCH/DELETE [Admin] | src/api/ui-elements.ts |
| canvas-examples | GET libraries / categories / 列表(library/category/group/search 过滤)/ :id [公开] + POST/PATCH [Admin] | src/api/canvas-examples.ts(CanvasExample 实体字段最多:code jsonb、loaderKind、group/chapter 排序字段、html/css/js/reactCode 等) |
两组 GET 端点全部 [公开]:visual-demos 提供 libraries/engines/groups/列表/:id;chart-examples 提供 categories(需 library query)/列表/:id。前端仅保留 src/api/visual-demos.ts 的单函数 getVisualDemoById——文件头注释明示"历史深链专用:/visual-demo/:id 详情仍走后端(SVG 课程深链共用),列表/库/引擎/分组 API 已随视觉演示板块拆分下线"。chart-examples 则完全无前端 api 文件(仅存测试目录残留 src/components/chart-examples/__tests__/)。
@Controller('api/knowledge/oss'):GET signature(直传签名,有效期至次日)、POST upload(服务端上传,单文件上限 MAX_UPLOAD_BYTES = 200MB,大文件走 STS 分片通道)。oss.service.ts 懒初始化 ali-oss client(四个 OSS_* 环境变量缺一即抛"配置不完整");.env.example 注释明确"不配置时后端仍可正常启动,仅网盘相关功能不可用(优雅降级)"。注意其路径挂在 /api/knowledge 前缀下——OSS 是 knowledge 域的配套设施。前端、姊妹项目与 /admin 静态页源码中均未发现调用方(2026-10-06 复核;⚠️ 待确认 仅剩是否存在未开源的网盘前端)。
@Controller('api/run') 类级 @UseGuards(AdminTokenGuard):POST code/execute(python3/java 子进程执行)、POST sql/query(PostgreSQL 只读查询)、POST mysql/query(MySQL,依赖 mysql2)。admin-token.guard.ts 校验请求头 x-admin-token 与 RUN_ADMIN_TOKEN 一致,未配置该环境变量时整个运行通道直接禁用(避免默认放行);注释称"前端(devtools)仅在登录账号 role=admin 时才携带该令牌(见 getRunAdminToken)"——但 devtools 现源码中并无 getRunAdminToken,调用方存疑(见《13-技术债务》第 5 条)。run.service.ts 的安全设计值得记录:代码写入 mkdtemp 临时目录执行(timeout 钳制 500ms–10s、代码上限 100K 字符、输出上限 100K 字符并标记 truncated);SQL 强制只读前缀白名单(^\s*(SELECT|SHOW|DESCRIBE|DESC|EXPLAIN),注释称"服务端最终安全边界,必须与前端校验一致且更严格")。
server/src/modules/knowledge/ 下按子域分目录:knowledge-bases(知识库 CRUD + 回收站 + 设置/可见性)、documents(文档全家桶:树/搜索/最近/回收站/模板/版本/回滚/分享/浏览记录,端点约 25 个)、comments(文档评论:增删改查 + resolve/unresolve)、favorites(文档收藏:列表/exists/增删)、notifications(通知:未读数/标记已读/批量删除)、snippets(代码片段:集合树/标签/批量打标/批量删除/版本/回滚/use/copy,其中集合与片段的"公共化"由迁移 1786000000000-AddPublicSnippetSupport 的 is_public 列支撑)、board-library(画板库:GET/PUT 整库 + assets/upload)。除 snippets 控制器类级无守卫(逐端点叠加 RequireAuthGuard)外,其余子模块全部类级 @UseGuards(RequireAuthGuard)——整个域强制登录。15 个领域实体 + RefreshToken + CaptchaChallenge 两个认证实体都定义在 modules/knowledge/entities/(后两者的归属位置属于历史放置问题,见《13-技术债务》第 6 条)。该域在 html5-examples 前端与三个姊妹项目源码中均无消费方,接口契约详表见《15-接口全量参考》§2.9。
前端侧的依赖方向严格单向:views/css-lab/* → components/css-lab + generators/* → config/css-lab(catalog/routes/categories) → types/tool;views/* → composables/* → api/* → utils/*。后端侧则全部汇聚到 app.module.ts:模块间唯一横向依赖是 auth(全局守卫与各模块叠加的 guards)与 knowledge 实体被 auth.service 反向引用(RefreshToken)。理解这两张依赖图后,任何"改 A 会不会影响 B"的问题都可以先看依赖方向再定论——上游改契约才需要排查下游,下游实现变更对上游透明。
本篇完。下一篇:08-安全与权限设计.md