第2章 项目技术栈选择

2.1 项目架构与工程组织

技术栈选择本身不是重点,真正的重点在于"用什么技术取决于需求需要什么",而不是这项技术本身有多少优点。

求职阶段,我会优先选择应聘方向所需的主流技术栈来实现需求,因为面试考察的是能否胜任团队现有的技术语境,而不是是否掌握了最前沿的方案。

进入企业之后,技术选型更多取决于业务所处的阶段和团队能承受的试错成本——业务越依赖稳定运行,团队的"技术冒险额度"就越有限,往往优先选最稳妥、出问题概率最低的方案;但如果业务处于快速迭代期、或需要靠技术差异化建立竞争力,团队愿意承担的风险预算也会相应更高。所以不是简单地"求职选主流、在职选稳定",而是要看清当前所处的阶段,把有限的风险预算留给真正需要创新突破的地方。

新技术往往比旧技术带来更多特性、更高效率,但要不要用,首先取决于团队是否真正掌握它,其次取决于当前需求是否真的必须依赖这个新特性才能实现,最后取决于万一出问题,团队有没有能力兜底。新技术的生态往往还不完善,遇到问题未必有现成文档和社区经验可查,能否接受这种不确定性、以及需求是否紧迫到必须冒险,是决定要不要采用的关键。

所有技术都是为需求服务的,因为需求需要用到这项技术,所以才用它,而不是为了用某个技术反过来给自己制造一个需求。不过需求也不只限于"当下已经暴露出来的问题":对于可预见的业务增长,提前做一些有依据、可验证的架构准备,也属于合理的需求范畴,只要它建立在具体的增长趋势和数据支撑之上,而不是单纯"想用某个新技术"倒推出来的理由,依然算是需求驱动而非技术驱动。

如果有人问我"为什么选择这项技术栈",我不会只说"因为需求需要"这一句就结束,而是会说这个需求需要什么样的能力(比如高并发下的类型安全、快速迭代效率、和团队现有技术栈的兼容成本),而这项技术恰好在这个具体维度上有优势,所以才选它。不是因为它整体先进,而是因为这个具体特性正好对上了这个具体的需求痛点。聊"为什么选这项技术",本质上聊的是需求、团队现状和技术特性三者之间如何精确匹配,而不是回避讨论技术本身的优点。

2.1.1 系统总体架构

LearnWise 是一个融合英语课程学习、单词复习、AI 对话、语音交互、学习总结和在线支付的 Web 应用。此类系统既包含用户、课程、订单等结构稳定的传统业务,也包含大模型流式输出、智能体工具调用和异步报告生成等 AI 业务。若全部功能集中在单一服务中,虽然初期开发简单,但常规接口与耗时较长的模型请求会共享运行资源,模块边界也容易变得模糊。

项目因此采用前后端分离架构,并将服务端进一步划分为业务服务和 AI 服务。Vue 前端负责页面呈现和用户交互;NestJS 业务服务负责用户、课程、单词本、学习记录和支付;NestJS AI 服务负责模型调用、对话状态、联网搜索与学习报告生成;PostgreSQL 保存业务数据和 AI 检查点;Redis 与 BullMQ 承担延迟任务;MinIO 保存头像等对象文件。

这一设计尚未达到完整微服务架构。两个后端应用仍位于同一代码仓库并共享基础模块,因此更准确的说法是“模块化单体基础上的应用级拆分”。它减少了微服务注册发现、链路追踪和分布式事务等额外成本,同时为 AI 服务独立部署和扩容保留了空间。

2.1.2 TypeScript 全栈与前后端分离

项目的前端、业务服务、AI 服务和共享类型均使用 TypeScript。与 JavaScript 相比,TypeScript 能在编译阶段检查参数、返回值和对象结构,尤其适合接口较多、数据模型复杂的全栈项目。课程、用户、单词和聊天消息等结构可以在 workspace 包中共享,减少前后端各自声明类型造成的不一致。

TypeScript 的代价是增加类型设计和编译配置成本,第三方库类型不完整时也需要额外处理。但本项目同时使用 Vue、NestJS、Prisma 和 LangChain,这些技术均具有较好的 TypeScript 支持,因此统一语言带来的维护收益明显高于额外成本。

