用Flutter做背单词APP-04一个人,2周,从零撸出一个背单词App

作者:雪隐_上班了 本文欢迎分享、聚合讨论,但不接受整篇搬运。圈子不大,写文不易,急用可联系授权。

写在前面

这篇文章本来是我给自己写的"项目复盘",写着写着发现------好家伙,这字数快赶上毕业论文了。

既然都写这么多了,不如发出来给大伙看看。

如果你正打算用 Flutter + NestJS 做个全栈项目,这篇文章大概能帮你省下几个踩坑的夜晚。如果你只是闲着没事点进来看看......那也行,就当听一个失业人员吹牛。


一、项目背景:为什么我要自己造轮子?

事情的起因很简单:

我想背词组,市面上的App没有。

我问客服,客服说"以后可能会加"。

翻译一下:"以后" ≈ "下辈子"。

再加上正好失业在家,闲着也是闲着,不如自己干一个。

于是就有了佳禾英语(JiaHe English)------一个名字听起来像补习班、但确实能用的单词学习App。

整个项目的技术栈是:

  • 前端:Flutter 3.x(跨平台,一套代码跑iOS和Android)
  • 后端:NestJS 11 + TypeORM + MySQL 8
  • 通信方式:RESTful API,HTTP协议
  • 部署:树莓派(详见前几篇文章)

一句话概括:从单词怎么存,到单词怎么学,再到学了怎么记,全链路自己撸了一遍。

为什么叫"佳禾"? 因为我捏的那个虚拟形象叫"佳禾"。

对,就是上回文章里那个小女孩。

一个背单词App用虚拟美少女当吉祥物,这事儿干得出来吧?


二、后端架构:3600行代码,10个模块,9张表

后端约 3,600 行 TypeScript 代码,63 个源文件。

采用 NestJS 的模块化架构------说白了就是"分门别类放东西",按业务领域拆成了 10 个功能模块 + 1 个数据填充模块。

2.1 数据模型:9张表的关系网

先来个关系图,感受一下这9张表是怎么串起来的:

bash 复制代码
┌──────────────┐     ┌──────────────────┐     ┌──────────────┐
│    users     │────►│ user_settings    │     │    words     │
└──────────────┘     └──────────────────┘     └──────────────┘
       │                                          │
       │    ┌──────────────────────┐              │
       ├────►│ user_word_progress   │◄─────────────┤
       │    └──────────────────────┘              │
       │    ┌──────────────────────┐              │
       ├────►│  daily_records      │              │
       │    └──────────────────────┘              │
       │    ┌──────────────────────┐              │
       ├────►│  search_history     │              │
       │    └──────────────────────┘              │
       │    ┌──────────────────────┐              │
       └────►│  wordbooks          │              │
            └──────────┬───────────┘              │
                       │                          │
                       │  ┌──────────────────┐    │
                       └──│ wordbook_words   │◄───┘
                          └──────────────────┘

┌──────────────────┐
│     admins       │  ← 管理后台独立表
└──────────────────┘

看着复杂,其实核心就两条线:

单词线wordswordbook_wordswordbooks(单词属于哪个单词本)

学习线usersuser_word_progressdaily_records(谁学了啥、学了没)

其他表都是辅助------user_settings存个人偏好,search_history存搜索记录,admins给管理后台用。

2.2 核心表:user_word_progress 的设计

这张表是整个学习引擎的心脏,没有它,SM-2算法就是个空壳

typescript 复制代码
// user_word_progress 核心字段
easiness_factor: decimal(3,2)  // 初始 2.50,SM-2 核心参数
correct_streak: int            // 连续正确次数,达到5次就"掌握"
review_count: int              // 复习总次数(含答错)
next_review_at: datetime       // 下次复习时间,靠它决定今天该学啥
last_interval_days: int       // 上次间隔天数
status: tinyint               // 0=未学 1=学习中 2=已掌握
source: varchar(20)           // 'new' | 'review' | 'simple'

