# learn-words **Repository Path**: jetydu/learn-words ## Basic Information - **Project Name**: learn-words - **Description**: 背单词 - **Primary Language**: C# - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-08 - **Last Updated**: 2026-07-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 单词记忆系统 · 开发文档 > 本文档面向开发者,介绍项目的架构、环境搭建、目录结构、数据库设计、认证机制、API 接口、前端结构、核心业务逻辑与部署方式。 --- ## 一、项目概览 单词记忆系统是一款轻量化英语学习工具,覆盖「单词录入 → 背诵 → 默写 → 选词测试 → 学习统计」全流程。系统支持**多用户登录**,所有学习数据按用户隔离。 ### 核心特性 - 自定义单词批量录入,后端自动完成格式规整(去 `*` 号、音标加 `[]`)。 - 沉浸式背诵 / 默写 / 选词测试三种学习模式。 - 学习数据统计:完成率、正确率、薄弱单词 TOP、近 7 天趋势。 - 每日学习量(背诵、默写、测试共享)可统一配置。 - 基于 JWT 的用户认证,数据按 `UserId` 隔离。 --- ## 二、技术栈 | 层 | 技术 | 说明 | | --- | --- | --- | | 前端 | Vue 3(组合式 API) | 视图层框架 | | 前端 | Vite 8 | 构建与开发服务器 | | 前端 | Vue Router 4 | 前端路由 + 导航守卫 | | 前端 | Tailwind CSS 4 | 样式方案 | | 前端 | TypeScript | 类型安全 | | 后端 | .NET 10(ASP.NET Core Web API) | RESTful 接口服务 | | 认证 | JWT Bearer(Microsoft.AspNetCore.Authentication.JwtBearer) | 无状态登录态 | | 数据库 | SQLite | 文件型数据库,免部署 | | ORM | FreeSql 3.5.310(`UseAutoSyncStructure`) | 自动建表/同步结构 | | 文档 | Swashbuckle.AspNetCore(Swagger UI) | 接口调试 | > 端口约定:后端 `http://localhost:5077`,前端开发服务器 `http://localhost:5173`(已配置 `/api` 代理转发到后端)。 --- ## 三、环境搭建 ### 3.1 前置依赖 - [.NET 10 SDK](https://dotnet.microsoft.com/) - [Node.js 18+](https://nodejs.org/)(含 npm 或 pnpm) ### 3.2 后端启动 ```powershell cd backend\WordMemory.Api dotnet restore dotnet run ``` 启动后: - API 基址:`http://localhost:5077/api` - Swagger 文档:`http://localhost:5077/swagger` > FreeSql 会在首次访问时自动创建 `words`、`settings`、`users` 表(依据实体 `UseAutoSyncStructure`)。数据库文件为 `wordmemory.db`(位于后端项目运行目录)。 ### 3.3 前端启动 ```powershell cd frontend npm install # 或 pnpm install npm run dev # 开发模式,访问 http://localhost:5173 npm run build # 生产构建(含 vue-tsc 类型检查 + vite build) ``` ### 3.4 数据库配置 后端 `appsettings.json`: ```json "ConnectionStrings": { "DefaultConnection": "Data Source=wordmemory.db" } ``` 可直接修改为绝对路径或迁移到其它 SQLite 文件。 --- ## 四、目录结构 ``` 背单词/ ├─ prompt.md # 功能需求文档(业务视角) ├─ backend/ │ └─ WordMemory.Api/ │ ├─ Controllers/ # HTTP 接口层 │ │ ├─ AuthController.cs # 注册 / 登录(公开接口) │ │ ├─ WordsController.cs # 单词增删改查、背诵、默写 │ │ ├─ QuizController.cs # 选词测试 │ │ ├─ StatsController.cs # 学习统计 │ │ └─ SettingController.cs # 系统设置(每日学习量) │ ├─ Services/ # 业务逻辑层 │ │ ├─ WordService.cs # 单词/设置/统计核心逻辑 │ │ └─ UserService.cs # 注册/登录/JWT 生成 │ ├─ Models/ # 数据实体(映射数据库表) │ │ ├─ Word.cs # words 表 │ │ ├─ Setting.cs # settings 表 │ │ └─ User.cs # users 表 │ ├─ Dtos/ # 请求/响应数据契约 │ │ ├─ AuthDtos.cs # RegisterRequest / LoginRequest / AuthResponse │ │ ├─ AddWordRequest.cs │ │ ├─ UpdateWordRequest.cs │ │ ├─ BatchDeleteRequest.cs │ │ ├─ MemorizeRequest.cs │ │ ├─ QuizSubmitRequest.cs # 含 CorrectOptionIndex(修复统计错误) │ │ ├─ QuizGenerateRequest.cs │ │ ├─ SettingDto.cs │ │ └─ WordResponse.cs │ ├─ appsettings.json # 连接串 + JWT 配置 │ └─ Program.cs # 服务注册、JWT、CORS、中间件 └─ frontend/ ├─ src/ │ ├─ api/index.ts # 所有接口封装 + Token 注入 + 401 拦截 │ ├─ router/index.ts # 路由表 + 登录守卫 │ ├─ types/index.ts # 前后端共享 TS 类型 │ ├─ App.vue # 布局壳 + 导航 + 用户信息/退出 │ └─ views/ │ ├─ Login.vue # 登录/注册(合一) │ ├─ WordList.vue # 单词列表 │ ├─ Memorize.vue # 背诵模式 │ ├─ Dictation.vue # 默写模式 │ ├─ Quiz.vue # 选词测试 │ ├─ Stats.vue # 学习统计 │ └─ SystemSettings.vue # 系统设置(每日学习量 + 新增单词) └─ vite.config.ts # 端口 + /api 代理 ``` --- ## 五、数据库设计 ### 5.1 表关系 ``` users (1) ──< (N) words users (1) ──< (N) settings ``` 所有业务数据均通过 `UserId` 字段归属到具体用户,实现数据隔离。 ### 5.2 `users` 表(User.cs) | 字段 | 类型 | 说明 | | --- | --- | --- | | Id | int (PK, 自增) | 用户 ID | | Username | TEXT | 用户名(唯一) | | PasswordHash | TEXT | 密码 SHA256 哈希(**不存明文**) | | CreateTime | datetime | 注册时间 | ### 5.3 `words` 表(Word.cs) | 字段 | 类型 | 说明 | | --- | --- | --- | | Id | int (PK, 自增) | 单词 ID | | WordText | TEXT | 纯单词文本(已去 `*` 号) | | Phonetic | TEXT | 标准化音标(带 `[]`) | | Meaning | TEXT | 中文释义 | | IsMemorized | bool | 是否已掌握 | | MemorizeCount | int | 背诵次数 | | ErrorCount | int | 答错次数 | | CreateTime | datetime | 添加时间 | | LastMemorizeTime | datetime? | 最后学习/背诵时间 | | UserId | int | 所属用户 ID(数据隔离键) | ### 5.4 `settings` 表(Setting.cs) | 字段 | 类型 | 说明 | | --- | --- | --- | | Id | int (PK, 自增) | 设置 ID | | Key | TEXT (唯一索引) | 设置键名,如 `dailyMemorizeCount` | | Value | TEXT | 设置值(字符串存储) | | Description | TEXT? | 描述 | | UpdateTime | datetime | 更新时间 | | UserId | int | 所属用户 ID | > 当前使用设置键:`dailyMemorizeCount`(每日学习量,背诵/默写/测试共享,默认 10,范围 1–100)。 --- ## 六、认证与授权机制 ### 6.1 流程 1. 用户在 `/login` 页通过 `POST /api/auth/register` 或 `/api/auth/login` 获取 `token`。 2. 后端用 `UserService` 校验凭证,签发 **JWT**(含 `NameIdentifier=UserId`、`Name=Username` 声明,有效期 168 小时)。 3. 前端将 `token` 存入 `localStorage`,后续请求经 `api/index.ts` 的 `request()` 自动附加 `Authorization: Bearer ` 头。 4. 后端除 `AuthController` 外,所有 Controller 标注 `[Authorize]`,从 `User.Claims` 提取 `UserId` 传入 `WordService`。 5. 若返回 `401`,前端清除 token 并跳转 `/login`。 ### 6.2 JWT 配置(appsettings.json → Jwt) ```json "Jwt": { "SecretKey": "wordmemory_jwt_secret_key_2024_must_be_long_enough", "Issuer": "WordMemory", "Audience": "WordMemoryUsers", "ExpirationHours": "168" } ``` > 生产环境务必替换 `SecretKey` 为强随机串,避免泄露导致越权。 ### 6.3 密码安全 `UserService.HashPassword` 使用 **SHA256** 对密码做单向哈希存储,登录时同样哈希后比对。当前未引入盐值(salt),如需更高安全性可后续升级为 `PBKDF2` / `bcrypt`。 ### 6.4 路由守卫 `router/index.ts` 中 `beforeEach` 判断:未携带 token 访问受保护路由 → 跳转 `/login`;已登录访问 `/login` → 跳转 `/list`。 --- ## 七、后端 API 接口 统一响应结构: ```json { "code": 200, "data": <业务数据>, "message": "<可选提示>" } ``` - 成功:`code = 200` - 参数错误:`code = 400` - 未授权:`code = 401`(HTTP 401) - 冲突(如用户名已存在):`code = 409` > 除认证接口外,所有接口均需在请求头携带 `Authorization: Bearer `。 ### 7.1 认证 AuthController(`/api/auth`) | 方法 | 路径 | 说明 | 鉴权 | | --- | --- | --- | --- | | POST | `/api/auth/register` | 注册,body: `{username, password}`,返回 `AuthResponse` | 公开 | | POST | `/api/auth/login` | 登录,body: `{username, password}`,返回 `AuthResponse` | 公开 | `AuthResponse`:`{ userId, username, token }` ### 7.2 单词 WordsController(`/api/words`) | 方法 | 路径 | 说明 | | --- | --- | --- | | POST | `/api/words` | 批量新增单词(原始格式,自动规整) | | GET | `/api/words?search=&isMemorized=&page=&pageSize=` | 分页查询(支持搜索/状态筛选) | | GET | `/api/words/{id}` | 单词详情 | | PUT | `/api/words/{id}` | 编辑单词(自动重新规整音标) | | DELETE | `/api/words/{id}` | 删除单个单词 | | DELETE | `/api/words/batch` | 批量删除,body: `{ids:[...]}` | | GET | `/api/words/memorize/list?count=` | 获取待背诵单词列表(错题优先 + 新词补充) | | GET | `/api/words/random` | 随机待背诵单词 | | PUT | `/api/words/{id}/memorize` | 更新背诵状态,body: `{action:"mastered"\|"unfamiliar"}` | | GET | `/api/words/dictation/list?count=` | 获取待默写单词列表 | | GET | `/api/words/dictation/random` | 随机待默写单词 | | POST | `/api/words/dictation/check/{id}` | 默写校验,body: `{userInput}` | ### 7.3 选词测试 QuizController(`/api/quiz`) | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/api/quiz/generate?count=` | 生成测试题(4 选 1,含 `correctIndex`) | | POST | `/api/quiz/submit` | 提交答案,body: `QuizSubmitRequest[]` | `QuizSubmitRequest`:`{ wordId, selectedOptionIndex, correctOptionIndex }` > 统计正确性说明:`correctOptionIndex` 由前端在答题时随题目一并保存,提交时后端直接比对索引,**不再重新随机生成题目**,避免选项顺序变化导致统计错误。 ### 7.4 学习统计 StatsController(`/api/stats`) | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/api/stats` | 返回 `StatsResponse`(总数/完成率/正确率/薄弱 TOP10/近 7 天趋势) | ### 7.5 系统设置 SettingController(`/api/setting`) | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/api/setting` | 获取当前用户所有设置 | | GET | `/api/setting/{key}` | 按 key 获取设置值 | | PUT | `/api/setting` | 更新单个设置,body: `{key, value}` | | PUT | `/api/setting/batch` | 批量更新 | | GET | `/api/setting/dailyMemorizeCount` | 获取每日学习量(默认 10) | | PUT | `/api/setting/dailyMemorizeCount/{count}` | 设置每日学习量 | --- ## 八、前端结构 ### 8.1 路由(router/index.ts) | 路径 | 视图 | 说明 | | --- | --- | --- | | `/` | → `/list` | 默认重定向到单词列表 | | `/login` | Login.vue | 登录/注册(不受守卫拦截) | | `/list` | WordList.vue | 单词列表 | | `/memorize` | Memorize.vue | 背诵模式 | | `/dictation` | Dictation.vue | 默写模式 | | `/quiz` | Quiz.vue | 选词测试 | | `/stats` | Stats.vue | 学习统计 | | `/settings` | SystemSettings.vue | 系统设置(每日学习量 + 新增单词) | ### 8.2 接口封装(api/index.ts) 统一 `request()` 封装: - 自动拼接 `/api` 前缀; - 自动读取 `localStorage.token` 并附加 `Authorization` 头; - `401` 时清除 token 并跳登录页; - 统一解析 `{code,data,message}`,非 200 抛错。 导出的 API 对象:`authApi`、`wordApi`、`quizApi`、`statsApi`、`settingApi`。 ### 8.3 关键状态 - 登录态:`localStorage` 中的 `token`、`username`、`userId`。 - 每日学习量:各学习页 `onMounted` 时调用 `settingApi.getDailyMemorizeCount()` 拉取,统一在 `SystemSettings.vue` 修改并保存。 --- ## 九、核心业务逻辑 ### 9.1 单词录入格式规整 输入支持两种格式: - 三段式:`单词/音标/中文` → 去首 `*` 号、音标加 `[]` - 两段式:`单词/中文`(无音标) 多条输入按行拆分批量处理;重复单词(同用户下 `WordText` 相同)跳过。 ### 9.2 学习推送策略(GetMemorizeWordsAsync) 1. 优先返回历史错误单词(按 `ErrorCount` 倒序); 2. 不足 `count` 时补充未背诵新词(按 `Id` 升序); 3. 仍不足时从已背诵单词(按 `MemorizeCount` 升序)补充。 背诵、默写复用同一策略。 ### 9.3 选词测试统计(SubmitQuizAsync) 逐题比对 `selectedOptionIndex === correctOptionIndex`: - 答对:`MemorizeCount++`,更新 `LastMemorizeTime`; - 答错:`ErrorCount++`,`IsMemorized=false`,收集进错题清单。 返回的 `QuizResult` 包含总题数、答对、答错、正确率、错题列表。 ### 9.4 数据隔离实现 `WordService` 每个公开方法均接收 `userId` 参数,所有 `WHERE` 条件附加 `UserId == userId`;新增单词/设置时写入 `UserId`。Controller 通过 `User.FindFirstValue(ClaimTypes.NameIdentifier)` 取得当前用户 ID。 --- ## 十、构建与部署 ### 10.1 后端发布 ```powershell cd backend\WordMemory.Api dotnet publish -c Release -o ./publish ``` 将 `publish/` 与 `wordmemory.db`(或重新生成)一并部署到服务器,配置反向代理(如 Nginx / IIS)转发到 `http://localhost:5077`。 ### 10.2 前端发布 ```powershell cd frontend npm run build ``` 产物在 `frontend/dist/`,可托管于任意静态服务器(Nginx、GitHub Pages、对象存储等)。需将 `/api` 请求代理到后端地址(生产环境在服务器或前端构建配置中处理,而非开发代理)。 ### 10.3 生产环境检查清单 - [ ] 替换 `appsettings.json` 中 JWT `SecretKey` 为强随机串。 - [ ] 确认 `ConnectionStrings` 指向正确的 SQLite 路径且文件可写。 - [ ] 前端 `/api` 代理指向正确后端域名(处理 CORS 或同源部署)。 - [ ] 若对外暴露 Swagger,建议在生产环境关闭或加鉴权。 --- ## 十一、已知限制 / 后续可优化 1. **密码哈希无盐值**:当前仅 SHA256,建议升级为带盐的慢哈希算法。 2. **JWT 无服务端吊销**:令牌在 168 小时内有效,注销仅清除客户端 token;如需即时失效可引入刷新令牌或黑名单。 3. **设置项单一**:目前仅 `dailyMemorizeCount` 一项,背诵/默写/测试共享同一数值;如需各自独立可扩展 Setting 键值。 4. **旧数据迁移**:新增 `UserId` 字段前已存在的单词 `UserId` 为 0,需在注册首个用户后由业务层处理(当前版本要求先登录再使用,新数据均带正确 `UserId`)。 5. **统计页图表**:`prompt.md` 提及饼图/趋势图,当前 `Stats.vue` 以表格为主,可按需接入图表库(如 ECharts)。 --- ## 十二、常见问题(FAQ) **Q:启动后数据库表没生成?** A:FreeSql `UseAutoSyncStructure(true)` 在首次 DB 操作(如首次请求)时建表。可先访问一次接口或 Swagger 触发;也可确认 `wordmemory.db` 文件所在目录有写权限。 **Q:前端一直跳登录页?** A:检查后端是否正常返回 token;确认 `vite.config.ts` 的 `/api` 代理目标端口(默认 5077)与后端一致。 **Q:不同用户看到相同单词?** A:确认请求头携带了有效 `Authorization`,且单词写入时 `UserId` 正确(登录态下新增的单词自动归属当前用户)。 **Q:选词测试答对却统计为错?** A:已修复——提交时携带前端保存的 `correctOptionIndex` 比对,不再由后端重新随机生成选项。