前后端分离使前端可以独立构建和部署,并通过 /api/ai 两类入口访问不同服务。开发环境由 Vite 代理隐藏端口差异,生产环境则可由反向代理统一暴露服务。该方式也使 REST、SSE 和 Socket.IO 能按照各自场景独立演进。

2.1.3 pnpm Workspace 与 NestJS Monorepo

项目外层使用 pnpm Workspace 管理 appsserverpackages。相较 npm,pnpm 通过内容寻址存储和链接机制减少重复依赖占用,并对未声明依赖的访问更加严格;相较 Yarn Workspace,pnpm 配置直接、安装性能较好,适合中小型 TypeScript monorepo。

packages/common 用于共享业务类型,packages/config 用于共享端口等配置。后端内部又使用 NestJS Monorepo,将业务应用、AI 应用和共享库组织在同一工程中。两层 monorepo 的优点是代码复用和统一开发体验,缺点是构建边界容易复杂化,因此应保持共享包职责单一,避免将具体业务逻辑放入公共模块。

2.2 前端核心框架选型

2.2.1 Vue 3、React 与 Angular 对比

Vue 3 是本项目的前端核心框架。它采用响应式数据系统、单文件组件和 Composition API,适合将页面拆分为组件、状态和可复用逻辑。项目已将登录、聊天、课程练习、消息气泡等功能组织为组件,并将语音、Socket、登录和滚动控制封装为组合式函数。

React 生态规模更大,灵活性更强,但路由、状态和组件方案通常需要团队自行组合;Angular 提供完整且严格的企业级框架能力,但学习成本和工程体量相对较高。Vue 3 在渐进式使用、模板可读性和开发复杂度之间更均衡,符合本项目团队规模和交互型应用的需求。

项目选择 Vue 3 还因为 Element Plus、Pinia、Vue Router 和 Vite 等配套方案成熟。需要注意的是,Composition API 若缺乏统一规则,也可能出现单个组件逻辑过长的问题,因此项目通过 composables 和业务组件继续拆分复杂页面。

2.2.2 Vite 构建工具选型

Vite 使用浏览器原生 ES Module 提供快速开发启动,并通过 Rollup 完成生产构建。与传统 Webpack 全量打包后再启动的方式相比,Vite 在开发阶段只按需转换被请求的模块,热更新速度更快,配置也更精简。

项目通过 Vite 集成 Vue、Tailwind CSS、Vue DevTools 和 SVG Loader,同时配置 /api/ai 代理。Vite 还提供 @src 的路径别名,使组件和工具模块的引用更清晰。对于当前规模的 Vue 单页应用,Vite 比维护复杂 Webpack 配置更合适。

2.2.3 Vue Router、Pinia 与状态持久化

Vue Router 管理首页、聊天、课程、设置和单词本等页面。显式路由配置能够清晰表达页面关系,并支持后续增加鉴权守卫和懒加载。相比基于目录自动生成的文件路由,它需要手动维护,但对当前页面数量而言更加直观。

Pinia 管理用户信息和登录状态。与 Vuex 相比,Pinia API 更简洁,对 TypeScript 推导更友好,也不需要 mutation 层。项目通过 pinia-plugin-persistedstate 保存必要状态,使页面刷新后仍能恢复用户会话。持久化数据应限制在必要字段,敏感信息不应直接长期存放在浏览器中。

2.3 UI、样式与可视化扩展

2.3.1 Element Plus 与 Tailwind CSS 混合方案

项目没有完全依赖单一 UI 方案,而是使用 Element Plus 提供表单、消息提示和通用图标,同时使用 Tailwind CSS、原生 CSS 和 scoped CSS 完成业务界面。Element Plus 能降低表单校验、反馈提示等常规功能的开发成本;Tailwind CSS 适合快速组合布局;原生 CSS 则便于实现高度定制的聊天、课程和首页视觉效果。

Ant Design Vue 更偏企业后台风格,Naive UI 的 TypeScript 体验和主题能力较好,但项目已采用 Element Plus,且其中文生态成熟。Tailwind 与 Sass、CSS Modules 相比不强调预处理语法,而是通过原子类快速构建样式。混合方案兼顾效率与定制能力,不过也会产生样式来源分散的问题,因此应统一颜色、间距和断点变量,避免同类样式重复实现。