简单说一下这些字段是干嘛的:

  • easiness_factor:SM-2的核心参数,初始2.5。你答对了它就变大(最多2.5),答错了它就变小(最少1.3),直接影响下次复习间隔。
  • correct_streak:连续答对次数。连续答对5次,你就"掌握"了这个词。
  • next_review_at:下次该复习的时间。每天的"复习任务"就是从这里捞数据。

说白了,这张表就是每个人的"学习账本",

你今天学了什么、学得怎么样、明天该复习哪个词,全记在上面。

2.3 SM-2间隔重复算法:我照抄了SuperMemo的作业

学习引擎的核心是 SuperMemo SM-2 算法。这东西1987年就有了,比我还大几岁,但至今仍是背单词App的黄金标准。

算法的核心逻辑在 LearnService 里,简化版是这样的:

ini 复制代码
// 回答正确时的处理
IF correct_streak == 0:
    interval = 1 天
ELIF correct_streak == 1:
    interval = 6 天
ELSE:
    interval = last_interval_days * easiness_factor  // 最多365天

easiness_factor += 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)
// quality 取值范围 0-5,由用户回答决定

// 回答错误时的处理
correct_streak = 0
easiness_factor = max(1.3, easiness_factor - 0.2)
interval = 1 天

翻译成人话:

  • 第一次答对 → 间隔1天再复习
  • 第二次答对 → 间隔6天再复习
  • 第三次及以后 → 间隔 = 上次间隔 × 容易度系数(越容易间隔越长)
  • 答错了 → 明天就给我回来重学!

quality(回答质量)怎么来的?

  • 用户点"认识" → quality = 5(完美)
  • 用户点"不认识" → quality = 1(错误)
  • 拼写模式正确 → quality = 5
  • 拼写模式错误 → quality = 1

连续正确达到 5次 时,状态标记为"已掌握",不再进入每日复习队列------但用户还是可以在单词本里找到它。

这算法听起来复杂,其实核心就一句话:

"你会了我就少烦你,你不会我就多烦你。"

跟老妈催你找对象是一个逻辑。

2.4 每日学习任务:怎么决定你今天学什么?