2.3.2 自定义 SVG 组件化方案

项目为登录、聊天、导航、弹窗和设置等模块设计了多组 SVG 图标,并通过 vite-svg-loader.svg 文件直接导入为 Vue 组件。相比 PNG,SVG 在任意缩放比例下仍保持清晰,可以通过 CSS 控制尺寸和部分颜色;相比 Icon Font,SVG 不存在字体加载闪烁和字符映射问题,也更适合多色图标。

构建阶段使用 SVGO 压缩冗余属性,同时显式保留 viewBox,确保图标可以响应式缩放。图标按业务分类并通过各目录的 index.ts 集中导出,降低页面对具体文件路径的依赖。项目同时保留 Element Plus Icons,用于无需定制的通用操作图标,形成“组件库图标负责通用语义,自定义 SVG 负责产品视觉”的组合方式。

2.3.3 Three.js 与 glTF 三维模型展示

登录界面使用 Three.js 渲染本地 glTF 模型,并通过 GLTFLoader 加载模型、二进制数据和纹理,通过 OrbitControls 提供观察交互。Three.js 对原生 WebGL 的渲染流程进行了封装,可直接使用场景、相机、材质和灯光;与 Babylon.js 相比,它更轻量、生态广泛,也更适合在现有 Vue 页面中嵌入单个展示场景。

glTF 是面向实时渲染的三维资产格式,能够同时描述网格、材质、纹理和场景关系,比直接解析 OBJ 等格式更适合 Web。项目还对模型包围盒、中心位置、相机距离、环境光和阴影进行了调整。三维渲染会增加首屏资源体积和 GPU 消耗,因此需要按需加载,并在组件卸载时释放几何体、材质、纹理和渲染器资源。

2.3.4 CSS 动画、自定义指令与组合式函数

项目使用 CSS 动画完成文字散落、页面揭示和过渡效果,并通过自定义指令封装自动聚焦和元素进入视口后的呈现行为。与将所有动画交给 JavaScript 相比,CSS 动画更容易由浏览器优化,也能减少主线程计算。

登录、语音、Socket、事件监听、头像和滚动锁定等能力被封装为组合式函数。这种设计让组件专注于模板和业务流程,同时提高逻辑复用性。对于监听器和动画帧,应在组件卸载时统一清理,避免页面切换后仍有后台任务运行。

2.4 网络请求与实时通信选型

2.4.1 Axios 与 REST API

用户、课程、学习记录、单词本和支付等常规业务通过 REST API 交互,前端使用 Axios 封装请求。Axios 内置请求与响应转换、拦截器、超时和取消支持,比原生 Fetch 更适合建立统一客户端。项目分别创建业务 API 和 AI API,并通过拦截器添加认证信息、处理错误和刷新 Token。

REST 资源模型清晰,便于调试、缓存和接口文档化,适用于一次请求对应一次完整响应的业务。然而它不适合持续推送 AI 文本或服务端主动通知,因此项目没有试图用单一通信方式覆盖所有场景。

2.4.2 SSE 流式响应

AI 回答使用 Server-Sent Events 流式返回。SSE 基于 HTTP 长连接,服务端可以持续向浏览器推送文本事件,协议简单,并具备断线处理基础。项目采用 @microsoft/fetch-event-source,使 SSE 请求能够使用 POST、自定义 Header 和 JSON 请求体,弥补原生 EventSource 只能方便地发起 GET 请求的限制。

与 WebSocket 相比,SSE 只提供服务端到客户端的单向推送,但 AI 对话中用户输入本身可通过初始 HTTP 请求发送,后续主要是模型持续输出,因此单向模型恰好满足需求。它也比短轮询减少重复请求和额外延迟。

2.4.3 Socket.IO 双向通信

项目使用 Socket.IO 在支付结果发生变化时主动通知前端。Socket.IO 在 WebSocket 之上提供事件语义、自动重连、心跳和兼容性回退,开发成本低于直接维护原生 WebSocket 协议。支付回调由服务端异步接收,前端无法预知完成时刻,因此实时连接比固定轮询更及时。

Socket.IO 的代价是客户端和服务端都要引入额外协议层,并不与原生 WebSocket 客户端完全兼容。当前项目只在确有双向或主动通知需求时使用它,避免所有接口都维持长连接。

2.4.4 REST、SSE 与 Socket.IO 的协同

三种通信方案在项目中按职责组合:REST 处理确定性业务请求,SSE 处理 AI 单向流式输出,Socket.IO 处理支付状态等实时事件。这种选型比强行统一为 WebSocket 更容易开发和维护,也使接口语义更加明确。后续若实时协作功能显著增多,可再扩大 Socket.IO 的职责;若只有少量服务端通知,则应继续控制长连接范围。

2.5 浏览器能力与内容呈现

2.5.1 Web Speech API 语音交互

项目通过 SpeechRecognition 实现语音转文字,通过 SpeechSynthesis 和 SpeechSynthesisUtterance 朗读单词、例句或回答。相比调用云端语音服务,浏览器原生方案无需上传音频、接入成本低,也不会产生额外 API 费用,适合教学演示和基础发音辅助。

其局限是不同浏览器和操作系统的支持程度、可用音色和识别质量不一致。项目需要在调用前检测能力,并在不支持时回退到文本输入或隐藏语音按钮。若未来需要统一的发音质量、音素评分或口语测评,则应接入专业云端语音服务。

2.5.2 Marked 与流式 Markdown 渲染

AI 回答天然包含标题、列表和代码等结构,项目使用 Marked 将 Markdown 转换为 HTML,并分别渲染推理内容和最终回答。Marked 体积较小、解析速度快,适合聊天场景;markdown-it 插件体系更灵活,Remark 则更适合基于语法树进行复杂转换。当前需求以快速展示为主,因此 Marked 足够直接。

流式内容可能在任意位置截断 Markdown 语法,前端需要容忍不完整片段并在后续数据到达后重新解析。由于最终 HTML 会进入页面,生产环境还应结合 DOMPurify 等工具进行清洗,避免模型输出或外部搜索内容形成跨站脚本风险。

2.6 后端框架与服务设计

2.6.1 NestJS 框架选型

后端使用 NestJS 11。NestJS 在 Express 之上提供模块、控制器、服务、守卫、拦截器和依赖注入,使用户、课程、支付、AI 等模块可以遵循统一结构。相比直接使用 Express 或 Koa,它的约束更多,但能减少大型项目中路由、依赖和错误处理方式不一致的问题。

Fastify 在吞吐性能方面通常更有优势,NestJS 也可以更换为 Fastify Adapter。不过本项目的主要瓶颈更可能来自数据库、模型 API 和外部服务,而非 HTTP 框架本身,因此优先选择生态成熟、团队易理解的 Express Adapter 更合理。

2.6.2 业务服务与 AI 服务拆分

业务服务负责账户、课程、学习记录、单词本、订单和支付;AI 服务负责 DeepSeek 调用、会话记忆、联网搜索和学习总结。拆分后,AI 请求的长连接和较长执行时间不会直接混入常规业务模块,后续也可以针对模型并发单独配置资源。

与完整微服务相比,当前方案共享仓库、配置和公共库,没有引入消息总线作为所有模块的通信基础。这降低了部署和排错复杂度,适合项目当前阶段。需要避免两个服务直接复制业务逻辑,公共基础能力应通过 shared library 复用,业务数据仍应由明确的服务边界负责。

2.6.3 模块化、依赖注入与统一异常响应

NestJS Module 组织 Prisma、JWT、邮件、MinIO、支付和队列等能力,依赖注入让业务服务无需自行创建底层客户端。全局拦截器负责统一成功响应,全局异常过滤器负责将错误转换为稳定结构,RxJS 则参与 Guard 和 Interceptor 的异步处理。

统一响应便于前端封装,但应保留正确的 HTTP 状态码,不能只在响应体中表达成功或失败。DTO 目前仍有继续完善的空间,后续可结合 class-validator 对外部输入进行运行时校验。

2.7 数据库与数据访问层

2.7.1 PostgreSQL 数据库选型