getDailyWords 方法的工作流程:

  1. user_settings 读取用户设定的每日新词数(默认10个)
  2. 查询今天已经学了多少个(从 daily_records
  3. 计算今天还能学几个新词(额度 - 已学)
  4. user_word_progress 中捞"未学"的单词,补满额度 → NEW
  5. user_word_progress 中捞 next_review_at <= NOW 的单词 → REVIEW
  6. 合并 NEW + REVIEW,同一个单词的不同词性合并成一张卡
  7. 返回完整的卡片数据(含所有词性、音标、例句、音频)

关键细节:同一个单词的不同词性合并为一张卡。

什么意思?比如你学 run

  • 作为名词(n.)→ 跑步
  • 作为动词(v.)→ 跑;运转

后端会把两个词性合并成一个"学习卡片",你一次学完所有词性,而不是分三次学。

这功能看着简单,

但市面上有一半背单词App都做不到,

因为它们的设计压根儿没考虑"一词多性"。

2.5 数据从哪儿来?------上一篇文章的事,这儿就不重复了

数据怎么填进 words 表的,我在上一篇文章里已经详细讲过了------本地大模型批量生成、Free Dictionary API、有道API三种策略轮着上,最终搞定了1.5万条记录。

感兴趣的同学可以翻回去看,这里就不再水字数了。

2.6 补签机制:给自己留条后路

日历模块的补签功能是个完整的"前端操作→后端处理"链路:

markdown 复制代码
前端操作:
  1. 点击灰色日期 → 弹出补签确认
  2. 确认 → POST /calendar/makeup { userId, date }

后端校验:
  - 日期是否在过去 30 天内
  - 日期不能是今天或未来
  - 剩余补签次数 > 0(从 user_settings.makeup_chances 读取)
  
后端处理:
  1. 扣除一次补签机会
  2. 创建或更新 daily_records(record_date)
  3. 返回最新日历数据

补签卡这东西,

说白了就是"我给自己留的后路"。

毕竟我知道自己有多懒,总有几天不想学。

2.7 其他后端设计

  • 认证 :管理后台用 JWT(passport-jwt),7天过期。用户端目前硬编码 userId=1------TODO列表第一位就是"实现用户认证"。
  • 日志:用 Pino 输出结构化 JSON 日志,按天滚动,区分 app.log / error.log。
  • 异常处理 :全局 HttpExceptionFilter 捕获所有异常,统一返回格式。
  • 健康检查GET /health 验证数据库连接和核心表是否存在。

三、前端架构:6200行Dart,19个文件,纯setState

前端约 6,200 行 Dart 代码,19 个文件。

一个重要的技术决策:我没有用 Provider / Riverpod / Bloc 等任何状态管理库。

是的,纯 StatefulWidget + setState

有人可能觉得这是"技术债",

但我的想法是:项目初期功能边界清晰,页面间共享状态极少,

setState 完全够用,而且省去了学新框架的时间。

就像你只是下楼买个菜,没必要开辆卡车去。

后续如果加多用户、实时数据、页面间复杂通信,再迁移到 Riverpod 也不迟。

3.1 页面结构:5个主要页面 + 嵌套子页

perl 复制代码
MainPage(IndexedStack 保活 3 个 Tab)
│
├── [Tab 0] HomePage
│   ├── 顶部渐变 Header + 搜索入口
│   ├── 周打卡状态(7 天网格,每格带🔥图标)
│   ├── 连续天数 / 累计天数
│   ├── "开始背单词"按钮 → push StudyPage
│   └── "最近添加"单词列表(WordCard)
│
├── [Tab 1] CalendarPage
│   ├── 月度日历网格(TableCalendar)
│   ├── 学习统计卡片(总词数 / 学习天数 / 连续天数)
│   └── 补签交互(BottomSheet → 确认 Dialog → API)
│
├── [Tab 2] ProfilePage
│   ├── 用户头像 + 欢迎语
│   ├── "我的单词本" → push WordbookPage
│   └── "设置" → push SettingsPage
│
├── SearchPage → push(AppBar 内嵌搜索框)
│   ├── 搜索历史(GET /search-history)
│   ├── 搜索结果列表
│   └── 自定义添加单词(BottomSheet 表单)
│
├── WordDetailPage → push
│   ├── 多词性卡片展示
│   ├── 音频播放(audioplayers)
│   └── 加入/移出单词本 + 标记忘记
│
├── StudyPage → push(最复杂的页面,1114 行)
│   ├── 三种学习模式切换
│   ├── 本地进度缓存(SharedPreferences)
│   └── 学习结果提交(POST /learn/result)
│
├── WordbookPage → push
│   └── WordbookDetailPage → push(4 Tab 筛选)
│       ├── 今日任务(今日添加 + 复习词)
│       ├── 在学单词
│       ├── 未学单词
│       └── 简单词
│
└── SettingsPage → push
    ├── 每日目标(Dialog 数字输入)
    ├── 通知开关(Switch)
    └── 通知时间(TimePicker)

导航用的是原生的 Navigator.push(MaterialPageRoute(...)),没引入 GoRouter。底部导航用 IndexedStack 保活三个 Tab 的状态。

为什么页面设计这么多?

因为市面上背单词App有的功能,我全都要有。

他们没有的(词组、补签、多词性合并),我也要有。

3.2 学习页:1114行的庞然大物

StudyPage 是整个应用最复杂的页面------1114行Dart代码

它实现了一套前端自维护的简易间隔重复引擎,跟后端的SM-2是"双轨并行"。

三种学习模式:

css 复制代码
StudyMode.showMeaning(显示单词,用户选择"认识/不认识")
  ┌──────────────────────┐
  │      abandon         │
  │     /əˈbændən/       │  ← 显示单词 + 音标 + 发音按钮
  │                      │
  │  [🔊 发音]           │
  │                      │
  │  [认识]  [不认识]    │  ← 两个按钮
  └──────────────────────┘

StudyMode.quiz(点击"认识"后进入拼写模式)
  ┌──────────────────────┐
  │  请输入单词拼写:     │
  │  ┌──────────────┐   │
  │  │ a b a n d _ _│   │  ← 用户输入
  │  └──────────────┘   │
  │  [提交]              │
  └──────────────────────┘

StudyMode.showResult(显示结果)
  ┌──────────────────────┐
  │      abandon         │
  │      ✓ 正确!        │  ← 或 ✗ 错误
  │     例句展示...      │
  │  [继续]              │
  └──────────────────────┘

候选池算法:

  1. 初始化:从当前单词本加载所有单词
  2. 构建候选池(_candidatePool):固定大小约10个,优先选"未掌握且失败次数少"的
  3. 学习逻辑:
    • 回答正确 → streak++,移入下一轮
    • 回答错误 → streak=0,移到池尾(轮转)
    • 掌握条件:streak >= (从未失败 ? 1 : 4)
    • 已掌握单词移出池,加入 _masteredWordIds
  4. 补充机制:当未掌握单词 ≤ 5 个时,从全量列表自动补充新词

进度持久化(SharedPreferences):

dart 复制代码
// 存储的数据结构
{
  'study_progress_2026-07-14': {
    masteredWordIds: [1, 3, 5, ...],
    wordStreaks: { '2': 3, '4': 1, ... },
    everFailedWordIds: [4, 7, ...],
    candidatePool: [8, 9, 2, ...],
    currentIndex: 2
  }
}

关键是 按日期键名存储。如果日期变更(跨天),自动重置进度,保证每天的学习从新开始。

自动保存机制:

dart 复制代码
// WidgetsBindingObserver 监听应用生命周期
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  if (state == AppLifecycleState.paused || 
      state == AppLifecycleState.inactive) {
    _saveProgress(); // 锁屏/切换后台时自动保存
  }
}