项目使用 PostgreSQL 保存用户、课程、单词、学习记录、订单和每日总结。此类数据之间关系明确,并涉及唯一约束、事务和按用户聚合查询,因此关系型数据库比 MongoDB 等文档数据库更匹配。PostgreSQL 在复杂查询、索引、JSON 扩展和事务能力方面较强,也能被 LangGraph 用作 AI 检查点存储。

MySQL 同样可以完成主要业务,但 PostgreSQL 对复杂数据类型和扩展功能支持更丰富。选择 PostgreSQL 使业务数据与 AI 对话持久化可以使用同类基础设施,不过二者仍应通过独立数据库或 Schema 隔离,防止生命周期和权限相互影响。

2.7.2 Prisma ORM、迁移与种子数据

Prisma 通过 Schema 声明模型并生成类型安全客户端。与 TypeORM 的装饰器实体方式相比,Prisma 的模型定义更集中,查询结果类型推导清晰;与 Sequelize 相比,其 TypeScript 开发体验更现代。项目还使用 PostgreSQL Adapter 建立连接,通过 Prisma Migrate 管理结构变化,通过 Seed 初始化词库、课程和图片数据。

Prisma 的限制是复杂 SQL 和特定数据库能力有时仍需使用原生查询,生成客户端也会增加构建步骤。对本项目以 CRUD、关系查询和事务为主的数据访问场景而言,其开发效率和类型安全更有价值。

2.7.3 关系建模、约束与索引设计

数据库围绕用户、单词、用户单词记录、复习日志、课程记录、支付记录和学习总结建立关系。用户与单词的学习状态需要按 (userId, wordId) 唯一,因此使用复合唯一约束;到期复习按照用户和下次复习时间查询,因此设置 (userId, nextReviewAt) 复合索引。

这些约束不仅提升查询性能,也把关键业务规则下沉到数据库,避免并发请求产生重复记录。关联记录使用级联删除时需要谨慎,尤其是支付和邮件日志等审计数据,后续可根据合规要求改为软删除或限制删除。

2.8 AI 模型与智能体技术

2.8.1 DeepSeek 模型选型

项目通过 @langchain/deepseek 接入 DeepSeek,分别配置普通对话和深度思考模式,并启用流式输出。普通模式适合日常问答、解释和练习反馈,深度思考模式适合复杂分析。模型温度、最大输出长度和 thinking 参数根据场景分别设置。

模型选型通常需要比较推理能力、中文与英文表现、延迟、上下文长度、价格和 API 稳定性。DeepSeek 在中文语境、推理能力和使用成本之间具有较好的平衡,也提供与 LangChain 兼容的接口。其风险是外部模型服务存在延迟、限流和不可用情况,因此服务端应配置超时、错误转换和必要的重试策略,并避免把模型供应商细节扩散到业务层。

2.8.2 LangChain Agent 与工具调用

项目使用 LangChain 的 createAgent 构建智能体,并将联网搜索、学习数据读取等能力封装为 Tool。相比直接调用模型 API,LangChain 提供统一的消息、流式输出、工具调用和 Agent 抽象,适合需要模型自主选择工具的场景。

如果应用只是单轮问答,直接调用 API 会更轻、更容易调试。当前系统不仅要聊天,还要生成基于用户数据的学习复盘和进行联网搜索,因此 Agent 抽象具有实际价值。仍应控制工具数量和参数范围,并在服务端校验工具输入,避免模型拥有不必要的数据访问能力。

2.8.3 LangGraph 对话记忆与持久化

项目使用 LangGraph PostgreSQL Checkpoint 保存 Agent 状态,并按对话线程恢复上下文。相较只把历史消息保存在前端,这种方式在刷新页面或更换设备后仍能继续会话,也避免客户端篡改完整上下文。

持久化记忆会持续增长,应设置历史裁剪、摘要或归档策略。对话数据还可能包含用户隐私,需要按照用户维度隔离查询,并明确保留和删除规则。

2.8.4 Prompt、联网搜索与自定义 AI Skill

项目维护不同英语学习角色和每日复盘提示词,并通过博查搜索 API 为 Agent 提供联网信息。搜索结果被整理为标题、链接、摘要、站点和时间,再作为模型上下文。这属于搜索增强生成,与基于私有文档向量检索的传统 RAG 不同:它强调实时公开网页,而不是构建本地知识库。

每日学习复盘被封装为自定义 AI Skill,结合用户学习记录、Prompt、模型和邮件服务生成个性化总结。将能力封装为 Skill 有利于隔离提示词、输入类型和执行流程。联网内容并不天然可靠,后续应保留来源链接、限制不可信指令进入系统提示词,并对关键学习结论进行结构化校验。

2.9 英语学习核心算法

2.9.1 SM-2 间隔重复算法

项目自行实现 SM-2 间隔重复算法,根据回答质量调整难度系数、连续正确次数和下次复习间隔。相比每天固定复习相同单词,SM-2 会让熟悉内容的间隔逐步增长,让不熟悉内容更快重新出现,从而在有限学习时间内提高复习效率。

选择自行实现而不是引入大型学习算法库,是因为 SM-2 公式相对明确,且项目需要将状态直接映射到自己的单词记录模型中。缺点是经典 SM-2 对不同用户、词汇难度和学习场景的适应能力有限,后续可基于实际正确率校准评分映射,或评估 FSRS 等现代调度算法。

2.9.2 掌握度计算与复习调度

每个用户对每个单词都保存独立的 easeFactorintervalrepetitionsnextReviewAtlastReviewAt,同时记录正确次数、错误次数和连续答对次数。这符合记忆状态属于“用户与单词关系”而非单词本身的业务事实。

服务端按照用户和到期时间查询待复习单词,数据库复合索引保证调度查询效率。掌握状态由学习记录推导,而不是允许用户随意切换,使单词本、错词本和复习计划使用同一数据来源。

2.10 异步任务与基础设施

2.10.1 Redis 与 BullMQ 任务队列

项目使用 Redis 作为 BullMQ 的状态存储,通过 @nestjs/bullmq 注册队列、Worker 和 Processor。每日学习总结可能涉及数据库聚合、模型生成和邮件发送,不适合阻塞普通 HTTP 请求,因此采用异步任务处理。

RabbitMQ 提供成熟的消息路由和确认机制,Kafka 更适合高吞吐事件流;BullMQ 基于 Redis、与 Node.js 和 NestJS 集成直接,适合当前规模的延迟任务和后台作业。其前提是正确处理重试、幂等性和失败任务,防止同一用户重复生成或发送报告。

2.10.2 MinIO 对象存储

用户头像等文件通过 MinIO 保存。与直接写入应用服务器磁盘相比,对象存储更容易独立扩容和统一访问;与公有云对象存储相比,MinIO 可以本地部署,并提供接近 S3 的接口,适合开发和可控部署环境。

服务端使用 MinIO SDK 管理 Bucket、上传对象并生成访问地址。生产环境还应限制 MIME 类型和文件大小,采用不可预测的对象名,并根据隐私需求使用签名 URL,而不是默认公开所有文件。

2.10.3 邮件与每日学习报告

项目使用 Nodemailer 通过 SMTP 发送 HTML 邮件。AI 先生成 Markdown 学习总结,Marked 再将其转换为 HTML,BullMQ 负责按计划执行生成与发送。相比第三方邮件 API,SMTP 接入通用、迁移成本低;邮件 API 在送达率、统计和模板管理方面通常更强。

数据库保存学习报告和邮件日志,便于追踪任务状态并实施幂等控制。邮件正文仍需进行 HTML 清洗,同时应对模型失败、邮件失败分别记录,避免将部分成功误判为任务全部完成。

2.11 鉴权、支付与安全设计

2.11.1 JWT 双令牌鉴权

项目使用 JWT Access Token 和 Refresh Token。短期 Access Token 用于访问接口,过期后由 Refresh Token 获取新令牌,前端 Axios 拦截器负责刷新和重试。与服务器 Session 相比,JWT 减少共享会话存储需求,适合前后端分离和多服务验证。

JWT 签发后难以即时撤销,因此 Refresh Token 应支持服务端失效控制、轮换和异常复用检测。前端还要避免多个并发请求同时触发刷新,可通过刷新队列合并请求。