为什么前端要自己维护一套进度?

  1. 即时反馈:用户点"认识"不需要等网络请求就能看到进度更新
  2. 离线降级:没网也能学,学完有网了再同步到后端
  3. 双重保险:后端数据万一出问题,前端缓存还能兜底

3.3 单词详情页:一词多性的可视化

一个单词可能有多个词性,后端 /words/detail/:word 返回按词性分组的数组:

dart 复制代码
// WordDetailPage 展示逻辑
ListView.builder(
  itemCount: entries.length,  // 每个词性一个卡片
  itemBuilder: (context, index) {
    final entry = entries[index];
    return Card(
      child: Column(
        children: [
          Text(entry.word),      // record
          Text(entry.phonetic),  // /'rekɔːd/
          Text(entry.pos),       // n.
          Text(entry.definition), // 记录,档案
          Text(entry.example),   // "Keep a record..."
          if (entry.audioUrl != null)
            IconButton(
              icon: Icon(Icons.volume_up),
              onPressed: () => _playAudio(entry.audioUrl),
            ),
        ],
      ),
    );
  },
)

展示效果就是:一个词,多个卡片,每个卡片对应一个词性,每个词性都有单独的发音、释义、例句。

3.4 搜索与自定义添加:打破"只能背内置词库"的限制

搜索页支持两种交互:

搜索已有单词 :输入关键词 → GET /words?word=keyword → 返回匹配结果列表,点击进入详情页。

自定义添加新单词:通过 BottomSheet 展开表单:

dart 复制代码
// 自定义添加表单
showModalBottomSheet(
  context: context,
  builder: (_) => Form(
    child: Column(
      children: [
        // 类型选择:单词 / 短语
        Wrap(
          children: ['word', 'phrase']
            .map((type) => ChoiceChip(
              label: Text(type == 'word' ? '单词' : '短语'),
              selected: _selectedType == type,
            ))
        ),
        // 必填:单词/短语 + 释义
        TextFormField(decoration: InputDecoration(labelText: '单词/短语 *')),
        TextFormField(decoration: InputDecoration(labelText: '释义 *')),
        // 选填:音标、词性、例句、例句翻译
        TextFormField(decoration: InputDecoration(labelText: '音标')),
        TextFormField(decoration: InputDecoration(labelText: '词性')),
        TextFormField(decoration: InputDecoration(labelText: '英文例句')),
        TextFormField(decoration: InputDecoration(labelText: '中文翻译')),
        ElevatedButton(onPressed: _submitCustomWord, child: Text('添加')),
      ],
    ),
  ),
);

这个功能就是当初我吐槽"市面上的App不能自定义词组"的解决方案。

你们没有?那我自己加。就是这么简单粗暴。

3.5 ApiService与统一错误处理

所有 API 调用通过单例 ApiService 管理,每个页面都遵循统一的加载/错误/成功模式:

dart 复制代码
class _HomePageState extends State<HomePage> {
  HomeData? _homeData;
  bool _isLoading = true;
  String? _errorMessage;

  Future<void> _loadData() async {
    setState(() => _isLoading = true);
    try {
      final data = await ApiService().getHomeData();
      setState(() {
        _homeData = data;
        _errorMessage = null;
      });
    } on ApiException catch (e) {
      setState(() => _errorMessage = e.message);
    } catch (e) {
      setState(() => _errorMessage = '网络错误,请稍后重试');
    } finally {
      setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) return Center(child: CircularProgressIndicator());
    if (_errorMessage != null) return ErrorWidget(message: _errorMessage!, onRetry: _loadData);
    return ContentWidget(data: _homeData!);
  }
}

同样的模式出现在 HomePage、CalendarPage、WordbookPage、StudyPage 等所有页面中------保证了全 App 一致的交互体验。


四、一次完整的学习流程:从点击到数据落地的全链路

以"完成一次学习"为例,看看数据是怎么流动的:

yaml 复制代码
用户操作:进入 StudyPage → 学习 10 个单词 → 点击"完成"
                    │
                    ▼
前端内存:更新 _masteredWordIds / _wordStreaks / _everFailedWordIds
                    │
                自动保存到 SharedPreferences(备灾)
                    │
                    ▼
POST /learn/result  ← 批量的学习结果
Body: {
  userId: 1,
  results: [
    { wordId: 101, action: 'remember' },
    { wordId: 102, action: 'forgot' },
    { wordId: 103, action: 'remember' },
    ...
  ]
}
                    │
                    ▼
后端 LearnService.handleLearnResult()
    │
    ├─► 逐条处理:
    │   ├─► 更新 user_word_progress(easiness_factor, correct_streak,
    │   │    next_review_at, review_count, status...)
    │   ├─► remember → SM-2 间隔递增
    │   └─► forgot → SM-2 重置
    │
    ├─► 更新 daily_records(record_date +1)
    │
    └─► 返回 { learnedCount: 10, masteredCount: 3, reviewCount: 7 }
                    │
                    ▼
前端收到响应 → 展示学习结果页
                    │
                    ▼
用户回到首页 → GET /home/data → 刷新周状态和连续天数

整个链路的核心思想

  • 前端先把用户体验搞定(即时反馈、离线缓存)
  • 后端再把数据持久化搞定(SM-2算法、学习记录)
  • 最后两者通过API对齐(批量提交学习结果)

说白了就是:前端"先记着",后端"最终确认",

两者都对上了,数据才是真的。

有点像"你老婆说买了,你看了眼支付宝,确认真的买了"------双重验证。


五、一个诚实的技术坦白:这项目到底有多"丐"?

前面讲得热火朝天,但其实有几个事我一直没好意思说。

既然文章都写到这儿了,不如一起坦白了吧。

5.1 代码大部分是AI写的

先别急着骂我。

是的,这项目的大部分代码是AI生成的

不是我一行一行敲的,而是我让AI写了初稿,然后我一行一行改的。

过程大概是这样的:

  1. 我跟AI说:"帮我写一个NestJS的LearnService,实现SM-2算法"
  2. AI吐出一堆代码
  3. 我看了之后说:"这里不对,interval应该这么算"或者"你这个字段类型用错了"
  4. AI修正
  5. 我再改改细节
  6. 如此反复N个来回

最后出来的代码,可能是 60% AI写的 + 40% 我改的。

有人可能会说:"那这不就是AI写的项目吗?"

我的回答是:你让AI自己试一个看看?

没有人类在中间把关、纠错、调整、整合,AI生成的东西就是一堆会跑的"赛博废品"。

AI是副驾驶,方向盘还得我握着。

5.2 免费Apple开发者账号:一周续命一次

更惨的是发布。

我用的是免费的Apple开发者账号 ,用 flutter run --release 安装到手机上。

只能管7天。

7天之后,App就"过期"了,打不开。你需要重新连上电脑再跑一次 flutter run --release

这感觉就像你养了只电子宠物,

每周得给它"喂一次代码",不然它就饿死了。

所以目前的"发布流程"是:

  1. 每周日晚上
  2. 打开电脑,连上iPhone
  3. 跑一遍 flutter run --release
  4. 然后祈祷接下来7天不要出bug

每年99美元的Apple开发者账号?

等我找到工作再说吧。

现在?

99美元够我吃一个月挂壁面(加蛋版)。

5.3 后台地址:为什么是 192.168..

还有一个更"丐"的:

我的App请求的后台地址是 http://192.168.1.12:3000/api/...

对,就是内网IP。

为什么不用域名?

因为用域名就得买证书、搞备案、弄公网IP......每一样都要钱。而且我只是做个App给自己用,内网够用了。

目前的状态是:

  • 树莓派跑后端,连在家里路由器上
  • 出门在外时,通过某种方式连回家里网络
  • App的BaseURL写死成 http://192.168.1.12:3000/api

好处是:不要钱,不用证书,随时能跑。

坏处是:出门在外得先连回家里网络,不然App就"网络错误"。

所以这App目前的使用场景是:

在家,连Wi-Fi,背单词。

在外头?你只能看着"网络错误"发呆。


六、技术决策与取舍:为什么这么干?

6.1 为什么前端自己也实现了一套学习算法?

后端有完整的 SM-2,前端也有简易版进度追踪。

这是有意为之的双轨设计

  • 前端本地进度用于即时反馈(用户不需要等网络请求就知道自己掌握了哪些单词)
  • 后端进度是权威数据源,用于跨设备同步和数据分析
  • 前端 SharedPreferences 缓存也是离线降级方案------没网也能学

打个比方:

前端是"草稿本",涂涂画画随时看;

后端是"正式账本",一笔一画不能错。

草稿本可以丢,正式账本不能丢。

6.2 为什么不用状态管理库?

初期评估认为:

  • 页面间共享状态极少(主要是 userId = 1 硬编码)
  • 每个页面独立管理自己的数据
  • 团队(一个人)熟悉 setState
  • 减少依赖引入,降低项目复杂度

后续如果加入多用户、实时数据、页面间复杂通信,迁移到 Riverpod 或 Bloc 是合理的演进方向。

简单说就是:

"够用就行"------这是单人项目的生存哲学。

6.3 为什么用户API没有认证?

这是一个 BFF(Backend For Frontend)风格 的简化设计。后端假设调用方(Flutter App)已经通过某种方式获得了用户身份,目前是写死 userId=1

管理后台使用独立的 JWT 认证,互不干扰。

在项目的 TODO 列表里,用户认证排在靠前的位置:

typescript 复制代码
// LearnService 中的 TODO
// TODO: 从 JWT token 获取真实 userId
const userId = body.userId || 1;

我知道这事儿早晚得做,

但先把核心功能跑通,再考虑安全------

毕竟现阶段,我的App连用户都没有,

黑客也不会闲到黑一个日活为1(我自己)的系统。


七、项目统计与演进路线

项目规模

指标 前端(Flutter) 后端(NestJS)
源文件数 19 63
代码行数 ~6,200 ~3,600
API 端点数 --- 37
数据库表 --- 9
依赖包 6 ~20+
单词数据 --- 5,614 个单词,1.5万+ 条记录
代码中AI生成比例 ~60% ~60%
开发者账号费用 $0(免费账号) ---
服务器费用 ¥0.6/月(电费) ---
域名/证书费用 ¥0 ---

从数据量来看,这已经不是一个"玩具项目"了,

但从成本来看,它还是个"丐版项目"。

全栈开发,月成本6毛钱,

这事儿说出去估计能把云服务厂商气死。

不足之处与下一步

  1. 用户认证 --- 从硬编码 userId 迁移到 JWT + 手机号/微信登录
  2. 状态管理 --- setState → Riverpod 或 Bloc,解决页面间状态共享
  3. 路由 --- MaterialPageRoute → GoRouter,支持深层链接和页面过渡动画
  4. 离线能力 --- SharedPreferences → SQLite(drift/sqflite),支持完整离线学习
  5. 通知推送 --- 本地通知 → firebase_messaging 远程推送
  6. 测试覆盖 --- 当前只有1个测试文件,远不够
  7. CI/CD --- 自动化测试 + 构建 + 部署流水线
  8. Apple开发者账号 --- 免费 → 付费(等我找到工作)
  9. HTTPS/域名 --- 内网HTTP → 公网HTTPS(等我找到工作)
  10. 公网部署 --- 内网 → 真正的公网服务(等我找到工作)

注意上面最后三条:

**"等我找到工作"**出现了三次。

这大概就是独立开发者最真实的写照:

不是技术做不到,是钱包不允许。

可能的扩展方向

  • 学习模式扩展:选择题、听力理解、拼写纠错、句子填空
  • 数据分析:艾宾浩斯遗忘曲线展示、学习效率分析、薄弱环节识别
  • 社交功能:学习小组、排行榜、打卡分享
  • 内容生态:内置词库扩展(四六级/考研/雅思/托福词库)、用户上传词库

上面这些"下一步"和"扩展方向",

翻译一下就是:

"我脑子里还有很多想法,但手只有一双,而且还得找工作。"


八、总结

佳禾英语是一个从零搭建的全栈项目,涵盖了 数据库设计 → 后端API开发 → Flutter跨平台客户端 的完整链路。

项目最核心的价值在于:

  1. 实现了一套可用的 SM-2间隔重复算法
  2. 围绕它构建了单词管理、学习引擎、日历打卡、补签机制、搜索词库等完整功能
  3. 前后端双轨进度管理,实现了即时反馈 + 数据持久化

技术选型上偏向务实(或者说"穷"):

  • NestJS 提供清晰的模块化架构
  • TypeORM 简化数据库操作
  • Flutter 实现跨平台UI
  • setState + SharedPreferences 够用就先用着
  • 树莓派当服务器,月成本6毛
  • 免费Apple账号,每周续命一次
  • HTTP + 内网IP,证书?不存在的

虽然还有很多简化之处和待改进项,但作为一个个人全栈项目,它已经跑通了 从学到练到数据追踪 的完整闭环。

而且,它真的只花了6毛钱一个月。

有人说:"你这么做太凑合了吧?"

我想说:在找到工作之前,凑合就是我的生产力。

等我有钱了,证书、域名、付费账号、云服务器,全套安排上。

但现在?

6毛钱的服务器 + 免费账号 + AI写的代码 + 每周手动续命一次,

能跑就是胜利。


最后的最后

这篇文章写了两天,全文超过1.6万字。

如果你看到了这里------

你是真爱,我谢谢你。

如果这篇文章帮到了你,或者让你觉得"这事儿我也能干",那我的目的就达到了。

欢迎评论区交流,

也欢迎点个让我知道你来过。

相关推荐
Ivanqhz2 小时前
DRN(深度强化学习推荐网络)
人工智能·线性代数·机器学习·矩阵·dnn
xd1855785552 小时前
梦境解析师-基于鸿蒙的梦境心理分析应用开发实践
人工智能·华为·harmonyos·鸿蒙
147API2 小时前
ChatGPT Skills开放后,开发团队先补权限和上传检查
人工智能·chatgpt
OpenTiny社区2 小时前
Skill 设计:封装、评测、运营,一次讲透
前端·github
大白要努力!2 小时前
uni-app 全面入门速查手册
前端·uni-app
索西引擎2 小时前
【React】虚拟 DOM:原理、机制与核心价值分析
前端·react.js·前端框架
ltqvibe2 小时前
怎么让本体语义变成可执行的智能体
java·人工智能·本体语义
console.log('npc')2 小时前
网页移动端UI/UX Pro Max - 设计智能skill
前端·人工智能·ui·ux
qiaozhangmenai2 小时前
2026年AI超级公司系统行业趋势与技术演进分析|AI营销闭环|乔掌门AI
大数据·人工智能