2.11.2 支付宝支付与实时通知

支付模块使用支付宝 SDK 创建网页支付请求,并通过异步回调更新订单。Nano ID 用于生成业务订单标识,支付成功后 Socket.IO 主动通知前端。该流程比仅依赖支付页面跳转结果可靠,因为最终状态以支付宝服务端回调为准。

回调处理必须验证签名、金额、商户应用和订单状态,并保证同一通知重复到达时结果一致。订单更新和权益发放应处于同一事务或使用可靠的幂等状态机。

2.11.3 当前安全方案及改进方向

当前前端使用 MD5 对密码摘要后再发送。MD5 已不适合作为密码安全存储算法:计算速度过快且无法抵抗现代暴力破解。如果数据库直接保存该摘要,即使网络传输使用 HTTPS,泄露后的破解风险仍然较高。

改进方案应由服务端使用 Argon2id 或 bcrypt 加随机盐保存密码,前端通过 HTTPS 传输原始密码或协议要求的临时凭据。除此之外,还应为 Markdown HTML 增加清洗、为上传增加校验、为登录和模型接口增加限流,并确保 .env 与密钥不会进入版本控制和日志。

2.12 工程质量与测试体系

2.12.1 代码规范、格式化与类型检查

后端使用 ESLint、typescript-eslint 和 Prettier,前端使用 Oxfmt,并通过 vue-tsc 检查 Vue 单文件组件类型。Oxfmt 追求更快的格式化速度,Prettier 的生态和稳定性更成熟;两者分别作用于不同子工程不会产生直接冲突,但长期最好统一格式规则和提交检查流程。

项目还使用路径别名简化模块引用,使用 Vue DevTools 调试前端,并通过 npm-run-all2 并行执行类型检查和构建。根脚本使用 concurrently 同时启动三个应用,提升本地联调效率。

2.12.2 Jest、Supertest 与测试现状

后端已配置 Jest、ts-jest、NestJS Testing 和 Supertest,具备单元测试与端到端测试基础。Jest 适合测试 Service 和算法,Supertest 适合验证控制器、鉴权和完整 HTTP 流程。当前仓库中的实际测试仍以少量脚手架测试为主,覆盖度不足。

后续应优先覆盖风险较高的 SM-2 边界、Token 刷新、支付回调幂等、BullMQ 重试、AI 流式事件解析和 Prisma 事务。前端可补充 Vitest 与 Vue Test Utils,并使用端到端测试覆盖登录、学习、聊天和支付状态变化。

2.13 技术选型总结

2.13.1 技术栈协作关系与选型优势

项目的核心特点不是简单堆叠 Vue、NestJS 和 PostgreSQL,而是针对不同业务性质选择不同技术:Vue 3 与 Pinia 负责交互界面,Vite 负责开发和构建;REST 处理常规业务,SSE 传输模型流,Socket.IO 推送异步支付状态;NestJS 提供模块化服务端结构,Prisma 和 PostgreSQL保证业务数据一致性;LangChain、LangGraph 与 DeepSeek构成智能体能力;Redis、BullMQ、MinIO 和 Nodemailer 支撑异步任务、文件和邮件。

该方案在开发效率、类型安全、交互体验和 AI 扩展能力之间取得了较好平衡。业务服务与 AI 服务的拆分也使系统可以逐步演进,而不必在项目早期承担完整微服务架构的成本。

2.13.2 局限性与后续演进方向

当前技术体系仍有若干改进点:MD5 密码方案需要替换;Markdown 渲染需要增加 HTML 清洗;DTO 运行时校验和接口限流尚需完善;测试覆盖不足;Three.js 资源和流式连接需要持续关注清理;AI 调用需要更完整的超时、重试、用量统计和供应商降级方案;队列任务需要严格的幂等与失败补偿。

此外,根依赖中的 Day.js 暂未在主要源码中发现明确使用,部分示例 Store 和测试也属于脚手架遗留。技术文档应区分“实际使用”“已集成但使用较少”和“仅安装未使用”,避免把依赖清单直接等同于技术栈。后续演进应以真实业务瓶颈为依据,而不是为了技术数量继续拆分服务或引入新的基础设施。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP