From 794e27e0982402e3186bc5dcbd2d6c91cffba3f1 Mon Sep 17 00:00:00 2001
From: CyC2018
Date: Sat, 31 Oct 2026 14:31:33 +0800
Subject: [PATCH 01/10] Add learn/
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
新增学习站:按学习路线自评定级、生成每日计划、间隔复习,记录保存在浏览器中,可导出导入。
---
learn/.gitignore | 2 +
learn/README.md | 93 +
learn/build.py | 335 +++
learn/content.lock.json | 3647 ++++++++++++++++++++++++++++++
learn/content.py | 211 ++
learn/maintain.py | 151 ++
learn/make_images.py | 69 +
learn/routes.json | 9 +
learn/serve.py | 18 +
learn/src/app.css | 686 ++++++
learn/src/app.js | 1499 ++++++++++++
learn/src/core.js | 583 +++++
learn/src/icons/favicon-32.png | Bin 0 -> 1113 bytes
learn/src/icons/icon-180.png | Bin 0 -> 6006 bytes
learn/src/icons/icon-192.png | Bin 0 -> 6508 bytes
learn/src/icons/icon-512.png | Bin 0 -> 17977 bytes
learn/src/icons/maskable-512.png | Bin 0 -> 8182 bytes
learn/src/index.html | 53 +
learn/src/manifest.webmanifest | 16 +
learn/src/og.png | Bin 0 -> 73063 bytes
learn/src/qr.js | 177 ++
learn/src/sw.js | 22 +
learn/tests/core.test.cjs | 480 ++++
learn/tests/e2e.py | 414 ++++
learn/tests/maintenance.py | 339 +++
learn/tests/personas.py | 300 +++
learn/tests/replay_history.py | 78 +
learn/tests/test_content.py | 205 ++
28 files changed, 9387 insertions(+)
create mode 100644 learn/.gitignore
create mode 100644 learn/README.md
create mode 100644 learn/build.py
create mode 100644 learn/content.lock.json
create mode 100644 learn/content.py
create mode 100644 learn/maintain.py
create mode 100644 learn/make_images.py
create mode 100644 learn/routes.json
create mode 100644 learn/serve.py
create mode 100644 learn/src/app.css
create mode 100644 learn/src/app.js
create mode 100644 learn/src/core.js
create mode 100644 learn/src/icons/favicon-32.png
create mode 100644 learn/src/icons/icon-180.png
create mode 100644 learn/src/icons/icon-192.png
create mode 100644 learn/src/icons/icon-512.png
create mode 100644 learn/src/icons/maskable-512.png
create mode 100644 learn/src/index.html
create mode 100644 learn/src/manifest.webmanifest
create mode 100644 learn/src/og.png
create mode 100644 learn/src/qr.js
create mode 100644 learn/src/sw.js
create mode 100644 learn/tests/core.test.cjs
create mode 100644 learn/tests/e2e.py
create mode 100644 learn/tests/maintenance.py
create mode 100644 learn/tests/personas.py
create mode 100644 learn/tests/replay_history.py
create mode 100644 learn/tests/test_content.py
diff --git a/learn/.gitignore b/learn/.gitignore
new file mode 100644
index 000000000..2ead01d05
--- /dev/null
+++ b/learn/.gitignore
@@ -0,0 +1,2 @@
+dist/
+__pycache__/
diff --git a/learn/README.md b/learn/README.md
new file mode 100644
index 000000000..b166a002e
--- /dev/null
+++ b/learn/README.md
@@ -0,0 +1,93 @@
+# CS-Notes 学习站
+
+从三条学习路线(Java 后端、大模型应用开发、面试冲刺)生成的静态站点,与笔记网站一起部署在 `/learn/`。读者先用路线中的自测题自评定级,按水平、每天的时间和面试日期排出计划,自评过的题按 1、3、7、15、30、60 天的间隔复习。不需要账号,不连接后端或 AI 接口,记录只保存在浏览器的 localStorage 中,可以导出和导入。
+
+## 功能
+
+- 今天:只给一个“下一步”(继续定级、先复习一批、学今天的内容),下面是连续学习天数、最近 7 天和预计学完的日期;中断几天后回来时提示,并把到期的题分批复习;
+- 复习份额:每天的复习按设定的时间分批,做完份额先学当天的内容;积压超过两批时份额加倍;时间紧时可以“只有 5 分钟”,复习 5 道保住连续天数;
+- 连续学习:每学满 7 天得到 1 次保护(最多 2 次),中断一天自动使用,不清零;连续天数到 3、7、14、30 等数字时在完成页提示;
+- 定级:每个阶段两三道题,看参考要点后自评,“模糊”算半个“会”;
+- 计划:按路线顺序装进每天的时间,先留出复习的时间;今天完成的天仍算在今天;可以导出 .ics 加到日历;
+- 每天:导读、原文链接、自测题卡、动手和算法;完成后显示进度和下一步,学完一个阶段单独提示;
+- 复习与模拟面试:键盘 空格、1、2、3 操作;模拟面试按热度和不牢的程度抽 8 道题,结束时给出得分和需要再看的题;
+- 题库:按路线、热度、掌握情况和关键词找题,每道题有单独的链接(`#/q/`),可以分享;
+- 进度:16 周热力图、各阶段掌握、常忘的题,以及在本地用 canvas 生成的 1080×1440 分享图(带指向本站的二维码,不含学习记录);
+- 回答对照:写了回答再看要点时,列出要点中的术语哪些提到了、哪些没提到(只作提示,不打分);
+- 反馈:题卡上的“要点有误”和页脚的“反馈建议”打开 GitHub 新建议题页,预填题号、题目和要点,读者自己提交;维护者按题号在路线文件中修改;
+- 题卡上的“复制给 AI 追问”把题目、回答、参考要点和原文链接整理成提示词;
+- 学习记录:导出成文件或复制成文字,在另一台设备上选择文件、拖入或粘贴导入;导入前显示两边的概况,可以合并(同一道题取最近一次自评)或替换;进度页在记录较多又久未导出时轻提示备份;
+- 数据与隐私(页脚链接,`#/privacy`):不收集个人信息、不用 Cookie 和第三方脚本、记录只在本机、访问日志由托管服务商按其规则记录、怎样转移和删除;内容有变化时更新页面上的日期;
+- 外观:清爽(默认)、纸面、活泼三套配色,在学习记录或设置页切换,只存在本机;
+- 手机上导航在底部;可以安装到桌面,载入一次后离线可用;深色模式跟随系统。
+
+## 构建与预览
+
+```bash
+python3 learn/build.py --notes-base 'http://127.0.0.1:8769/#/notes/' # 单独构建到 learn/dist,链接指向本机的笔记网站
+python3 learn/serve.py # http://127.0.0.1:8770/
+```
+
+笔记网站的 `website/builder/build.py` 会调用 `learn/build.py`,把学习站生成到 `website/dist/learn/`,题目链接改为站内的 `/#/notes/…`,并在首页、侧边栏和学习路线的文章中加入口;随网站一起发布,不需要单独部署。单独构建时严格检查所有指向笔记的链接,锚点不存在时报错退出;和网站一起构建时用 --lenient,只给警告(见下文“维护”)。地址后加 `?today=2026-11-02` 可以模拟日期。
+
+图标和链接预览图由 `python3 learn/make_images.py` 生成到 `src/icons/` 和 `src/og.png`(需要 Pillow,先构建一次以读取题数)。
+
+## 维护
+
+学习站的内容全部来自三条学习路线和它们链接到的笔记,笔记更新后随网站一起重新构建,不需要单独发布。读者的记录按题目和天的编号保存,编号由 content.py 根据提交在仓库中的 content.lock.json 分配,保证笔记更新后不变:
+
+- 改了题目的问法或参考要点:按文字和要点的相似度找回原编号,复习记录不受影响;
+- 新增题、新增天:拿新编号;新天的编号接着这条路线用过的最大序号,插在中间也不会让后面的天换编号;
+- 学过的天改了导读或阅读范围(lock 中记录每天的指纹):完成状态不变,首页列出这些天,读者可以再看一眼;
+- 删掉的题和天:标记为下线,读者的记录移到归档中保留,不再安排复习;重新加回来时恢复原编号和记录;
+- 路线下线(从 routes.json 删掉):在学这条路线的读者看到换路线的提示,路线中相同的题接着按原来的间隔复习;新增路线在 routes.json 中加一项。
+
+改了学习路线或笔记标题后,在本机运行:
+
+```bash
+python3 learn/maintain.py # 列出新增、修改、下线的题和天,以及链接到的小节不存在等问题
+python3 learn/maintain.py --write # 确认无误后写入 lock,这次变化记入更新记录,读者在首页看到和自己有关的部分
+```
+
+自动判断错了时(两道不同的题被当成同一道,或改写后没认出来),用 `--separate 新题临时编号` 或 `--map 新题临时编号=旧编号` 指定,临时编号在 maintain.py 的输出中。没有运行 maintain.py 就提交也能正常发布:CI 中宽松构建,读者的记录照样保留,只是更新记录没有日期;构建日志和 dist-report.json 的 learn 一项会提示 lock 不一致,链接到的小节改名时改为链接到文章开头并给出警告,不挡住笔记的发布。
+
+首页的“内容更新”只列和读者自己有关的部分:学过的天新增的题(可以一键练习)、当前路线新增的天、练过的题改了什么(带链接)、学过的天改了导读、练过的题下线。页面只带最近 20 条更新,落下更多的读者只看到一句概括。还没发布过时不需要更新记录:第一次发布前运行 `python3 learn/maintain.py --baseline`,写入现在的内容并清空更新记录。
+
+页面一侧:本地记录有版本号(现在是 2),旧版本的记录读入时自动转换;另一个标签页已经用更新版本保存过时,旧页面只读并提示刷新。部署新版本后,打开着的页面在切回前台时检查更新,提示刷新;离线缓存按构建号整体替换。
+
+## 上线步骤
+
+1. 第一次发布前运行 `python3 learn/maintain.py --baseline`,按现在的内容写入 lock 并清空更新记录;之后每次改路线用 `maintain.py` 和 `--write`;
+2. 提交 learn/(含 content.lock.json、routes.json、src/icons、src/og.png)、website/ 中学习站相关的改动和 .github/workflows/website.yml;
+3. CI 构建网站时生成 /learn/,跑核心逻辑、编号和网站的测试,发布后校验 /learn/ 的入口、数据和离线缓存脚本;
+4. 上线后在手机和电脑上各打开一次 https://cyc2018.xyz/learn/:能定级、能学一天、刷新后记录还在、断网后能打开。
+
+读者反馈的题目问题,用 `python3 learn/maintain.py --find 题号` 找到所在的天和路线文件。
+
+## 上线与安全
+
+- 入口页带内容安全策略(CSP):只允许本站的脚本、样式、图片和请求,head 中的内联脚本由 build.py 计算哈希写入;以后加内联脚本无需改动,但不能加 onclick 这类内联事件。e2e 测试把 CSP 拦截记为错误;
+- 页面渲染出错时显示“页面出错了”,提供刷新和直接导出记录,不会白屏;题目数据载入失败时可以重试;
+- 发布后的线上校验(website/deploy/deploy.mjs)同时核对 learn/index.html、data.json、app.js、sw.js,以及目录地址 /learn/ 返回的是学习站入口页;
+- 旧浏览器(不支持 color-mix 等)有颜色回退,顶部和底部导航不会透明。
+
+## 测试
+
+```bash
+node learn/tests/core.test.cjs # 调度、定级、计划、日历、导入校验;dist 存在时附带真实数据的检查
+python3 learn/tests/e2e.py --notes http://127.0.0.1:8769/ # 无头 Edge 走完整流程,需要 websocket-client
+python3 learn/tests/personas.py # 访客、连续 20 天学习、中断后回来、模拟面试、离线等场景,约 10 分钟
+python3 learn/tests/test_content.py # 编号对应:改问法、改要点、插入一天、删了再加回、路线增删、手工指定
+python3 learn/tests/maintenance.py # 读者学了 3 天后笔记陆续更新、部署:记录保留、更新提示、刷新提示、路线下线,约 3 分钟
+python3 learn/tests/replay_history.py # 按 Git 历史逐个提交重放路线的修改,检查编号是否稳定
+```
+
+e2e.py 和 personas.py 默认测试 8770 上的单独构建,加 `--base http://127.0.0.1:8769/learn/` 测试笔记网站中的版本。
+
+## 文件
+
+- build.py:解析 routes.json 中的路线,生成 dist/data.json,复制 src,给静态文件加版本号,写入离线缓存的文件列表;
+- content.py、content.lock.json:题目和天的稳定编号与更新记录;maintain.py:查看变化并写入 lock;routes.json:放进学习站的路线;
+- src/core.js:不依赖页面的逻辑(日期、调度、定级、计划、模拟面试抽题、日历、提示词、导入校验),浏览器和 Node 共用;
+- src/qr.js:二维码生成(字节模式、纠错等级 M、版本 1~10);
+- src/app.js、app.css、index.html:页面;sw.js、manifest.webmanifest:离线缓存和安装。
diff --git a/learn/build.py b/learn/build.py
new file mode 100644
index 000000000..72af9e6c2
--- /dev/null
+++ b/learn/build.py
@@ -0,0 +1,335 @@
+#!/usr/bin/env python3
+"""从学习路线生成学习站的数据,并把 src/ 复制到 dist/。
+
+学习路线(notes/学习路线 - *.md)每天有导读、阅读范围、自测题和折叠的参考要点。这里把它们拆成:
+- 路线:阶段(二级标题)和每天的安排;
+- 题目:自测题的原问法、参考要点、原文链接和热度(链接到的小节带 🔥 的数量)。
+不同路线中相同的问题共用一张题卡。题目和天的编号由 content.py 按 content.lock.json 分配,笔记更新后保持不变,
+读者的复习记录和完成情况不会因为改了措辞、插入一天或调整顺序而丢失。
+
+用法:python3 learn/build.py [--notes-base URL] [--output DIR] [--lenient]
+--lenient 用于和笔记网站一起构建:链接到的小节不存在时改为链接到文章开头、某一天的题目和要点对不上时跳过这一天的题,
+只给出警告,不让学习站的问题挡住笔记的发布。单独构建和 maintain.py 默认严格检查。
+"""
+from __future__ import annotations
+
+import argparse
+import base64
+import hashlib
+import html
+import json
+import re
+import shutil
+import sys
+import urllib.parse
+from collections import Counter
+from pathlib import Path
+
+import content
+
+HERE = Path(__file__).resolve().parent
+REPO = HERE.parent
+sys.path.insert(0, str(REPO / 'website' / 'builder'))
+# 笔记网站的构建脚本也叫 build.py,按路径加载,避免和本文件同名冲突
+import importlib.util # noqa: E402
+_spec = importlib.util.spec_from_file_location('site_build', REPO / 'website' / 'builder' / 'build.py')
+_site = importlib.util.module_from_spec(_spec)
+_spec.loader.exec_module(_site)
+# 代码块的正则在新旧版本的网站脚本中位置不同,统一从 build.py 取
+github_slug, strip_hot, VERBATIM = _site.github_slug, _site.strip_hot, _site.VERBATIM
+from site_assets import heading_slug # noqa: E402
+
+CONFIG = HERE / 'routes.json'
+LOCK = HERE / 'content.lock.json'
+# 页面中显示的更新记录条数
+HISTORY_SHOWN = 20
+# 路线中这些二级标题是说明,不是阶段
+INTRO = re.compile(r'^[一二三四]、(说明|开始之前|每天的安排)$')
+DAY = re.compile(r'^### 第 (\d+) 天:(.+)$')
+LINK = re.compile(r'\[([^\]]+)\]\(([^)\s]+)\)')
+HOT = re.compile(r' (🔥{1,2})[ \t]*$')
+
+
+def headings(path: Path):
+ """一篇笔记中每个 GitHub 锚点对应的 Docsify id 和热度(0、1、2)。"""
+ result, github, docsify, fence = {}, Counter(), Counter(), None
+ for line in re.sub(r'', '', path.read_text()).splitlines():
+ marker = re.match(r'^\s*(`{3,}|~{3,})', line)
+ if marker:
+ fence = marker[1][0] if fence is None else (None if marker[1][0] == fence else fence)
+ continue
+ if fence is not None:
+ continue
+ match = re.match(r'^(#{1,6})\s+(.+?)(?:\s+#+)?\s*$', line)
+ if not match:
+ continue
+ heat = HOT.search(match[2])
+ text = strip_hot(match[0]).split(None, 1)[1].strip()
+ slug = github_slug(text)
+ count = github[slug]
+ github[slug] += 1
+ key = slug + (f'-{count}' if count else '')
+ result.setdefault(key, (heading_slug(text, docsify), len(heat[1]) if heat else 0, text))
+ return result
+
+
+class Notes:
+ """把路线中的相对链接换成笔记网站上的地址,并记录无法解析的链接。"""
+
+ def __init__(self, root: Path, base: str, lenient: bool = False):
+ self.root, self.base, self.cache, self.broken, self.warnings, self.lenient = root, base, {}, [], [], lenient
+
+ def resolve(self, href: str, where: str):
+ if re.match(r'^https?://', href):
+ return {'href': href, 'external': True}
+ target, _, fragment = href.partition('#')
+ name = urllib.parse.unquote(target)
+ if not name.endswith('.md'):
+ self.broken.append((where, href, '不是笔记链接'))
+ return None
+ path = self.root / 'notes' / name
+ if not path.is_file():
+ (self.warnings if self.lenient else self.broken).append((where, href, '笔记不存在' + (',已去掉这个链接' if self.lenient else '')))
+ return None
+ if name not in self.cache:
+ self.cache[name] = headings(path)
+ note = name[:-3]
+ url = self.base + urllib.parse.quote(note)
+ heat = 0
+ if fragment:
+ anchor = self.cache[name].get(urllib.parse.unquote(fragment))
+ if not anchor and self.lenient:
+ # 小节改了名:先链接到文章开头,读者仍能找到内容
+ self.warnings.append((where, href, '小节不存在,已改为链接到文章开头'))
+ return {'href': url, 'note': note, 'heat': 0}
+ if not anchor:
+ self.broken.append((where, href, '小节不存在'))
+ return None
+ url += '?id=' + urllib.parse.quote(anchor[0])
+ heat = anchor[1]
+ return {'href': url, 'note': note, 'heat': heat}
+
+
+def links(text: str, notes: Notes, where: str):
+ found = []
+ for label, href in LINK.findall(text):
+ target = notes.resolve(href, where)
+ if target:
+ found.append({'text': label, **target})
+ return found
+
+
+CJK_LATIN = re.compile(r'(?<=[\u4e00-\u9fff])(?=[A-Za-z0-9])|(?<=[A-Za-z0-9])(?=[\u4e00-\u9fff])')
+
+
+def plain(text: str):
+ """去掉 Markdown 链接语法,只留文字;链接文字与相邻的汉字之间补上空格(题目编号去掉空白后计算,不受影响)。"""
+ return CJK_LATIN.sub(' ', re.sub(r'\s+', ' ', LINK.sub(lambda m: m[1], text)).strip())
+
+
+def question_id(text: str):
+ """临时题号:文字的哈希,content.resolve 再换成稳定编号。"""
+ return content.text_id(plain(text))
+
+
+def parse_route(key, file, title, summary, notes: Notes, questions: dict):
+ text = VERBATIM.sub('', (notes.root / 'notes' / (file + '.md')).read_text())
+ stages, days, stage, day, lines = [], {}, None, None, []
+
+ def finish():
+ if day is None:
+ return
+ days[day['id']] = parse_day(day, lines, key, notes, questions)
+
+ for line in text.splitlines():
+ if line.startswith('## '):
+ finish()
+ day, lines = None, []
+ name = line[3:].strip()
+ stage = None if INTRO.match(name) else {'title': re.sub(r'^[一二三四五六七八九十]+、', '', name), 'days': []}
+ if stage:
+ stages.append(stage)
+ continue
+ match = DAY.match(line)
+ if match and stage is not None:
+ finish()
+ # 临时编号,content.resolve 再换成稳定编号;n 是显示用的第几天,随路线变化
+ day = {'id': f'{key}~{int(match[1])}', 'n': int(match[1]), 'title': match[2].strip(), 'stage': len(stages) - 1}
+ stage['days'].append(day['id'])
+ lines = []
+ continue
+ if day is not None:
+ lines.append(line)
+ finish()
+ return {'id': key, 'title': title, 'summary': summary, 'source': file, 'stages': stages}, days
+
+
+def parse_day(day, lines, route, notes: Notes, questions: dict):
+ where = f'{route} 第 {day["n"]} 天'
+ body = '\n'.join(lines)
+ guide, reading, practice, expect, algos, asks = [], [], '', '', [], []
+ block = re.search(r'\s*参考要点 ([\s\S]*?) ', body)
+ points = [re.sub(r'\s+', ' ', p).strip() for p in re.findall(r'^\d+\.\s+(.+)$', block[1], re.M)] if block else []
+ section, folded = None, False
+ for raw in lines:
+ line = raw.strip()
+ # 折叠块中是参考要点,同样带编号,不能当作自测题
+ if line.startswith('\n',
+ f' \n'
+ f' \n'
+ ' \n \n')
+ # 内容安全策略:只允许本站的脚本(加上 head 中那段应用配色的内联脚本的哈希)、样式、图片和请求,
+ # 页面即使被注入内容也无法加载第三方脚本或把数据发到别处;也和“数据与隐私”中不加载第三方脚本的说明一致
+ inline = re.findall(r'', entry, re.S)
+ hashes = ' '.join("'sha256-" + base64.b64encode(hashlib.sha256(code.encode()).digest()).decode() + "'" for code in inline)
+ csp = ("default-src 'self'; script-src 'self' " + hashes + "; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; "
+ "connect-src 'self'; font-src 'self'; manifest-src 'self'; worker-src 'self'; object-src 'none'; base-uri 'self'; form-action 'none'")
+ entry = entry.replace(' ', f' \n ', 1)
+ (output / 'index.html').write_text(entry)
+ # 离线缓存的文件列表和构建号
+ files = ['./'] + [f'{n}?v={hashlib.sha256((output / n).read_bytes()).hexdigest()[:10]}' for n in ('app.css', 'core.js', 'qr.js', 'app.js', 'data.json')]
+ files += sorted(str(p.relative_to(output)) for p in (output / 'icons').glob('*.png')) + ['manifest.webmanifest']
+ build_id = hashlib.sha256(entry.encode() + json.dumps(files).encode()).hexdigest()[:10]
+ sw = (output / 'sw.js').read_text().replace('__BUILD__', build_id).replace('__FILES__', json.dumps(files))
+ (output / 'sw.js').write_text(sw)
+ return {'routes': {r['id']: sum(len(s['days']) for s in r['stages']) for r in routes},
+ 'questions': len(questions), 'sharedQuestions': sum(1 for q in questions.values() if len(q['days']) > 1),
+ 'hot': Counter(q['heat'] for q in questions.values()),
+ 'pending': {group: {k: len(v) for k, v in items.items()} for group, items in changes.items()} if content.has_changes(changes) else None,
+ 'warnings': [f'{w}:{h}({why})' for w, h, why in notes.warnings]}
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description=__doc__.split('\n')[0])
+ parser.add_argument('--notes-base', default='https://cyc2018.xyz/#/notes/', help='笔记网站中文章地址的前缀')
+ parser.add_argument('--source', type=Path, default=REPO)
+ parser.add_argument('--output', type=Path, default=HERE / 'dist')
+ parser.add_argument('--site-url', default='', help='学习站部署后的地址,用于链接预览图(og:image)')
+ parser.add_argument('--lenient', action='store_true', help='链接和题目的问题只给警告,用于和笔记网站一起构建')
+ parser.add_argument('--lock', type=Path, default=LOCK, help='编号对照文件,测试时可以指定别的文件')
+ parser.add_argument('--routes', type=Path, default=CONFIG, help='路线配置,测试时可以指定别的文件')
+ args = parser.parse_args()
+ print(json.dumps(build(args.source.resolve(), args.output.resolve(), args.notes_base, args.site_url, args.lenient, args.lock.resolve(),
+ args.routes.resolve()), ensure_ascii=False))
diff --git a/learn/content.lock.json b/learn/content.lock.json
new file mode 100644
index 000000000..617c46bd4
--- /dev/null
+++ b/learn/content.lock.json
@@ -0,0 +1,3647 @@
+{
+ "days": {
+ "java-01": {
+ "questions": [
+ "380a188f36",
+ "8c543dbbcb",
+ "7649a66405"
+ ],
+ "route": "java",
+ "sig": "fbb7a6f2f4",
+ "stage": "Java 基础",
+ "status": "active",
+ "title": "数据类型与 String"
+ },
+ "java-02": {
+ "questions": [
+ "d85d93a06e",
+ "2af89296be"
+ ],
+ "route": "java",
+ "sig": "999e7b369a",
+ "stage": "Java 基础",
+ "status": "active",
+ "title": "常量池与运算"
+ },
+ "java-03": {
+ "questions": [
+ "ed40759232",
+ "1535c683ad",
+ "a47f889523"
+ ],
+ "route": "java",
+ "sig": "d42f2e52e7",
+ "stage": "Java 基础",
+ "status": "active",
+ "title": "关键字与 Object 的方法"
+ },
+ "java-04": {
+ "questions": [
+ "518704a7fc",
+ "cf1562e438",
+ "731153a537"
+ ],
+ "route": "java",
+ "sig": "9fb87602fb",
+ "stage": "Java 基础",
+ "status": "active",
+ "title": "继承与多态"
+ },
+ "java-05": {
+ "questions": [
+ "a7e52d0151",
+ "15b9ac4352",
+ "61d62633f1"
+ ],
+ "route": "java",
+ "sig": "2bc7a709af",
+ "stage": "Java 基础",
+ "status": "active",
+ "title": "反射、异常、泛型与新特性"
+ },
+ "java-06": {
+ "questions": [
+ "875d608421",
+ "a85a26685e",
+ "3cde3d237b"
+ ],
+ "route": "java",
+ "sig": "b6c4723d21",
+ "stage": "Java 容器",
+ "status": "active",
+ "title": "集合与 List"
+ },
+ "java-07": {
+ "questions": [
+ "54392ab875",
+ "393777a7d3",
+ "2de7600ab9"
+ ],
+ "route": "java",
+ "sig": "127e5d6fef",
+ "stage": "Java 容器",
+ "status": "active",
+ "title": "HashMap"
+ },
+ "java-08": {
+ "questions": [
+ "0679b458cf",
+ "8e70e9576a",
+ "7e5379ca96"
+ ],
+ "route": "java",
+ "sig": "a038b6f320",
+ "stage": "Java 容器",
+ "status": "active",
+ "title": "ConcurrentHashMap 与 LinkedHashMap"
+ },
+ "java-09": {
+ "questions": [
+ "bcb8868d08",
+ "ef0a1e3f79",
+ "154ba456f2"
+ ],
+ "route": "java",
+ "sig": "6abe05001b",
+ "stage": "Java 并发",
+ "status": "active",
+ "title": "线程与线程池"
+ },
+ "java-10": {
+ "questions": [
+ "1c0bbdffef",
+ "79b04fb5b0",
+ "f3cbce1b55"
+ ],
+ "route": "java",
+ "sig": "f29a28c96d",
+ "stage": "Java 并发",
+ "status": "active",
+ "title": "中断与互斥同步"
+ },
+ "java-11": {
+ "questions": [
+ "bcd73345b9",
+ "b50334ebd5",
+ "fb5644c7bf"
+ ],
+ "route": "java",
+ "sig": "69d86009ce",
+ "stage": "Java 并发",
+ "status": "active",
+ "title": "线程协作与线程状态"
+ },
+ "java-12": {
+ "questions": [
+ "bb799ba354",
+ "5efd34dc3a",
+ "0a5b56bc23"
+ ],
+ "route": "java",
+ "sig": "7208abc17a",
+ "stage": "Java 并发",
+ "status": "active",
+ "title": "J.U.C 的常用组件"
+ },
+ "java-13": {
+ "questions": [
+ "454c7daf16",
+ "bde7a7dfbe",
+ "e2b985d29c"
+ ],
+ "route": "java",
+ "sig": "0664d8544b",
+ "stage": "Java 并发",
+ "status": "active",
+ "title": "Java 内存模型与 volatile"
+ },
+ "java-14": {
+ "questions": [
+ "4383694e3e",
+ "36367df7c4",
+ "e326fb1c48"
+ ],
+ "route": "java",
+ "sig": "4faac9d338",
+ "stage": "Java 并发",
+ "status": "active",
+ "title": "线程安全与 CAS"
+ },
+ "java-15": {
+ "questions": [
+ "97c02eae0a",
+ "d5ef30f656",
+ "3896059e83"
+ ],
+ "route": "java",
+ "sig": "64c6db5ed9",
+ "stage": "Java 并发",
+ "status": "active",
+ "title": "ThreadLocal 与锁优化"
+ },
+ "java-16": {
+ "questions": [
+ "d175c9b0a1",
+ "d30a0abfb5",
+ "a806a94773"
+ ],
+ "route": "java",
+ "sig": "8179b551b2",
+ "stage": "Java 虚拟机",
+ "status": "active",
+ "title": "运行时数据区域"
+ },
+ "java-17": {
+ "questions": [
+ "152f56d6c4",
+ "d4c6da97dc",
+ "71ab53cab0"
+ ],
+ "route": "java",
+ "sig": "71c6e96394",
+ "stage": "Java 虚拟机",
+ "status": "active",
+ "title": "对象存活与垃圾收集算法"
+ },
+ "java-18": {
+ "questions": [
+ "7796810063",
+ "b432826e95",
+ "347baa0bbd"
+ ],
+ "route": "java",
+ "sig": "51793d06cc",
+ "stage": "Java 虚拟机",
+ "status": "active",
+ "title": "垃圾收集器"
+ },
+ "java-19": {
+ "questions": [
+ "1df5ce34bc",
+ "f9317cc094",
+ "01d822171f"
+ ],
+ "route": "java",
+ "sig": "a688f7aba8",
+ "stage": "Java 虚拟机",
+ "status": "active",
+ "title": "三色标记与内存分配"
+ },
+ "java-20": {
+ "questions": [
+ "3011568278",
+ "15d74500a9",
+ "6159fb310f"
+ ],
+ "route": "java",
+ "sig": "9a5a5c658b",
+ "stage": "Java 虚拟机",
+ "status": "active",
+ "title": "类加载机制"
+ },
+ "java-21": {
+ "questions": [
+ "75d8147fc6",
+ "f806988135",
+ "d82af70a7a"
+ ],
+ "route": "java",
+ "sig": "96984118f8",
+ "stage": "Java 虚拟机",
+ "status": "active",
+ "title": "线上问题排查"
+ },
+ "java-22": {
+ "questions": [
+ "68acba4c85",
+ "e93ce780bc",
+ "fc65cb524a"
+ ],
+ "route": "java",
+ "sig": "9f4404f0d9",
+ "stage": "MySQL",
+ "status": "active",
+ "title": "事务、并发一致性与锁"
+ },
+ "java-23": {
+ "questions": [
+ "48101acfd2",
+ "a1c4bbe5c6",
+ "47712a7a5f"
+ ],
+ "route": "java",
+ "sig": "b840538d9f",
+ "stage": "MySQL",
+ "status": "active",
+ "title": "隔离级别与 MVCC"
+ },
+ "java-24": {
+ "questions": [
+ "59a27f0b68",
+ "76eb545f8c",
+ "52c9969ca3"
+ ],
+ "route": "java",
+ "sig": "b335016f18",
+ "stage": "MySQL",
+ "status": "active",
+ "title": "Next-Key Lock 与死锁"
+ },
+ "java-25": {
+ "questions": [
+ "4391e4bcee",
+ "91de874283",
+ "f311870155"
+ ],
+ "route": "java",
+ "sig": "2638e86add",
+ "stage": "MySQL",
+ "status": "active",
+ "title": "B+ 树与索引的类型"
+ },
+ "java-26": {
+ "questions": [
+ "56455baece",
+ "16787de94b",
+ "80d3fed1b5"
+ ],
+ "route": "java",
+ "sig": "aaa9a44152",
+ "stage": "MySQL",
+ "status": "active",
+ "title": "索引的使用"
+ },
+ "java-27": {
+ "questions": [
+ "b9fdebae51",
+ "c0ce068130",
+ "fddac344d2"
+ ],
+ "route": "java",
+ "sig": "6af43022ca",
+ "stage": "MySQL",
+ "status": "active",
+ "title": "查询性能优化"
+ },
+ "java-28": {
+ "questions": [
+ "f399525472",
+ "394e2ef8d2",
+ "fd1182bd45"
+ ],
+ "route": "java",
+ "sig": "a310b8d5cd",
+ "stage": "MySQL",
+ "status": "active",
+ "title": "存储引擎、切分与复制"
+ },
+ "java-29": {
+ "questions": [
+ "a79dbaa169",
+ "1528bcdda2",
+ "21184e47ef"
+ ],
+ "route": "java",
+ "sig": "7a40e52189",
+ "stage": "MySQL",
+ "status": "active",
+ "title": "日志"
+ },
+ "java-30": {
+ "questions": [
+ "0aad3661f4",
+ "d977db9dde",
+ "535132ab86"
+ ],
+ "route": "java",
+ "sig": "5afc6efca0",
+ "stage": "Redis 与缓存",
+ "status": "active",
+ "title": "Redis 的数据类型"
+ },
+ "java-31": {
+ "questions": [
+ "babcc095aa",
+ "2081fb3147",
+ "a96d51b500"
+ ],
+ "route": "java",
+ "sig": "02311b9d62",
+ "stage": "Redis 与缓存",
+ "status": "active",
+ "title": "使用场景与底层结构"
+ },
+ "java-32": {
+ "questions": [
+ "af9254f7f2",
+ "fb32f47b96",
+ "4489ec0668"
+ ],
+ "route": "java",
+ "sig": "b369769ba9",
+ "stage": "Redis 与缓存",
+ "status": "active",
+ "title": "过期、淘汰、持久化与事务"
+ },
+ "java-33": {
+ "questions": [
+ "be8cf2c3e0",
+ "ab0c96ab4a",
+ "9de3a8a2c2"
+ ],
+ "route": "java",
+ "sig": "2acf2d18a4",
+ "stage": "Redis 与缓存",
+ "status": "active",
+ "title": "线程模型、复制与集群"
+ },
+ "java-34": {
+ "questions": [
+ "c79e20122e",
+ "4978768999",
+ "57ed7e67f6"
+ ],
+ "route": "java",
+ "sig": "d9e1a3616b",
+ "stage": "Redis 与缓存",
+ "status": "active",
+ "title": "缓存的位置与三种问题"
+ },
+ "java-35": {
+ "questions": [
+ "218297315a",
+ "6ee29f4ce5",
+ "082e65c58c"
+ ],
+ "route": "java",
+ "sig": "d47c240757",
+ "stage": "Redis 与缓存",
+ "status": "active",
+ "title": "缓存一致性与一致性哈希"
+ },
+ "java-36": {
+ "questions": [
+ "1d67fbd177",
+ "87aaeeaa89",
+ "b21b2cd3d2"
+ ],
+ "route": "java",
+ "sig": "77c2164ad9",
+ "stage": "计算机网络",
+ "status": "active",
+ "title": "TCP 的连接管理"
+ },
+ "java-37": {
+ "questions": [
+ "0214655d6c",
+ "221ffa2522",
+ "d45f6f5a22"
+ ],
+ "route": "java",
+ "sig": "ca41cb00a8",
+ "stage": "计算机网络",
+ "status": "active",
+ "title": "可靠传输与拥塞控制"
+ },
+ "java-38": {
+ "questions": [
+ "f79e6d406d",
+ "699cec5d70",
+ "fd090d3343"
+ ],
+ "route": "java",
+ "sig": "63ce9fe4cd",
+ "stage": "计算机网络",
+ "status": "active",
+ "title": "HTTP 基础"
+ },
+ "java-39": {
+ "questions": [
+ "72afd7353e",
+ "3b0c37d0ee",
+ "f9d6fbbf46"
+ ],
+ "route": "java",
+ "sig": "4ef74f7447",
+ "stage": "计算机网络",
+ "status": "active",
+ "title": "连接管理与 Cookie"
+ },
+ "java-40": {
+ "questions": [
+ "f308884a22",
+ "4e79ae834e",
+ "710b3551d1"
+ ],
+ "route": "java",
+ "sig": "39dc79c3c1",
+ "stage": "计算机网络",
+ "status": "active",
+ "title": "缓存、WebSocket 与跨域"
+ },
+ "java-41": {
+ "questions": [
+ "a3c215dfb9",
+ "29246511a6",
+ "6dbf8f2ba2"
+ ],
+ "route": "java",
+ "sig": "e9b030d100",
+ "stage": "计算机网络",
+ "status": "active",
+ "title": "HTTPS"
+ },
+ "java-42": {
+ "questions": [
+ "eb8b8fb51f",
+ "6236dc8524",
+ "271db04da7"
+ ],
+ "route": "java",
+ "sig": "1a7871d767",
+ "stage": "计算机网络",
+ "status": "active",
+ "title": "HTTP 的版本、GET 与 POST、DNS"
+ },
+ "java-43": {
+ "questions": [
+ "dd70642b39",
+ "a4e729949e",
+ "7ce5cde1cd"
+ ],
+ "route": "java",
+ "sig": "6563f58ac8",
+ "stage": "计算机网络",
+ "status": "active",
+ "title": "从输入 URL 到显示页面"
+ },
+ "java-44": {
+ "questions": [
+ "ea588f5661",
+ "7ec003a5dd",
+ "0ac347cf06"
+ ],
+ "route": "java",
+ "sig": "0c596cf628",
+ "stage": "操作系统",
+ "status": "active",
+ "title": "进程与线程"
+ },
+ "java-45": {
+ "questions": [
+ "5cfaefbdbb",
+ "28fb95c127",
+ "94fc7c8234"
+ ],
+ "route": "java",
+ "sig": "ae085852e5",
+ "stage": "操作系统",
+ "status": "active",
+ "title": "系统调用、进程同步与通信"
+ },
+ "java-46": {
+ "questions": [
+ "64f1daf686",
+ "b129be802f",
+ "8d674478d1"
+ ],
+ "route": "java",
+ "sig": "1b9213d938",
+ "stage": "操作系统",
+ "status": "active",
+ "title": "死锁"
+ },
+ "java-47": {
+ "questions": [
+ "62d04c530a",
+ "752067dae4",
+ "80afd5ae0b"
+ ],
+ "route": "java",
+ "sig": "8c96a15e75",
+ "stage": "操作系统",
+ "status": "active",
+ "title": "内存管理"
+ },
+ "java-48": {
+ "questions": [
+ "a9201474fc",
+ "7ef8c8b5a2",
+ "20c2e1e5be"
+ ],
+ "route": "java",
+ "sig": "00d1f75e0b",
+ "stage": "操作系统",
+ "status": "active",
+ "title": "I/O 多路复用"
+ },
+ "java-49": {
+ "questions": [
+ "3f287e23ea",
+ "e4be6236bb",
+ "25bf570498"
+ ],
+ "route": "java",
+ "sig": "59da29c1f9",
+ "stage": "Spring 与设计模式",
+ "status": "active",
+ "title": "IoC 与 Bean"
+ },
+ "java-50": {
+ "questions": [
+ "2d35e2a6e9",
+ "7d1a93e400",
+ "eab615f7b3"
+ ],
+ "route": "java",
+ "sig": "c8d70664df",
+ "stage": "Spring 与设计模式",
+ "status": "active",
+ "title": "AOP 与事务"
+ },
+ "java-51": {
+ "questions": [
+ "30520434c8",
+ "da147d6f3a",
+ "a61b813949"
+ ],
+ "route": "java",
+ "sig": "440c855487",
+ "stage": "Spring 与设计模式",
+ "status": "active",
+ "title": "Spring Boot、Spring MVC 与 MyBatis"
+ },
+ "java-52": {
+ "questions": [
+ "1a5bcb758d",
+ "100d141107",
+ "18a112ceaf"
+ ],
+ "route": "java",
+ "sig": "6eb9107734",
+ "stage": "Spring 与设计模式",
+ "status": "active",
+ "title": "设计模式"
+ },
+ "java-53": {
+ "questions": [
+ "874987f0de",
+ "283fe326d9",
+ "f76f14f53c"
+ ],
+ "route": "java",
+ "sig": "71334cf903",
+ "stage": "分布式与消息队列",
+ "status": "active",
+ "title": "微服务"
+ },
+ "java-54": {
+ "questions": [
+ "4c736dfa3f",
+ "7040328bec",
+ "86472abfac"
+ ],
+ "route": "java",
+ "sig": "dcd58ad57b",
+ "stage": "分布式与消息队列",
+ "status": "active",
+ "title": "分布式锁"
+ },
+ "java-55": {
+ "questions": [
+ "d707267199",
+ "3eda6c30d3",
+ "e2b9c83f1c"
+ ],
+ "route": "java",
+ "sig": "de34216b56",
+ "stage": "分布式与消息队列",
+ "status": "active",
+ "title": "分布式事务与 CAP"
+ },
+ "java-56": {
+ "questions": [
+ "d97306ed39",
+ "57e6bf391b",
+ "a9fdb0e73d"
+ ],
+ "route": "java",
+ "sig": "9c0c92eda6",
+ "stage": "分布式与消息队列",
+ "status": "active",
+ "title": "RPC、分布式 ID 与幂等"
+ },
+ "java-57": {
+ "questions": [
+ "a21036a9de",
+ "b6e89f7079",
+ "3362091f47"
+ ],
+ "route": "java",
+ "sig": "330dcd9b4f",
+ "stage": "分布式与消息队列",
+ "status": "active",
+ "title": "消息队列的作用与可靠性"
+ },
+ "java-58": {
+ "questions": [
+ "0e366e4baa",
+ "d3574a433d",
+ "2309010a02"
+ ],
+ "route": "java",
+ "sig": "066b4100b7",
+ "stage": "分布式与消息队列",
+ "status": "active",
+ "title": "顺序、积压与延迟消息"
+ },
+ "java-59": {
+ "questions": [
+ "17a016b327",
+ "e554283074",
+ "b06acac358"
+ ],
+ "route": "java",
+ "sig": "eeedb47fdc",
+ "stage": "分布式与消息队列",
+ "status": "active",
+ "title": "Kafka"
+ },
+ "java-60": {
+ "questions": [
+ "302564cde9",
+ "fa62f2e19c",
+ "d47bb94d7b"
+ ],
+ "route": "java",
+ "sig": "6319016bb7",
+ "stage": "系统设计",
+ "status": "active",
+ "title": "性能与可用性"
+ },
+ "java-61": {
+ "questions": [
+ "4baf13900d",
+ "7032e684b1",
+ "f5bc9de8b8"
+ ],
+ "route": "java",
+ "sig": "b3c5607e14",
+ "stage": "系统设计",
+ "status": "active",
+ "title": "秒杀与抢红包"
+ },
+ "java-62": {
+ "questions": [
+ "488c66ff9c",
+ "85d1c43400",
+ "857dd1fbdb"
+ ],
+ "route": "java",
+ "sig": "b8fd27960b",
+ "stage": "系统设计",
+ "status": "active",
+ "title": "海量数据、短链与 Feed 流"
+ },
+ "java-63": {
+ "questions": [
+ "6d49cb17be",
+ "b1361d5a9a",
+ "bf51732677"
+ ],
+ "route": "java",
+ "sig": "decc93632e",
+ "stage": "系统设计",
+ "status": "active",
+ "title": "高并发系统的设计"
+ },
+ "java-64": {
+ "questions": [
+ "f38768f09b",
+ "b3dc5cbe84",
+ "89a386e6ca"
+ ],
+ "route": "java",
+ "sig": "7fdda12d7a",
+ "stage": "系统设计",
+ "status": "active",
+ "title": "Docker"
+ },
+ "java-65": {
+ "questions": [
+ "9dde8d6033",
+ "5f403638bf",
+ "c8485a2dab"
+ ],
+ "route": "java",
+ "sig": "c5f573017a",
+ "stage": "系统设计",
+ "status": "active",
+ "title": "Kubernetes"
+ },
+ "java-66": {
+ "questions": [
+ "cd627f049d",
+ "b813e96d80",
+ "34f42af76f"
+ ],
+ "route": "java",
+ "sig": "bfe3bf196a",
+ "stage": "系统设计",
+ "status": "active",
+ "title": "系统设计题的思路与即时通讯"
+ },
+ "java-67": {
+ "questions": [
+ "c93bb3c65b",
+ "e1c4dd69ef",
+ "2c37283a5c"
+ ],
+ "route": "java",
+ "sig": "6ed51dc735",
+ "stage": "系统设计",
+ "status": "active",
+ "title": "订单与支付"
+ },
+ "java-68": {
+ "questions": [
+ "16e0b97125",
+ "ec06220d69",
+ "2c02de7f11"
+ ],
+ "route": "java",
+ "sig": "49bc06a543",
+ "stage": "大模型应用",
+ "status": "active",
+ "title": "RAG 与 Agent"
+ },
+ "java-69": {
+ "questions": [
+ "efe411f83c",
+ "c5494b65a3",
+ "904bc79056"
+ ],
+ "route": "java",
+ "sig": "3d4ec02919",
+ "stage": "大模型应用",
+ "status": "active",
+ "title": "MCP、Skills 与 AI 编程助手"
+ },
+ "llm-01": {
+ "questions": [
+ "cff796edde",
+ "98a89cafcd",
+ "aeb37c713b"
+ ],
+ "route": "llm",
+ "sig": "81ace0c3e4",
+ "stage": "大模型基础",
+ "status": "active",
+ "title": "Token 与 Transformer"
+ },
+ "llm-02": {
+ "questions": [
+ "128dcc61a9",
+ "0b76f3a00d",
+ "479c864f58"
+ ],
+ "route": "llm",
+ "sig": "bb9e8fd086",
+ "stage": "大模型基础",
+ "status": "active",
+ "title": "注意力机制"
+ },
+ "llm-03": {
+ "questions": [
+ "e4952a889a",
+ "493fe133cc",
+ "beeadc9d28"
+ ],
+ "route": "llm",
+ "sig": "1f17c0dd24",
+ "stage": "大模型基础",
+ "status": "active",
+ "title": "生成、KV Cache 与采样"
+ },
+ "llm-04": {
+ "questions": [
+ "e2bec394b4",
+ "f319983c2c",
+ "e628ba7bc1"
+ ],
+ "route": "llm",
+ "sig": "19969e06c1",
+ "stage": "大模型基础",
+ "status": "active",
+ "title": "训练过程"
+ },
+ "llm-05": {
+ "questions": [
+ "c4e5f8bc1a",
+ "f2ed00f34c",
+ "fa683c8ea1"
+ ],
+ "route": "llm",
+ "sig": "0650f10589",
+ "stage": "大模型基础",
+ "status": "active",
+ "title": "微调与量化"
+ },
+ "llm-06": {
+ "questions": [
+ "e90dd83642",
+ "8a7213cfde",
+ "3d4ae89337"
+ ],
+ "route": "llm",
+ "sig": "355a1a9ef7",
+ "stage": "大模型基础",
+ "status": "active",
+ "title": "幻觉与推理模型"
+ },
+ "llm-07": {
+ "questions": [
+ "8c4d592987",
+ "888d45b3b3",
+ "92dc236853"
+ ],
+ "route": "llm",
+ "sig": "cf9fcea4a9",
+ "stage": "提示词与上下文",
+ "status": "active",
+ "title": "提示词与结构化输出"
+ },
+ "llm-08": {
+ "questions": [
+ "63f07a4386",
+ "0a3efb066b",
+ "23a4be74d7"
+ ],
+ "route": "llm",
+ "sig": "77a4054a33",
+ "stage": "提示词与上下文",
+ "status": "active",
+ "title": "上下文工程"
+ },
+ "llm-09": {
+ "questions": [
+ "03016da606",
+ "45bf4a5b4d",
+ "4ff0d2d0d0"
+ ],
+ "route": "llm",
+ "sig": "697f70f997",
+ "stage": "检索增强生成",
+ "status": "active",
+ "title": "RAG 的流程、文档解析与切片"
+ },
+ "llm-10": {
+ "questions": [
+ "b9bc59a83b",
+ "f2d13da1ee",
+ "da52b096ec"
+ ],
+ "route": "llm",
+ "sig": "2d7f462d49",
+ "stage": "检索增强生成",
+ "status": "active",
+ "title": "Embedding 与向量索引"
+ },
+ "llm-11": {
+ "questions": [
+ "e9083e85a7",
+ "91d75f1bb2",
+ "c82b36ba2a"
+ ],
+ "route": "llm",
+ "sig": "aa9d7e0cf5",
+ "stage": "检索增强生成",
+ "status": "active",
+ "title": "向量数据库、混合检索与重排"
+ },
+ "llm-12": {
+ "questions": [
+ "1cb0108be7",
+ "6d00dbcd74",
+ "80e07271f2"
+ ],
+ "route": "llm",
+ "sig": "f092e64977",
+ "stage": "检索增强生成",
+ "status": "active",
+ "title": "Top-K 与查询改写"
+ },
+ "llm-13": {
+ "questions": [
+ "a53acc431d",
+ "a7f4a61121",
+ "b5c9ebc9ee"
+ ],
+ "route": "llm",
+ "sig": "7e7af62853",
+ "stage": "检索增强生成",
+ "status": "active",
+ "title": "RAG 的评测与扩展"
+ },
+ "llm-14": {
+ "questions": [
+ "513b0767e9",
+ "a2d1d91ef7",
+ "c226c6df57"
+ ],
+ "route": "llm",
+ "sig": "0a16a4cfab",
+ "stage": "Agent",
+ "status": "active",
+ "title": "Agent 与工具调用"
+ },
+ "llm-15": {
+ "questions": [
+ "c9434cba34",
+ "a645db12eb",
+ "1cd94a5fd7"
+ ],
+ "route": "llm",
+ "sig": "14048899f3",
+ "stage": "Agent",
+ "status": "active",
+ "title": "意图识别与规划"
+ },
+ "llm-16": {
+ "questions": [
+ "bc38dbf743",
+ "5b6919dab0",
+ "4bf4ba1fb0"
+ ],
+ "route": "llm",
+ "sig": "de47d8b380",
+ "stage": "Agent",
+ "status": "active",
+ "title": "记忆与多轮对话"
+ },
+ "llm-17": {
+ "questions": [
+ "d0cadbf383",
+ "676b456ee1"
+ ],
+ "route": "llm",
+ "sig": "4b80a829b6",
+ "stage": "Agent",
+ "status": "active",
+ "title": "MCP 与 Skills"
+ },
+ "llm-18": {
+ "questions": [
+ "634ab6d850",
+ "72f9842f42",
+ "4ec5716b53"
+ ],
+ "route": "llm",
+ "sig": "99f0739a24",
+ "stage": "Agent",
+ "status": "active",
+ "title": "多 Agent 与 Harness"
+ },
+ "llm-19": {
+ "questions": [
+ "bdc1af9732",
+ "bfcd308f00",
+ "bbc2863617"
+ ],
+ "route": "llm",
+ "sig": "6f37433fcb",
+ "stage": "Agent",
+ "status": "active",
+ "title": "开发框架、可靠性与评测"
+ },
+ "llm-20": {
+ "questions": [
+ "e36c204678",
+ "e8de3674b2",
+ "100f1b6f0b"
+ ],
+ "route": "llm",
+ "sig": "346ac5093e",
+ "stage": "工程实践",
+ "status": "active",
+ "title": "流式输出、成本与部署"
+ },
+ "llm-21": {
+ "questions": [
+ "52aa24db2a",
+ "1313e1319a",
+ "9f8d77ff45"
+ ],
+ "route": "llm",
+ "sig": "1ef0a754bf",
+ "stage": "工程实践",
+ "status": "active",
+ "title": "稳定性与安全"
+ },
+ "sprint-01": {
+ "questions": [
+ "56d0a4ee0e",
+ "0679b458cf",
+ "9c0c516330"
+ ],
+ "route": "sprint",
+ "sig": "7c00e65e35",
+ "stage": "Java",
+ "status": "active",
+ "title": "集合"
+ },
+ "sprint-02": {
+ "questions": [
+ "e4941b7494",
+ "f3cbce1b55",
+ "14a8b66241"
+ ],
+ "route": "sprint",
+ "sig": "437c9782af",
+ "stage": "Java",
+ "status": "active",
+ "title": "线程池、锁与 volatile"
+ },
+ "sprint-03": {
+ "questions": [
+ "36367df7c4",
+ "efa6604d75",
+ "97c02eae0a"
+ ],
+ "route": "sprint",
+ "sig": "0208298a44",
+ "stage": "Java",
+ "status": "active",
+ "title": "CAS、锁升级与 ThreadLocal"
+ },
+ "sprint-04": {
+ "questions": [
+ "b5eb99096c",
+ "b432826e95",
+ "75d8147fc6"
+ ],
+ "route": "sprint",
+ "sig": "14e300ab57",
+ "stage": "Java",
+ "status": "active",
+ "title": "JVM"
+ },
+ "sprint-05": {
+ "questions": [
+ "bc787d309a",
+ "76647ed961",
+ "d560ae1052"
+ ],
+ "route": "sprint",
+ "sig": "59d9d1a94e",
+ "stage": "MySQL",
+ "status": "active",
+ "title": "索引"
+ },
+ "sprint-06": {
+ "questions": [
+ "9a4ab4e114",
+ "955ea8cac8",
+ "59a27f0b68"
+ ],
+ "route": "sprint",
+ "sig": "5bf91b50c1",
+ "stage": "MySQL",
+ "status": "active",
+ "title": "事务、MVCC 与锁"
+ },
+ "sprint-07": {
+ "questions": [
+ "b9fdebae51",
+ "a5d2df43ec",
+ "394e2ef8d2"
+ ],
+ "route": "sprint",
+ "sig": "147d845c87",
+ "stage": "MySQL",
+ "status": "active",
+ "title": "SQL 优化、日志与分库分表"
+ },
+ "sprint-08": {
+ "questions": [
+ "4f74d11814",
+ "fb32f47b96",
+ "a0f17ec6e9"
+ ],
+ "route": "sprint",
+ "sig": "b10a65d9e0",
+ "stage": "Redis 与缓存",
+ "status": "active",
+ "title": "Redis"
+ },
+ "sprint-09": {
+ "questions": [
+ "c79e20122e",
+ "218297315a",
+ "c7974bfd54"
+ ],
+ "route": "sprint",
+ "sig": "42c1273538",
+ "stage": "Redis 与缓存",
+ "status": "active",
+ "title": "缓存问题与 Redis 集群"
+ },
+ "sprint-10": {
+ "questions": [
+ "f87401d466",
+ "2eacd733a9",
+ "a3c215dfb9"
+ ],
+ "route": "sprint",
+ "sig": "05ef6623f8",
+ "stage": "网络与操作系统",
+ "status": "active",
+ "title": "TCP 与 HTTPS"
+ },
+ "sprint-11": {
+ "questions": [
+ "1c3ffb95a4",
+ "d2c8cce9c3",
+ "a9201474fc"
+ ],
+ "route": "sprint",
+ "sig": "dcec70f902",
+ "stage": "网络与操作系统",
+ "status": "active",
+ "title": "进程、死锁与 I/O 多路复用"
+ },
+ "sprint-12": {
+ "questions": [
+ "2d35e2a6e9",
+ "e6469eab7d",
+ "a79be5580a"
+ ],
+ "route": "sprint",
+ "sig": "d6b2df2bd0",
+ "stage": "框架、分布式与大模型",
+ "status": "active",
+ "title": "Spring 与设计模式"
+ },
+ "sprint-13": {
+ "questions": [
+ "8325776328",
+ "747ba7232d",
+ "4fad9621d5"
+ ],
+ "route": "sprint",
+ "sig": "ead905739d",
+ "stage": "框架、分布式与大模型",
+ "status": "active",
+ "title": "分布式锁、消息队列与秒杀"
+ },
+ "sprint-14": {
+ "questions": [
+ "a03e2ca327",
+ "f62bbcd15d",
+ "485504eb0f"
+ ],
+ "route": "sprint",
+ "sig": "bef1d7ba3e",
+ "stage": "框架、分布式与大模型",
+ "status": "active",
+ "title": "大模型应用"
+ }
+ },
+ "history": [],
+ "manual": {
+ "map": {},
+ "separate": []
+ },
+ "questions": {
+ "01d822171f": {
+ "days": [
+ "java-19"
+ ],
+ "points": "调用 System.gc(),规范上只是建议,HotSpot 默认会执行一次 Full GC,-XX:+DisableExplicitGC 可以禁用;老年代空间不足,例如大对象、长期存活的对象不断进入,或者空间分配担保失败,G1 中是混合回收来不及释放、复制时找不到空闲 Region,或 Humongous 对象找不到连续的 Region;JDK 7 及以前永久代不足,JDK 8 起元空间达到阈值(日志原因为 Metadata GC Threshold)时 Serial 和 ",
+ "status": "active",
+ "text": "Full GC 在什么情况下触发?"
+ },
+ "0214655d6c": {
+ "days": [
+ "java-37"
+ ],
+ "points": "检验和:出错的报文段直接丢弃;序号和累积确认:去重、排序,SACK 告诉发送方收到了哪些区间;重传:超时重传(RTO 按 RTT 动态计算、超时后翻倍),或收到 3 个重复确认时快重传;滑动窗口、流量控制和拥塞控制让发送速率不超过接收方和网络的能力。",
+ "status": "active",
+ "text": "TCP 怎样保证可靠传输?"
+ },
+ "03016da606": {
+ "days": [
+ "llm-09"
+ ],
+ "points": "离线:解析文档、切片,用 Embedding 模型转为向量存入向量数据库,同时建立关键词索引;在线:改写问题,向量和关键词两路召回,合并后重排,把最相关的几段和问题放进提示词,生成带引用的回答;提示词中给切片编号,要求每个结论标注来源,资料中没有答案时如实回答没找到。",
+ "status": "active",
+ "text": "RAG 的完整流程是怎样的?"
+ },
+ "0679b458cf": {
+ "days": [
+ "java-08",
+ "sprint-01"
+ ],
+ "points": "JDK 1.7 用分段锁,Segment 继承 ReentrantLock,默认 16 个,最多 16 个线程同时写,并发度创建后不能改;JDK 1.8 去掉 Segment,用 Node 数组加链表或红黑树,空桶用 CAS 插入,非空桶用 synchronized 锁住头节点,锁粒度缩小到桶;get 不加锁,靠 volatile 读到最新值。",
+ "status": "active",
+ "text": "ConcurrentHashMap 怎样保证线程安全?JDK 1.7 和 1.8 有什么区别?"
+ },
+ "082e65c58c": {
+ "days": [
+ "java-35"
+ ],
+ "points": "取模分布在节点数变化时几乎所有键都要迁移,一致性哈希把节点和键放在哈希环上,键存到顺时针第一个节点,增删节点只影响相邻节点,4 个节点扩到 5 个约 18% 的键迁移;节点少时分布不均(最多 44.7%、最少 9.0%),每个节点放 150 个虚拟节点后占比在 22.8%~27.0%。",
+ "status": "active",
+ "text": "一致性哈希解决了什么问题?虚拟节点有什么用?"
+ },
+ "0a3efb066b": {
+ "days": [
+ "llm-08"
+ ],
+ "points": "方法有截断(只留最近几轮,丢失早期内容)、摘要(多一次调用,可能漏掉或改写细节)、清理旧的工具结果(风险最小)和外置到文件,工具调用和结果要成对删除;常组合成“系统提示词 + 摘要 + 最近几轮原文 + 当前问题”;时机按 Token 数而不是轮数,约 70% 时在后台生成摘要,到上限时必须先压缩。",
+ "status": "active",
+ "text": "对话历史太长时怎样压缩?什么时候压缩?"
+ },
+ "0a5b56bc23": {
+ "days": [
+ "java-12"
+ ],
+ "points": "用有界的 ArrayBlockingQueue 作为共享缓冲区;生产者调用 put(),队列满时阻塞;消费者调用 take(),队列空时阻塞,不需要自己写 wait()/notify()。",
+ "status": "active",
+ "text": "怎样用 BlockingQueue 实现生产者消费者?"
+ },
+ "0aad3661f4": {
+ "days": [
+ "java-30"
+ ],
+ "points": "STRING 缓存对象、计数器、分布式锁,LIST 按插入顺序两端读写,例如最新 N 条消息、简单的消息队列;SET 去重和求交集、并集,例如标签、共同好友,HASH 保存对象的多个字段,例如用户信息、购物车;ZSET 按分值排序和范围查找,例如排行榜、按时间排序的 Feed;BITMAP、HyperLogLog 用于签到和 UV 统计,GEO 用于附近的门店,Stream 用于带消费者组的消息。",
+ "status": "active",
+ "text": "Redis 有哪些数据类型?分别用在什么场景?"
+ },
+ "0ac347cf06": {
+ "days": [
+ "java-44"
+ ],
+ "points": "协程在用户态由运行时调度,Go 和 Java 虚拟线程把大量协程映射到少量线程上(M:N),Python asyncio 则在一个线程的事件循环中执行,切换不陷入内核、内存占用小,可以大量创建,但同一线程上的协程不能并行;虚拟线程是 JVM 实现的有栈协程,由 ForkJoinPool 的平台线程(默认等于 CPU 核数)执行,阻塞 I/O 时卸载,栈帧以栈块对象存入堆,就绪后再挂载到某个平台线程继续执行;JDK 21 中在 synchronized 内阻塞无法卸载,JDK ",
+ "status": "active",
+ "text": "协程和线程有什么区别?Java 的虚拟线程是怎样实现的?"
+ },
+ "0b76f3a00d": {
+ "days": [
+ "llm-02"
+ ],
+ "points": "单头注意力中每个 Token 只有一组权重,只能按一种标准挑选信息;多头把 Q、K、V 切成 h 份分别计算,拼接后乘 WO ,不同的头可以关注指代、相邻词等不同关系;每头维度为 d / h,计算量与单头基本相同。",
+ "status": "active",
+ "text": "多头注意力有什么作用?"
+ },
+ "0e366e4baa": {
+ "days": [
+ "java-58"
+ ],
+ "points": "消息队列只保证单个队列(Kafka 的分区)内有序;发送端按业务 Key(如订单号)把同一业务的消息发到同一个队列;接收端一个队列只由一个线程顺序消费,Kafka 中一个分区只分给组内一个消费者,RabbitMQ 用单个消费者或单活跃消费者;生产者重试可能让后发的消息先写入,Kafka 默认开启的幂等生产者用序号避免了这种乱序;分区数改变后同一 key 可能落到别的分区,要预先规划分区数。",
+ "status": "active",
+ "text": "怎样保证消息按顺序消费?"
+ },
+ "100d141107": {
+ "days": [
+ "java-52"
+ ],
+ "points": "例如下单校验用责任链:参数、限购、黑名单、库存各写一个 OrderValidator 实现类,用 @Order 排序,OrderService 注入 List 依次调用,不通过就抛异常;新增校验只需再写一个实现类,OrderService 不用改;类似地,运费和支付渠道用策略加工厂,支付成功后的通知用观察者。",
+ "status": "active",
+ "text": "项目中用过哪些设计模式?举一个例子。"
+ },
+ "100f1b6f0b": {
+ "days": [
+ "llm-20"
+ ],
+ "points": "早期系统按最大长度为每个请求预留连续的显存,真正存放 KV Cache 的只有 20.4%~38.2%;PagedAttention 借鉴操作系统的分页,把 KV Cache 切成固定大小的块(vLLM 默认 16 个 Token)按需分配,用块表记录位置,只浪费最后一块的空余;同样 7 GB 显存按 8K 预留只能服务 16 个请求,请求平均只用到 2K Token 时按块分配可服务 64 个;延迟相近时吞吐是 FasterTransformer、Orca 的 2~4 倍。",
+ "status": "active",
+ "text": "PagedAttention 解决了什么问题?"
+ },
+ "128dcc61a9": {
+ "days": [
+ "llm-02"
+ ],
+ "points": "公式为 softmax(QKT / √d) V:Q 与各 K 点积得到分数,softmax 成权重后对 V 加权求和;分量独立、均值 0、方差 1 时点积的方差为 d,维度大时 softmax 几乎集中到一个 Token 上,梯度接近 0;除以 √d 使方差恢复为 1。",
+ "status": "active",
+ "text": "注意力的计算公式是什么?为什么要除以 √d?"
+ },
+ "1313e1319a": {
+ "days": [
+ "llm-21"
+ ],
+ "points": "还没输出任何内容时直接重试,用户感知不到;已输出部分文字时,把已输出的部分作为 assistant 消息的开头请求续写(如 DeepSeek 的对话前缀续写),不支持就丢弃重新生成并通知前端清空;工具调用的参数只输出了一半时不能执行,丢弃后重新请求。",
+ "status": "active",
+ "text": "流式输出中途断开怎么办?"
+ },
+ "14a8b66241": {
+ "days": [
+ "sprint-02"
+ ],
+ "points": "保证可见性:写入后其它线程读取时立即可见;保证有序性:volatile 写之前、volatile 读之后的读写不会被重排序越过它,可用来发布数据(如双重校验锁单例);不保证 count++ 这类复合操作的原子性,要用锁或原子类。",
+ "status": "active",
+ "text": "volatile 能保证什么,不能保证什么?"
+ },
+ "1528bcdda2": {
+ "days": [
+ "java-29"
+ ],
+ "points": "事务要同时写 redo log 和 binlog,只写成功一个就崩溃会让主从不一致,例如 redo log 写了而 binlog 没写,主库保留修改,从库却收不到;两阶段提交:先写 redo log 并刷盘进入 prepare 状态,再写 binlog 并刷盘(带 Xid),最后在 redo log 中标记提交;崩溃恢复时 prepare 状态的事务在 binlog 中找到 Xid 就提交,否则回滚。",
+ "status": "active",
+ "text": "为什么需要两阶段提交?"
+ },
+ "152f56d6c4": {
+ "days": [
+ "java-17"
+ ],
+ "points": "引用计数无法处理循环引用,JVM 使用可达性分析:从 GC Roots 出发搜索,不可达的对象可以被回收;GC Roots 包括虚拟机栈中的参数和局部变量、JNI 引用的对象、类静态属性和常量引用的对象、被 synchronized 持有的锁对象和还没有结束的线程。",
+ "status": "active",
+ "text": "怎样判断一个对象可以被回收?哪些对象可以作为 GC Roots?"
+ },
+ "1535c683ad": {
+ "days": [
+ "java-03"
+ ],
+ "points": "约定是等价的两个对象哈希值必须相同;HashMap、HashSet 先用 hashCode() 确定桶并比较哈希值,相等才调用 equals();只重写 equals() 时两个等价对象的哈希值通常不同,会被当成两个键存入。",
+ "status": "active",
+ "text": "重写 equals() 时为什么必须重写 hashCode()?"
+ },
+ "154ba456f2": {
+ "days": [
+ "java-09"
+ ],
+ "points": "AbortPolicy(默认)抛出 RejectedExecutionException,CallerRunsPolicy 由提交任务的线程自己执行,DiscardPolicy 直接丢弃,DiscardOldestPolicy 丢弃队列中最早的任务再提交;不能丢且提交方能处理失败时用 Abort,能接受变慢时用 CallerRuns,Netty 的 I/O 线程这类不能阻塞的线程除外;允许丢失的统计上报用 Discard,只关心最新数据时用 DiscardOldest;线程池",
+ "status": "active",
+ "text": "线程池有哪些拒绝策略?怎样选择?"
+ },
+ "15b9ac4352": {
+ "days": [
+ "java-05"
+ ],
+ "points": "会执行,finally 在离开 try 语句之前执行;返回值在执行 finally 之前已经保存,返回基本类型时 finally 修改变量不影响返回值,返回引用时能看到对对象的修改;finally 中的 return 会覆盖返回值并吞掉异常;调用 System.exit()、kill -9 或 JVM 崩溃时 finally 不执行。",
+ "status": "active",
+ "text": "try 和 catch 中有 return 时,finally 还会执行吗?返回值是什么?"
+ },
+ "15d74500a9": {
+ "days": [
+ "java-20"
+ ],
+ "points": "类加载器先把加载请求交给父加载器,父加载器无法完成时才自己加载;好处是基础类统一,例如自己写的 java.lang.Object 不会替换启动类加载器加载的那个,程序中所有的 Object 都是同一个类;父加载器已经加载过的类不会重复加载,避免同一个类出现两个互不相等的 Class 对象。",
+ "status": "active",
+ "text": "什么是双亲委派模型?为什么需要它?"
+ },
+ "16787de94b": {
+ "days": [
+ "java-26"
+ ],
+ "points": "没有从联合索引的最左列开始,或者依赖联合索引中范围条件之后的列缩小扫描范围(范围条件带等号时例外);对索引列使用函数(例如 DATE(create_time)),或者发生隐式类型转换(例如字符串列与数字比较);LIKE 以 % 开头,OR 的一侧没有索引;匹配的行太多,优化器估算全表扫描更快,常见于 !=、NOT IN 和大范围的条件;这些是普通索引上的常见情况,只查索引中的列时可能改为扫描整个索引,缺少最左列时 8.0.13 起可能用跳跃扫描,对表达式建函数索引后函数条件也",
+ "status": "active",
+ "text": "哪些情况下索引会失效?"
+ },
+ "16e0b97125": {
+ "days": [
+ "java-68"
+ ],
+ "points": "离线阶段解析文档、切成小段,用 Embedding 模型把每段转为向量存入向量数据库,同时建立关键词索引;在线阶段先改写问题,向量检索和关键词检索同时召回候选,合并后重排,把最相关的几段资料和问题一起放进提示词,生成带引用的回答;能减少幻觉,是因为模型从凭记忆回答变为根据给定的资料回答,每个结论都能按编号核对;但资料没找到或找错时模型仍会编造,所以要用重排分数和相似度阈值判断有没有答案,没有时改写问题再检索、向用户追问,或者如实回答没找到。",
+ "status": "active",
+ "text": "RAG 的完整流程是什么?它为什么能减少幻觉?"
+ },
+ "17a016b327": {
+ "days": [
+ "java-59"
+ ],
+ "points": "顺序写和页缓存:消息只追加到日志末尾,写入页缓存即可,不要求每条消息刷盘,确认条件仍由 acks 决定,可靠性靠多副本;批量:同一分区的消息攒成批次传输、写入和复制,关闭批量后吞吐从每秒约 6 万条降到约 6600 条;零拷贝:broker 用 sendfile 把日志直接发到 Socket;分区并行:多个分区分布在不同 broker 上。",
+ "status": "active",
+ "text": "Kafka 为什么吞吐量高?"
+ },
+ "18a112ceaf": {
+ "days": [
+ "java-52"
+ ],
+ "points": "工厂(BeanFactory、FactoryBean)和单例(默认的 singleton 作用域);代理:AOP 的 JdkDynamicAopProxy、CglibAopProxy;模板方法:JdbcTemplate、TransactionTemplate;还有观察者(ApplicationEvent、@EventListener)、适配器(HandlerAdapter)、责任链(拦截器链)和策略(InstantiationStrategy)。",
+ "status": "active",
+ "text": "Spring 中用到了哪些设计模式?"
+ },
+ "1a5bcb758d": {
+ "days": [
+ "java-52"
+ ],
+ "points": "写法有懒汉式(线程不安全)、饿汉式、方法加 synchronized、双重校验锁、静态内部类、枚举(还能防反射和反序列化);new Singleton() 分为分配内存、初始化、赋值给引用三步,可能被重排序为先赋值再初始化,另一个线程在外层判空时就会拿到未初始化的对象,volatile 禁止这种重排序。",
+ "status": "active",
+ "text": "单例模式有哪几种写法?双重检查锁为什么要加 volatile?"
+ },
+ "1c0bbdffef": {
+ "days": [
+ "java-10"
+ ],
+ "points": "靠协作:其它线程设置 volatile 停止标记或调用 interrupt(),由线程自己检查后在合适的位置结束;线程在 sleep()、wait()、join() 中时 interrupt() 使其抛出 InterruptedException,捕获后是否结束由代码决定,只设停止标记唤醒不了它;循环中没有这类调用时,用 interrupted() 检查中断标记后退出;interrupt() 不能中断等待 synchronized 锁的阻塞,平台线程阻塞在传统 I/O 上时通",
+ "status": "active",
+ "text": "怎样停止一个正在运行的线程?"
+ },
+ "1c3ffb95a4": {
+ "days": [
+ "sprint-11"
+ ],
+ "points": "进程是资源分配的基本单位,线程是调度的基本单位,线程共享进程的资源,创建和切换开销小,能直接读写共享数据,进程间要用 IPC;进程互相隔离,而一个线程访问非法内存触发段错误时,整个进程都会被终止;协程是在用户态由运行时调度的轻量级线程,切换不陷入内核、内存占用小,大量协程映射到少量线程上,适合 I/O 密集任务,同一线程上的协程不能并行。",
+ "status": "active",
+ "text": "进程、线程和协程有什么区别?"
+ },
+ "1cb0108be7": {
+ "days": [
+ "llm-12"
+ ],
+ "points": "召回的 K 是每路交给重排的候选数(如 50),在评测集上取召回率不再明显上升的位置,有重排时可以取大些;放进提示词的 K 是重排后保留的数量(如 3~5),太大增加成本、混入无关或旧版本资料,太小答不全,按端到端正确率和上下文预算确定。",
+ "status": "active",
+ "text": "召回的 Top-K 和放进提示词的 Top-K 怎么定?"
+ },
+ "1cd94a5fd7": {
+ "days": [
+ "llm-15"
+ ],
+ "points": "类别定义互斥并写明边界,输出限制为枚举值,不在列表中的按“其它”处理;分层:规则只处理“转人工”、带订单号这类精确率高的情况,其余交给模型;置信度低时反问澄清或转人工,阈值在验证集上扫一遍再定,分错代价高的类别(如退款)单独设更高的阈值;多轮对话结合最近几轮;用评测集按类别看精确率、召回率和混淆矩阵,修改后回归。",
+ "status": "active",
+ "text": "怎样保证意图识别和路由准确?"
+ },
+ "1d67fbd177": {
+ "days": [
+ "java-36"
+ ],
+ "points": "客户端发 SYN(序号 x),服务器回 SYN+ACK(序号 y、确认号 x+1),客户端回 ACK(确认号 y+1),双方进入 ESTABLISHED;双方的初始序号都要得到对方确认,两次握手时服务器的序号得不到确认;第三次握手还能让客户端对滞留的旧连接请求回 RST,避免服务器多打开一个连接。",
+ "status": "active",
+ "text": "三次握手的过程是怎样的?为什么不是两次?"
+ },
+ "1df5ce34bc": {
+ "days": [
+ "java-19"
+ ],
+ "points": "并发标记时,用户线程让已经是黑色的对象引用了白色对象,同时删除了从灰色对象到它的所有引用,这个白色对象不会再被访问,标记结束后被当成垃圾回收;CMS 用增量更新,写入引用时写屏障把卡标记为脏,重新标记时重新扫描;G1 用原始快照(SATB),删除或覆盖引用前写屏障记录旧值并当作存活。",
+ "status": "active",
+ "text": "三色标记中的漏标是怎样发生的?CMS 和 G1 分别怎样解决?"
+ },
+ "2081fb3147": {
+ "days": [
+ "java-31"
+ ],
+ "points": "跳表在有序链表上加多层索引,每个节点以 1/4 的概率升一层,查找、插入、删除的期望复杂度都是 O(logN);与平衡树相比,插入删除只改前驱指针,不需要旋转,范围查询找到起点后沿底层链表遍历,实现也更简单;数据在内存中,不需要 B+ 树那样减少磁盘 I/O;每个指针记录跨度,ZRANK 也是 O(logN)。",
+ "status": "active",
+ "text": "ZSET 的底层为什么用跳表?"
+ },
+ "20c2e1e5be": {
+ "days": [
+ "java-48"
+ ],
+ "points": "BIO 对应阻塞式 I/O,线程阻塞在 read 上,每个连接要一个线程;NIO 对应 I/O 复用,Channel 设为非阻塞,一个线程在 Selector.select() 上等待多个连接中任意一个就绪(Linux 上用 epoll 实现);AIO 对应异步 I/O 的编程方式,数据复制到应用的缓冲区后才通知应用,但 Linux 上的 JDK 实际用 epoll 加线程池实现;BIO、NIO 在复制数据时仍会阻塞,属于同步 I/O。",
+ "status": "active",
+ "text": "BIO、NIO 和 AIO 有什么区别?"
+ },
+ "21184e47ef": {
+ "days": [
+ "java-29"
+ ],
+ "points": "以 UPDATE orders SET status = 3 WHERE id = 1024 为例:按主键找到这一行,页不在缓冲池就先读入,并加 X 锁;旧值写入 undo log,修改缓冲池中的数据页并生成 redo log 放入 redo log buffer,执行器把修改写入 binlog cache;提交时按两阶段提交把 redo log 和 binlog 刷盘,释放锁并返回;脏页之后由后台线程写回磁盘。",
+ "status": "active",
+ "text": "一条 UPDATE 语句执行时,三种日志是怎样写的?"
+ },
+ "218297315a": {
+ "days": [
+ "java-35",
+ "sprint-09"
+ ],
+ "points": "用 Cache Aside:读未命中时查数据库并写回缓存,写时先更新数据库(事务提交之后)再删除缓存;删除而不是更新缓存,避免并发写到达缓存的顺序与数据库不同;删除失败或慢读写回旧值时仍会不一致,用过期时间兜底,加上延迟双删、消息队列重试或订阅 binlog 删除;只能最终一致,余额、库存直接读写数据库。",
+ "status": "active",
+ "text": "怎样保证缓存与数据库的一致性?"
+ },
+ "221ffa2522": {
+ "days": [
+ "java-37"
+ ],
+ "points": "流量控制保护接收方:接收方在确认报文的窗口字段中通告缓冲区剩余空间,为 0 时发送方停止发送;拥塞控制保护整个网络:发送方维护拥塞窗口 cwnd,用慢开始、拥塞避免、快重传、快恢复调节;实际发送窗口取 cwnd 和接收窗口中的较小值。",
+ "status": "active",
+ "text": "拥塞控制和流量控制有什么区别?"
+ },
+ "2309010a02": {
+ "days": [
+ "java-58"
+ ],
+ "points": "下单时发一条 30 分钟后投递的延迟消息,到期后消费者用条件更新取消订单(WHERE status = 'UNPAID'),重复或晚到的消息不会取消已支付的订单,要求高的业务再用低频扫描兜底;用户已付款、回调还没到时本地仍是待支付,所以取消前要先向支付平台关单,平台返回已支付就按支付成功处理;RocketMQ 4.x 有 30m 延迟级别,5.x 可指定任意时间,RabbitMQ 用 TTL 加死信队列;也可以用 Redis 有序集合,分值为到期时间,后台线程取到期成员,多实",
+ "status": "active",
+ "text": "怎样实现订单 30 分钟未支付自动取消?"
+ },
+ "23a4be74d7": {
+ "days": [
+ "llm-08"
+ ],
+ "points": "订单号、金额、地址等由代码从工具参数和结果中提取单独保存,承诺通过工具写进记录;摘要分项输出、编号和金额照原文抄写,“已办理”只写有工具结果确认的操作,新摘要在旧摘要的基础上合并;用关键事实覆盖和探针问题检查,实测 3B 的结构化摘要保留 9.7 条事实,普通摘要只有 4.3 条(共 14 条)。",
+ "status": "active",
+ "text": "压缩后怎样保证关键事实不丢失?"
+ },
+ "25bf570498": {
+ "days": [
+ "java-49"
+ ],
+ "points": "不一定:Spring 不做同步,是否安全取决于有没有可变的成员变量;只持有其它 Bean 引用的 Controller、Service 是安全的,把请求数据存进成员变量就会互相覆盖;数据放在局部变量中,必须共享时用 AtomicInteger 等线程安全的类,每个线程各自的数据放在 ThreadLocal 中。",
+ "status": "active",
+ "text": "单例 Bean 是线程安全的吗?"
+ },
+ "271db04da7": {
+ "days": [
+ "java-42"
+ ],
+ "points": "先查浏览器缓存、系统缓存和 hosts,未命中则向本地 DNS 服务器递归查询;本地 DNS 服务器依次迭代询问根服务器、顶级域服务器,它们只返回下一级服务器的地址,最后由权威服务器返回 IP;各级结果按 TTL 缓存;一般用 UDP 53 端口,响应超过 UDP 报文的长度上限(不用 EDNS 时为 512 字节)被截断,或做全量区域传送(AXFR)时用 TCP。",
+ "status": "active",
+ "text": "DNS 的解析过程是怎样的?"
+ },
+ "283fe326d9": {
+ "days": [
+ "java-53"
+ ],
+ "points": "已经运行的服务还能调用,实例列表缓存在本地;新启动的服务默认注册失败直接退出,关闭 fail-fast 能启动,但没有实例列表,调用报 No servers available;所以不会立刻影响线上调用,但无法扩容和发布,生产环境要集群部署。",
+ "status": "active",
+ "text": "注册中心宕机后,服务之间还能调用吗?"
+ },
+ "28fb95c127": {
+ "days": [
+ "java-45"
+ ],
+ "points": "管道:单向,一般用于父子或兄弟这类有共同祖先的进程,FIFO(命名管道)去掉了这个限制;消息队列:独立于读写进程存在,可按类型接收;共享内存:同一物理页映射到多个进程,不用复制,最快,要配合信号量同步;套接字:可用于不同机器之间。",
+ "status": "active",
+ "text": "进程间通信有哪些方式?"
+ },
+ "29246511a6": {
+ "days": [
+ "java-41"
+ ],
+ "points": "对称加密快,但无法安全地把密钥交给对方;非对称加密的公钥可以公开传输(是否属于对方要靠证书验证),但运算慢(RSA-2048 私钥解密不到 0.4 MB/s,AES-128-GCM 加密约 6 GB/s);所以用非对称方式协商或传输会话密钥,再用对称加密传输数据;现在大多用 ECDHE 协商会话密钥,证书的密钥只用于签名,TLS 1.3 已删除 RSA 密钥交换。",
+ "status": "active",
+ "text": "HTTPS 为什么同时使用对称加密和非对称加密?"
+ },
+ "2af89296be": {
+ "days": [
+ "java-02"
+ ],
+ "points": "值传递;传入对象时复制的是对象的地址,方法中修改对象的字段会影响原对象;方法中让形参指向新对象,方法外的引用不变。",
+ "status": "active",
+ "text": "Java 是值传递还是引用传递?"
+ },
+ "2c02de7f11": {
+ "days": [
+ "java-68"
+ ],
+ "points": "工作流的步骤和顺序由代码事先写好,可预测、成本可控,适合步骤固定的业务;Agent 的下一步由模型临时决定,适合事先确定不了步骤的开放任务,代价是成本更高、错误会逐步累积,每步成功率 95% 时连续 10 步都成功只有约 60%;判断标准是步骤和步骤之间的转移规则能否事先写清;实际系统常把两者组合:先用意图路由分流,退款这类涉及资金、步骤固定的流程写成工作流,开放的咨询交给 Agent。",
+ "status": "active",
+ "text": "什么时候用工作流,什么时候用 Agent?"
+ },
+ "2c37283a5c": {
+ "days": [
+ "java-67"
+ ],
+ "points": "按账户 ID 从小到大加锁,两个方向的转账都先锁 id 小的行,就不会互相等待(实验中每 480 笔的死锁从约 70~100 次降为 0),只有涉及同一账户的转账在这一行的锁上排队,不是串行化;余额判断写在条件中:UPDATE account SET balance = balance - 100 WHERE id = 1 AND balance >= 100,影响 0 行说明余额不足、回滚,先读出余额在应用中算好再写回会互相覆盖;客户端生成的请求号作主键,死锁回滚或超时重试",
+ "status": "active",
+ "text": "A 给 B 转账的同时 B 给 A 转账,怎样避免死锁,又不让余额扣成负数?"
+ },
+ "2d35e2a6e9": {
+ "days": [
+ "java-50",
+ "sprint-12"
+ ],
+ "points": "Spring AOP 基于动态代理:容器中保存代理对象,调用时先执行切面逻辑,再调用原始对象的方法;JDK 动态代理要求目标类实现接口,代理类继承 Proxy 并实现同样的接口,只能代理接口方法;CGLIB 生成目标类的子类、重写方法,无法代理 final 类,以及 final、static 和 private 方法;Spring Boot 默认用 CGLIB。",
+ "status": "active",
+ "text": "AOP 的原理是什么?JDK 动态代理和 CGLIB 有什么区别?"
+ },
+ "2de7600ab9": {
+ "days": [
+ "java-07"
+ ],
+ "points": "两个线程同时向同一个空桶插入,后写的覆盖先写的,size++ 也不是原子操作,8 个线程各插 10 万个键 size 远少于 80 万;JDK 1.7 并发扩容时头插法可能让链表成环,get 死循环;JDK 1.8 改为尾插法,但仍会丢数据,转红黑树时还可能破坏树结构;多线程用 ConcurrentHashMap。",
+ "status": "active",
+ "text": "HashMap 为什么线程不安全?"
+ },
+ "2eacd733a9": {
+ "days": [
+ "sprint-10"
+ ],
+ "points": "UDP 无连接、尽最大努力交付、面向报文、没有拥塞控制、首部 8 字节,延迟低,支持一对多;TCP 面向连接、可靠交付、面向字节流、有流量控制和拥塞控制,只能点对点;TCP 靠检验和、序号和累积确认(去重、排序)、超时重传和快重传(3 个重复确认,配合 SACK 只重传缺失的段),以及滑动窗口、流量控制、拥塞控制保证可靠。",
+ "status": "active",
+ "text": "TCP 和 UDP 有什么区别?TCP 怎样保证可靠传输?"
+ },
+ "3011568278": {
+ "days": [
+ "java-20"
+ ],
+ "points": "加载:按全限定名取得字节流,转换为方法区的运行时结构,生成 Class 对象;验证字节流符合规范、不危害虚拟机;准备:为类变量分配内存并设为零值(基本类型或 String、用常量表达式初始化的 static final 常量不经 \\() 直接取 ConstantValue 的值,《深入理解 Java 虚拟机》把这一步归在准备阶段,JVMS SE 21 写在初始化过程中);解析:把常量池中的符号引用替换为直接引用;初始化:执行 \\(),即类变量赋",
+ "status": "active",
+ "text": "类加载的过程是怎样的?"
+ },
+ "302564cde9": {
+ "days": [
+ "java-60"
+ ],
+ "points": "固定窗口:每个窗口一个计数器,最简单,但窗口边界处最多放行 2 倍;滑动窗口:统计往前一个窗口内的请求,精确,但每个请求存一条记录;漏桶:排队后按固定速率处理,最平滑,突发要等待,适合保护处理能力固定的下游;令牌桶:按固定速率放令牌,允许不超过桶容量的突发,Guava RateLimiter 按这种方式实现,它还让当前请求先通过、把欠下的等待算到下一个请求上。",
+ "status": "active",
+ "text": "常用的限流算法有哪些?分别有什么特点?"
+ },
+ "30520434c8": {
+ "days": [
+ "java-51"
+ ],
+ "points": "@SpringBootApplication 包含 @EnableAutoConfiguration;启动时读取各 jar 中 META-INF/spring/...AutoConfiguration.imports(2.7 之前为 spring.factories)列出的自动配置类;大多数自动配置类带 @ConditionalOnClass、@ConditionalOnMissingBean 等条件注解,条件满足才生效;自动配置在用户的 Bean 注册之后处理,用户定义的 ",
+ "status": "active",
+ "text": "Spring Boot 的自动配置是怎样实现的?"
+ },
+ "3362091f47": {
+ "days": [
+ "java-57"
+ ],
+ "points": "处理完才确认,崩溃后会重复投递,所以消费逻辑要幂等:用业务唯一标识(订单号加事件类型,不用消息 ID)作去重键,在同一个本地事务中先写去重表再执行业务,主键冲突就直接确认;有状态的业务用条件更新(WHERE status = 'UNPAID');Kafka 的恰好一次不能代替业务幂等。",
+ "status": "active",
+ "text": "怎样避免重复消费?"
+ },
+ "347baa0bbd": {
+ "days": [
+ "java-18"
+ ],
+ "points": "把堆分成大小相等的 Region,每个 Region 可以单独回收;记录每个 Region 的回收耗时和能回收的空间,维护优先列表,优先回收价值最大的 Region;每次混合回收挑选的老年代 Region 数量受 MaxGCPauseMillis(默认 200 ms)限制;靠调整新生代大小满足目标,所以不要用 -Xmn 固定新生代。",
+ "status": "active",
+ "text": "G1 怎样控制停顿时间?"
+ },
+ "34f42af76f": {
+ "days": [
+ "java-66"
+ ],
+ "points": "TCP 连接由四元组区分,服务端所有连接共用一个监听端口,65535 不是服务端的上限,端口数只限制发起连接的一方(同一个源 IP 到同一个目的地址约 2.8 万个);服务端的瓶颈是文件描述符(调大 ulimit -n)、内存(实测每个空闲连接约 5 KB,10 万个约 500 MB)以及消息量带来的 CPU 和带宽;用 epoll 一个线程就能管理 10 万个连接,再配合应用层心跳及时清理已经断开的连接。",
+ "status": "active",
+ "text": "一台服务器能支撑多少个长连接?瓶颈在哪里?"
+ },
+ "36367df7c4": {
+ "days": [
+ "java-14",
+ "sprint-03"
+ ],
+ "points": "CAS 有内存地址 V、预期值 A、新值 B 三个操作数,V 等于 A 时才更新为 B,由处理器指令(x86 上带 lock 前缀的 cmpxchg)保证原子性,失败时不阻塞线程、只返回 false,由调用方决定重试还是放弃,incrementAndGet() 等方法在内部循环重试;ABA 指值从 A 改为 B 又改回 A,CAS 误以为没变,会让无锁栈这类结构出错;用 AtomicStampedReference 给引用加版本号,每次修改加 1,持有旧版本号的 CAS 会失",
+ "status": "active",
+ "text": "CAS 的原理是什么?ABA 问题怎么解决?"
+ },
+ "380a188f36": {
+ "days": [
+ "java-01"
+ ],
+ "points": "可变性:String 不可变,StringBuffer 和 StringBuilder 可变;线程安全:String 因不可变而线程安全,StringBuffer 的方法用 synchronized 同步,StringBuilder 不加锁,单线程拼接时用它;循环中拼接字符串应在循环外建一个 StringBuilder 调用 append(),用 + 每次都会创建新对象并复制内容。",
+ "status": "active",
+ "text": "String、StringBuffer 和 StringBuilder 有什么区别?"
+ },
+ "3896059e83": {
+ "days": [
+ "java-15"
+ ],
+ "points": "JDK 21 中依次为无锁(Mark Word 末尾 01)、轻量级锁(CAS 换成 Lock Record 指针,末尾 00)、重量级锁(其它线程持有锁时 CAS 失败,膨胀为 ObjectMonitor,末尾 10,先自适应自旋,抢不到再阻塞);偏向锁 JDK 15 起默认禁用、之后被移除;空闲的 ObjectMonitor 会被回收,对象头恢复为无锁,所以“只能升级不能降级”不准确。",
+ "status": "active",
+ "text": "说一下 synchronized 的锁升级过程。"
+ },
+ "393777a7d3": {
+ "days": [
+ "java-07"
+ ],
+ "points": "插入后 size 大于 threshold(capacity × 0.75)时容量翻倍,默认 16 时放入第 13 个键后变为 32;扩容时按 hash & oldCap 是否为 0 把节点分到 j 和 j + oldCap 两个位置,保持相对顺序;容量是 2 的幂时,hash & (capacity - 1) 只保留 hash 的低位,hash 非负时与 hash % capacity 相同(hash 为负时 % 可能得到负数,与运算仍是合法下标),位运算比取模快,扩容也只",
+ "status": "active",
+ "text": "HashMap 是怎样扩容的?为什么容量是 2 的幂?"
+ },
+ "394e2ef8d2": {
+ "days": [
+ "java-28",
+ "sprint-07"
+ ],
+ "points": "跨分片的事务要用分布式事务,跨分片的连接要拆成单表查询在应用中连接;不能再用自增主键,改用雪花算法等分布式 ID;不带分片键的查询要查所有分片,可以用基因法、映射表或按另一个维度再存一份,跨分片的深分页改用游标分页;增加分片要迁移数据,常用双写迁移。",
+ "status": "active",
+ "text": "分库分表之后会遇到哪些问题?"
+ },
+ "3b0c37d0ee": {
+ "days": [
+ "java-39"
+ ],
+ "points": "Cookie 是保存在浏览器、每次请求自动携带的小块数据(约 4 KB);Session 把状态存在服务器(如 Redis),Cookie 只带 Session ID,删除 Session 就能让用户下线,集群要共享;Token(如 JWT)把状态放在带签名的令牌中,服务无需保存会话就能验证,但过期前无法主动失效,常用短期访问令牌加刷新令牌,需要立即失效时把令牌 ID 放入 Redis 黑名单,这又回到了在服务器保存状态。",
+ "status": "active",
+ "text": "Cookie、Session 和 Token 有什么区别?"
+ },
+ "3cde3d237b": {
+ "days": [
+ "java-06"
+ ],
+ "points": "线程安全的 List 有 Vector、Collections.synchronizedList() 和 CopyOnWriteArrayList;CopyOnWriteArrayList 写操作加锁并在复制的新数组上进行,写完再替换原数组,读不加锁;适合读多写少的场景,缺点是写时内存约翻倍,写入期间读到的可能是旧数组,迭代器也看不到创建后的修改。",
+ "status": "active",
+ "text": "线程安全的 List 有哪些?CopyOnWriteArrayList 适合什么场景?"
+ },
+ "3d4ae89337": {
+ "days": [
+ "llm-06"
+ ],
+ "points": "编造参数,例如用户没给订单号就自己编一个去查;编造或误读工具结果,例如接口超时仍说“正在运输中”、把 updated_at 当成送达时间;声称做了没做的事,例如没调用退款工具却说已退款;应对要靠工具和代码:结果字段写清楚、校验参数来源、用代码核对关键信息。",
+ "status": "active",
+ "text": "Agent 调用工具时会出现哪些幻觉?"
+ },
+ "3eda6c30d3": {
+ "days": [
+ "java-55"
+ ],
+ "points": "TCC 由业务实现 Try(预留资源,如冻结库存)、Confirm、Cancel,不长期持有数据库锁,适合一致性要求较高、能改造出三个接口的资源操作,如库存、资金;本地消息表把消息和业务数据写在同一个本地事务中,再投递给对方幂等执行,失败只能重试,适合能接受最终一致的后续操作,如下单后发券、加积分。",
+ "status": "active",
+ "text": "TCC 和本地消息表分别适合什么场景?"
+ },
+ "3f287e23ea": {
+ "days": [
+ "java-49"
+ ],
+ "points": "实例化(反射调用构造函数)→ 属性填充 → Aware 回调 → BeanPostProcessor 的 before 方法 → 初始化(@PostConstruct、afterPropertiesSet()、init-method)→ BeanPostProcessor 的 after 方法,AOP 在这里替换为代理;容器关闭时依次调用 @PreDestroy、destroy()、destroy-method。",
+ "status": "active",
+ "text": "Bean 的生命周期是怎样的?"
+ },
+ "4383694e3e": {
+ "days": [
+ "java-14"
+ ],
+ "points": "多个线程不管以何种方式访问一个类,调用方不需要额外同步都能表现正确;根源是多个线程读写共享的可变数据;做法:不可变对象,互斥同步(synchronized、ReentrantLock),非阻塞同步(CAS、原子类),无同步方案(栈封闭、ThreadLocal)。",
+ "status": "active",
+ "text": "什么是线程安全?怎样保证线程安全?"
+ },
+ "4391e4bcee": {
+ "days": [
+ "java-25"
+ ],
+ "points": "数据在磁盘上,要让读磁盘的次数最少;红黑树是二叉树,200 万行约 21 层,B+ 树的一个节点是 16 KB 的页,有几百上千个子节点,200 万行只有 3 层;B 树每个节点都存数据,B+ 树只有叶子存数据,内部节点能放下更多 key,叶子用指针相连,便于范围查询,每次查找都走到叶子,I/O 次数稳定;哈希索引只支持等值查找,不能用于排序和范围查询。",
+ "status": "active",
+ "text": "MySQL 为什么用 B+ 树做索引,而不用 B 树、红黑树或哈希表?"
+ },
+ "4489ec0668": {
+ "days": [
+ "java-32"
+ ],
+ "points": "Redis 用一个线程执行命令,执行脚本期间不处理其它客户端的命令,整个脚本就像一条命令,可以根据前一条命令的结果决定后续操作,例如先 GET 再判断是否 INCR;这里的原子性是不被打断,不是出错回滚,中途出错时已执行的写命令不会回滚。",
+ "status": "active",
+ "text": "Lua 脚本为什么能保证原子性?"
+ },
+ "454c7daf16": {
+ "days": [
+ "java-13"
+ ],
+ "points": "volatile 保证可见性(写入对之后读取的线程立即可见)和有序性(volatile 写之前的读写不会被重排序到写之后,volatile 读之后的读写不会被重排序到读之前);单次读写是原子的,long、double 也不会被拆成两次,但不保证 i++ 这类复合操作的原子性:i++ 是读取、加 1、写回三步,两个线程可能读到同一个旧值,各自加 1 后写回,只增加了 1;读-改-写操作要用锁或原子类。",
+ "status": "active",
+ "text": "volatile 能保证什么?为什么 volatile 修饰的 i++ 仍然不安全?"
+ },
+ "45bf4a5b4d": {
+ "days": [
+ "llm-09"
+ ],
+ "points": "按标题、段落等结构切,表格和代码块保持完整,相邻切片保留重叠,切片带上标题路径;一节超过上限时递归切分,按空行、换行、句号、逗号由粗到细尝试,LangChain 默认的分隔符没有中文标点,要补上;没有标题的长文本可以语义切分,在相邻两句相似度明显下降处切开;也可以用小切片检索、返回所在的完整段落(父子切片);大小没有通用最优值,以 Embedding 模型的最大长度为上限,事实类问题用小切片,最后在评测集上比较召回率。",
+ "status": "active",
+ "text": "有哪些切片策略?切片大小怎么确定?"
+ },
+ "47712a7a5f": {
+ "days": [
+ "java-23"
+ ],
+ "points": "快照读是普通 SELECT,读 MVCC 快照,不加锁;当前读是 INSERT、UPDATE、DELETE 和 SELECT ... FOR SHARE / FOR UPDATE,读最新版本并加锁;可重复读下同一事务混用时,快照读看不到快照建立后其它事务提交的新行,当前读却能看到并修改它们,修改后本事务的快照读也能看到这些行。",
+ "status": "active",
+ "text": "快照读和当前读有什么区别?"
+ },
+ "479c864f58": {
+ "days": [
+ "llm-02"
+ ],
+ "points": "标准注意力的计算量为 O(n2 d),n × n 的分数矩阵占 O(n2 ) 显存,长上下文时显存读写成为瓶颈;FlashAttention 分块在 GPU 片上缓存中计算,分数矩阵不写回显存,用在线 softmax 逐块累积;结果在数学上与标准注意力相同,不是近似,省的是显存读写而不是计算量。",
+ "status": "active",
+ "text": "FlashAttention 解决了什么问题?"
+ },
+ "48101acfd2": {
+ "days": [
+ "java-23"
+ ],
+ "points": "未提交读三种问题都可能出现;提交读避免脏读;可重复读再避免不可重复读,InnoDB 用 MVCC 和 Next-Key Lock 在很大程度上也避免了幻读,是 MySQL 的默认级别;可串行化三种都能避免,InnoDB 在显式事务中把普通 SELECT 变成 FOR SHARE,读写互相阻塞,并发度最低。",
+ "status": "active",
+ "text": "四种隔离级别分别解决了哪些问题?"
+ },
+ "485504eb0f": {
+ "days": [
+ "sprint-14"
+ ],
+ "points": "Function Calling 是模型侧的能力,模型根据工具描述给出函数名和参数;MCP 是应用与外部工具、数据之间的协议,统一工具的发现(tools/list)和调用(tools/call),工具实现一次服务端就能被所有支持 MCP 的应用使用;两者配合:主机把 MCP 工具转成工具定义交给模型,再通过 MCP 执行模型给出的调用。",
+ "status": "active",
+ "text": "MCP 和 Function Calling 有什么区别?"
+ },
+ "488c66ff9c": {
+ "days": [
+ "java-62"
+ ],
+ "points": "按 hash(url) % 100 切成 100 个约 1 GB 的小文件,相同的 URL 一定在同一个文件中;逐个文件用 HashMap 统计次数,用小顶堆取出前 10 个;再用小顶堆从 100 × 10 个候选中选出全局前 10 个;某个文件中不同的 URL 仍然太多、HashMap 放不下时,换哈希函数对它再切分。",
+ "status": "active",
+ "text": "100 GB 的日志中每行是一个 URL,内存只有 4 GB,怎样找出出现次数最多的 10 个?"
+ },
+ "493fe133cc": {
+ "days": [
+ "llm-03"
+ ],
+ "points": "MHA 中每个查询头都有自己的 K、V;GQA 让多个查询头共享一组 K、V,Qwen2.5-7B 的 28 个查询头共享 4 组,每个 Token 缓存 56 KB,MHA 要 392 KB;MQA 中所有查询头共享一组,缓存最小,但效果下降更明显;GQA 减少的是缓存和解码每步读取的数据量,计算量基本不变,每个查询头仍有自己的 Q,所以效果接近 MHA。",
+ "status": "active",
+ "text": "MHA、MQA 和 GQA 有什么区别?"
+ },
+ "4978768999": {
+ "days": [
+ "java-34"
+ ],
+ "points": "先查本地缓存(Caffeine),未命中查 Redis,再未命中查数据库并逐级回填;本地读取约 21 纳秒、Redis 约 68 微秒,热点放本地还能分散单个 Redis 节点的压力;更新后删 Redis,并广播消息删除各实例的本地缓存,本地再设约 30 秒过期兜底;只放读多写少、能接受短暂不一致的数据。",
+ "status": "active",
+ "text": "本地缓存和 Redis 怎样配合使用?"
+ },
+ "4baf13900d": {
+ "days": [
+ "java-61"
+ ],
+ "points": "让“判断库存”和“扣减库存”成为一个整体;悲观锁 FOR UPDATE 正确,但锁持有到事务提交,其它请求排队;乐观锁同时读到同一版本的请求只有一个成功,有库存也会失败(150 人抢 100 件,重试 3 次也只卖出 48~57 件),适合冲突少的场景;条件更新 WHERE stock > 0 不需重试,但同一行更新仍串行,所以先用 Redis 挡住大部分请求,条件更新作为最后防线。",
+ "status": "active",
+ "text": "秒杀系统怎样防止超卖?悲观锁、乐观锁和条件更新各有什么问题?"
+ },
+ "4bf4ba1fb0": {
+ "days": [
+ "llm-16"
+ ],
+ "points": "完整历史在上下文中时模型一般能理解指代,问题出在检索、压缩和多个候选;检索前结合最近几轮和任务状态改写成完整的问题,例如“它什么时候发货”改为“订单 1026 什么时候发货”;任务状态记录当前和提到过的订单,有多个候选时追问而不是猜。",
+ "status": "active",
+ "text": "多轮对话中的指代和省略怎样处理?"
+ },
+ "4c736dfa3f": {
+ "days": [
+ "java-54"
+ ],
+ "points": "用 SET lock_key unique_value NX PX 30000 加锁,键不存在才成功,并同时设置过期时间;释放时用 Lua 脚本先比较 value 再删除,避免误删别人的锁;分成 SETNX 和 EXPIRE 两条命令时,执行完 SETNX 后进程崩溃,锁就永远不会过期。",
+ "status": "active",
+ "text": "用 Redis 怎样实现分布式锁?为什么加锁和设置过期时间要用一条命令?"
+ },
+ "4e79ae834e": {
+ "days": [
+ "java-40"
+ ],
+ "points": "HTTP 是请求-响应模式,服务器不能主动发消息;WebSocket 由一次 HTTP 升级(101)而来,之后在同一个 TCP 连接上全双工传输帧;SSE 是 text/event-stream 的普通 HTTP 响应,只能服务器推送,浏览器的 EventSource 断线后自动重连(它只能发 GET,用 fetch 读取响应流时要自己重连);只需服务器推送(如大模型逐字输出)用 SSE,双方都要随时发消息(聊天)用 WebSocket。",
+ "status": "active",
+ "text": "WebSocket 和 HTTP 有什么区别?什么时候用 SSE?"
+ },
+ "4ec5716b53": {
+ "days": [
+ "llm-18"
+ ],
+ "points": "第一次会话只做准备:把需求拆成带测试步骤和 passes 字段的 JSON 功能清单,写好启动脚本并提交到 git;之后每次会话先读进度文件和 git log、做冒烟测试,只实现一个未通过的功能,端到端测试通过才把 passes 改为 true,再提交并更新进度;只允许修改 passes,防止 Agent 删掉测试来“完成”任务。",
+ "status": "active",
+ "text": "长任务怎样在多次会话之间交接?"
+ },
+ "4f74d11814": {
+ "days": [
+ "sprint-08"
+ ],
+ "points": "STRING、LIST、SET、HASH、ZSET 五种,另有 BITMAP、HyperLogLog、GEO、Stream;ZSET 成员多时用跳表,在有序链表上加多层索引,以 1/4 的概率升层,查找和插入删除期望 O(logN);相比平衡树,插入删除只改前驱指针、不需要旋转,范围查询沿底层链表遍历,实现简单;数据都在内存中,不需要 B+ 树那样减少磁盘 I/O。",
+ "status": "active",
+ "text": "Redis 有哪些数据类型?ZSET 的底层为什么用跳表?"
+ },
+ "4fad9621d5": {
+ "days": [
+ "sprint-13"
+ ],
+ "points": "超卖源于判断库存和扣减库存不是原子操作;数据库中可用悲观锁 SELECT ... FOR UPDATE、乐观锁版本号或条件更新 UPDATE ... SET stock = stock - 1 WHERE stock > 0;实测 1000 个用户抢 100 件,先查再扣多卖了 34~62 件,悲观锁和条件更新都恰好卖出 100 件;乐观锁不会超卖,但冲突多时会少卖,150 个用户抢、重试 3 次也只卖出 48~57 件;条件更新不需要重试、最常用;秒杀时先用 Redis 的",
+ "status": "active",
+ "text": "秒杀系统怎样防止超卖?"
+ },
+ "4ff0d2d0d0": {
+ "days": [
+ "llm-09"
+ ],
+ "points": "PDF 只记录“在某坐标画某字”,没有段落和表格,直接读文本层会混入页眉页脚、句子在折行处断开;扫描件要先 OCR,表格可能被逐列输出;用版面分析识别标题、正文、表格和阅读顺序,表格转为 Markdown,按行切分时每行带上表头;跨页的表格在列数、列宽一致且中间没有新标题时拼成一张,去掉重复的表头。",
+ "status": "active",
+ "text": "PDF 为什么难解析?表格怎么处理?"
+ },
+ "513b0767e9": {
+ "days": [
+ "llm-14"
+ ],
+ "points": "模型负责理解任务、决定下一步;工具用于获取信息和执行操作,记忆保存对话历史和用户偏好,规划把复杂任务拆成步骤;控制循环是应用代码中反复调用模型、执行工具的循环,并负责限制步数、校验参数和权限;模型只输出文本,执行都由应用完成。",
+ "status": "active",
+ "text": "Agent 由哪些部分组成?"
+ },
+ "518704a7fc": {
+ "days": [
+ "java-04"
+ ],
+ "points": "设计上抽象类是 IS-A 关系,接口是 LIKE-A 关系,只提供方法契约;一个类可以实现多个接口,只能继承一个抽象类;接口的字段只能是 static final,成员默认 public,抽象类没有这些限制;相关类共享代码、需要非 public 成员或非常量字段时用抽象类,让不相关的类具备同一行为(例如 Comparable)时用接口,多数情况接口优先。",
+ "status": "active",
+ "text": "抽象类和接口有什么区别?分别用在什么场景?"
+ },
+ "52aa24db2a": {
+ "days": [
+ "llm-21"
+ ],
+ "points": "非流式请求的总超时要大于 TTFT + 最大输出 Token 数 × TPOT,例如 1000 个 Token、30 毫秒时要 30 多秒;流式请求分别设首 Token 超时、间隔超时和总时长上限,心跳不算内容;429、5xx、超时可以重试,400、401 直接失败,按指数退避加随机抖动、遵守 Retry-After,次数设上限且只在一层重试。",
+ "status": "active",
+ "text": "调用模型接口时怎样设置超时和重试?"
+ },
+ "52c9969ca3": {
+ "days": [
+ "java-24"
+ ],
+ "points": "常见原因:两个事务按相反的顺序更新同一组行,或者“先查有没有,没有再插入”时拿到了同一个间隙锁,插入意向锁与对方的间隙锁冲突;InnoDB 检测到死锁后回滚其中一个事务,应用要重试;排查:看 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK,打开 innodb_print_all_deadlocks 记录每次死锁,锁等待时查 performance_schema.data_locks 和 data_lock_waits;",
+ "status": "active",
+ "text": "遇到过 MySQL 死锁吗?怎样排查?"
+ },
+ "535132ab86": {
+ "days": [
+ "java-30"
+ ],
+ "points": "用位图:每天一个键 active:{日期},offset 为用户的连续序号,签到时 SETBIT 为 1,1 亿用户每天 12.5 MB、一年约 4.6 GB;BITCOUNT 统计当天的人数,BITOP 求多天的交集和并集;也可以每人每月一个键 sign:{用户 ID}:{年月},便于查一个人的签到,但大量小键的开销大,每人每年一个键时 1 亿用户约 15 GB;用户 ID 稀疏时先转换为连续的序号。",
+ "status": "active",
+ "text": "一亿用户每天的签到状态怎样存储?"
+ },
+ "54392ab875": {
+ "days": [
+ "java-07"
+ ],
+ "points": "用 hash() 计算哈希值,table 为空时先 resize() 创建数组;用 (n - 1) & hash 求桶下标,桶为空直接放入;头节点是 TreeNode 就在红黑树中插入,否则遍历链表,键相同则替换旧值,否则接到链表尾部,已有 8 个节点时调用 treeifyBin();size 大于 threshold 时扩容。",
+ "status": "active",
+ "text": "HashMap 的 put() 过程是怎样的?"
+ },
+ "56455baece": {
+ "days": [
+ "java-26"
+ ],
+ "points": "联合索引先按第一列排序,第一列相同再按第二列,所以条件要从最左列开始连续使用才能用索引定位,与 WHERE 中的书写顺序无关;(a, b, c) 只用到 a 和 c 时能走索引,但只用 a 定位,c 在定位到的范围内逐条判断,借助索引下推可以在索引上过滤,减少回表。",
+ "status": "active",
+ "text": "什么是最左前缀原则?联合索引 (a, b, c) 只用到 a 和 c 时能走索引吗?"
+ },
+ "56d0a4ee0e": {
+ "days": [
+ "sprint-01"
+ ],
+ "points": "JDK 1.8:table 为空先 resize(),用 (n - 1) & hash 定位桶,空桶直接放入;否则头节点是 TreeNode 就在红黑树中查找或插入,是链表就逐个比较,键相同就替换旧值,找不到就接在链表尾部;插入后 size 大于 threshold(容量 × 0.75,默认 12)时扩容为 2 倍;接上前链表已有 8 个节点时,数组长度小于 64 只扩容,否则转成红黑树。",
+ "status": "active",
+ "text": "HashMap 的 put() 过程是怎样的?什么时候扩容、什么时候转成红黑树?"
+ },
+ "57e6bf391b": {
+ "days": [
+ "java-56"
+ ],
+ "points": "64 位依次为 1 位符号位、41 位毫秒时间戳、10 位机器 ID、12 位序号,同一毫秒内序号递增,4096 个用完就等到下一毫秒;时钟回拨几毫秒时等时钟追上(Leaf 在 5 ms 以内等待两倍的差值),较大时报错并摘除实例;lastTime 只在内存中,重启后发现不了回拨,要把时间持久化,例如 Leaf 每 3 秒把本机时间写入 ZooKeeper,启动时本机时间比记录的还早就不启动;机器 ID 在启动时从 ZooKeeper、数据库等领取。",
+ "status": "active",
+ "text": "雪花算法是怎样生成 ID 的?时钟回拨怎么办?"
+ },
+ "57ed7e67f6": {
+ "days": [
+ "java-34"
+ ],
+ "points": "用 k 个哈希函数把 Key 映射到位数组的 k 个位置并置 1,查询时有一位为 0 就一定不存在,全为 1 则可能存在;误判是因为这些位可能都被其它 Key 置为 1;每个 Key 占 10 位、k = 7 时误判率约 0.82%,插入数超过预期后迅速上升,且不能删除元素。",
+ "status": "active",
+ "text": "布隆过滤器的原理是什么?为什么会误判?"
+ },
+ "59a27f0b68": {
+ "days": [
+ "java-24",
+ "sprint-06"
+ ],
+ "points": "快照读靠 MVCC:可重复读复用同一个 ReadView,看不到其它事务新插入的行;当前读靠 Next-Key Lock:锁住扫描到的记录和记录之间的间隙,其它事务无法在范围内插入;同一事务中快照读和当前读混用时仍会出现幻读。",
+ "status": "active",
+ "text": "InnoDB 在可重复读级别下怎样避免幻读?"
+ },
+ "5b6919dab0": {
+ "days": [
+ "llm-16"
+ ],
+ "points": "写入前检索相似的旧记忆,由模型选择 ADD、UPDATE、DELETE 或 NOOP(Mem0 论文的做法,开源版后来改为只新增),用户 ID、时间等字段由代码填写;冲突时用户本次说的优先,亲口说的优先于推断,同一来源以新的为准,只对这一次有效的说法不覆盖,仍无法判断就问用户;旧记忆标记失效而不物理删除,并按时间衰减排序、定期清理。",
+ "status": "active",
+ "text": "长期记忆中的信息过时或冲突时怎样处理?"
+ },
+ "5cfaefbdbb": {
+ "days": [
+ "java-45"
+ ],
+ "points": "内核运行在特权级 0(内核态),应用运行在 3 级(用户态),特权指令只能在内核态执行,用户态也只能访问本进程的用户空间,以保护内核和其它进程;系统调用(如读写文件、创建进程)、异常(如缺页)和外中断会从用户态进入内核态;一次系统调用约 165 ns,是普通函数调用的近 180 倍,但不一定发生上下文切换。",
+ "status": "active",
+ "text": "什么是用户态和内核态?什么时候会切换?"
+ },
+ "5efd34dc3a": {
+ "days": [
+ "java-12"
+ ],
+ "points": "Future 只能用 get() 阻塞等待结果,多个任务有依赖时要自己写等待逻辑;CompletableFuture 可以注册回调,用 thenApply、thenCompose 串联,用 thenCombine 合并,用 exceptionally、handle 处理异常;带 Async 的方法不传线程池时在 ForkJoinPool.commonPool() 中执行(并行度小于 2 时每个任务新建一个线程),阻塞的 I/O 任务应传入自己的线程池。",
+ "status": "active",
+ "text": "CompletableFuture 解决了 Future 的什么问题?"
+ },
+ "5f403638bf": {
+ "days": [
+ "java-65"
+ ],
+ "points": "Service 工作在传输层,用标签选出一组 Pod,提供固定的虚拟 IP 和域名,由 kube-proxy 写成 iptables 或 IPVS 规则转发,有 ClusterIP、NodePort、LoadBalancer 等类型;Ingress 是 HTTP 路由规则,按域名和路径把请求分给不同的 Service,由 Ingress 控制器执行,多个 HTTP 服务共用一个入口。",
+ "status": "active",
+ "text": "Service 和 Ingress 有什么区别?"
+ },
+ "6159fb310f": {
+ "days": [
+ "java-20"
+ ],
+ "points": "重写 loadClass(),先自己加载、找不到再交给父加载器;例子:JDBC 的 DriverManager 由平台类加载器加载,通过 ServiceLoader 用线程上下文类加载器加载 classpath 上的驱动;Tomcat 每个应用的 WebappClassLoader 先在本应用中查找,不同应用可以用同一个库的不同版本;热部署用新的类加载器加载修改后的类。",
+ "status": "active",
+ "text": "怎样打破双亲委派?有哪些实际的例子?"
+ },
+ "61d62633f1": {
+ "days": [
+ "java-05"
+ ],
+ "points": "泛型只在编译时检查类型,编译后类型参数替换为上界(没有上界时为 Object),取出元素处由编译器插入强制类型转换;运行时 List\\ 和 List\\ 是同一个类,所以类型参数不能是基本类型,不能 new T() 或用 instanceof List\\;类、字段、方法声明上的泛型信息保存在 Signature 属性中,可以通过反射读取。",
+ "status": "active",
+ "text": "什么是泛型擦除?"
+ },
+ "6236dc8524": {
+ "days": [
+ "java-42"
+ ],
+ "points": "语义:GET 获取资源,POST 传输实体主体;GET 安全且幂等,POST 会改变服务器状态,不幂等;参数放在哪里只是惯例,GET 参数通常在 URL 中(要编码,长度受浏览器和服务器的限制,例如 Nginx 默认请求行不超过 8 KB),POST 在主体中,但抓包同样能看到,并不更安全;GET 响应可缓存,POST 多数不可缓存。",
+ "status": "active",
+ "text": "GET 和 POST 有什么区别?"
+ },
+ "62d04c530a": {
+ "days": [
+ "java-47"
+ ],
+ "points": "虚拟内存让每个进程看到独立、连续的地址空间,程序使用虚拟地址,由硬件和操作系统按页翻译成物理地址;它让程序可以比物理内存大(只把用到的页放在内存中),隔离各进程并按页设置权限,简化加载(物理内存不必连续),还能让多个进程共享同一物理页。",
+ "status": "active",
+ "text": "什么是虚拟内存?为什么需要它?"
+ },
+ "634ab6d850": {
+ "days": [
+ "llm-18"
+ ],
+ "points": "子任务互相独立、需要读大量资料或反复试错时值得拆,需要隔离权限时也可以拆,好处是隔离上下文,子 Agent 只返回精简的结论;代价是 Token 消耗成倍增加(Anthropic 的数据中多 Agent 系统约为普通对话的 15 倍),只传摘要会丢失细节;依赖紧密的任务和对延迟、成本敏感的在线请求用单个 Agent;协作方式有主管分派(子 Agent 包装成工具、可并行)、流水线(固定顺序)、讨论(互看答案后投票或裁决)和转交(把控制权和上下文交给另一个 Agent,由它直接",
+ "status": "active",
+ "text": "什么时候需要多个 Agent?有哪些协作方式?"
+ },
+ "63f07a4386": {
+ "days": [
+ "llm-08"
+ ],
+ "points": "提示词工程关注怎样写好指令,尤其是系统提示词;上下文工程决定每次调用前把哪些内容、按什么顺序放进窗口,包括规则、工具定义、检索资料、对话历史和记忆;Agent 每一步都有新的工具结果,每次调用前都要重新挑选和压缩。",
+ "status": "active",
+ "text": "上下文工程和提示词工程有什么区别?"
+ },
+ "64f1daf686": {
+ "days": [
+ "java-46"
+ ],
+ "points": "互斥:资源要么已分配给一个进程,要么可用;占有和等待:已持有资源的进程还能请求新资源;不可抢占:资源只能由持有者显式释放;环路等待:多个进程形成环,每个都在等下一个持有的资源。",
+ "status": "active",
+ "text": "死锁的四个必要条件是什么?"
+ },
+ "676b456ee1": {
+ "days": [
+ "llm-17"
+ ],
+ "points": "Tool 是可调用的函数,提供查询订单这类原子能力;MCP 是连接工具和数据的协议,解决工具怎么接入;Skill 是包含 SKILL.md、脚本和资料的文件夹,提供完成某类任务的步骤和规范,执行时仍要调用工具,平时只加载名称和描述(约 100 个 Token),需要时才读取正文。",
+ "status": "active",
+ "text": "Skill 和 MCP、Tool 有什么区别?"
+ },
+ "68acba4c85": {
+ "days": [
+ "java-22"
+ ],
+ "points": "原子性:要么全部成功,要么全部回滚,InnoDB 用 undo log 记录修改前的值来回滚;一致性:事务前后满足约束和业务规则,由另外三者和主键、唯一索引等约束共同保证,业务规则还要应用自己维护;隔离性:未提交的修改对其它事务不可见,写操作加锁,普通读用 MVCC 读快照;持久性:提交后不丢失,先写 redo log,提交时刷盘,崩溃后重放。",
+ "status": "active",
+ "text": "ACID 分别是什么?InnoDB 怎样保证它们?"
+ },
+ "699cec5d70": {
+ "days": [
+ "java-38"
+ ],
+ "points": "301 永久重定向,没有缓存指令时默认可被浏览器缓存,适合域名迁移;302 临时重定向,默认不缓存;304 表示条件请求(If-None-Match、If-Modified-Since)的资源没有变化,不带响应体;502 是网关从上游收到无效响应(后端出错或不可达),504 是网关等待上游超时(后端太慢)。",
+ "status": "active",
+ "text": "301、302、304、502、504 分别表示什么?"
+ },
+ "6d00dbcd74": {
+ "days": [
+ "llm-12"
+ ],
+ "points": "向量相似度的绝对值很难定阈值:实测阈值 0.50 时无答案的问题放行 40%,0.60 时有答案的问题误拒 10%;所以只用较低的阈值过滤明显无关的结果,有没有答案靠重排分数和“资料不足时回答不知道”的提示词判断;阈值在评测集上确定,换模型后重新确定。",
+ "status": "active",
+ "text": "相似度阈值怎么设置?"
+ },
+ "6d49cb17be": {
+ "days": [
+ "java-63"
+ ],
+ "points": "先把日流量换算成高峰时的 QPS:日 PV 6 亿平均约 6944 QPS,高峰一小时占 6% 时这一小时平均约 1 万 QPS,大促按 10 倍设计为 10 万,小时内的秒级突发还要留余量;机器数 = 峰值 QPS ÷(单实例拐点 QPS × 目标利用率),再按可用区取整,一个可用区故障时其余实例仍要低于拐点;存储按每天的增量乘以保存时间估算,例如日下单 1200 万、一年约 44 亿行,分库分表主要由数据量决定。",
+ "status": "active",
+ "text": "设计一个系统前,怎样估算需要的机器和存储?"
+ },
+ "6dbf8f2ba2": {
+ "days": [
+ "java-41"
+ ],
+ "points": "CA 用私钥对证书内容(域名、公钥、有效期等)的哈希值签名,客户端用上一级证书的公钥验证,内容改一个字节就会失败;沿证书链逐级验证,顶端必须是预装在系统或浏览器中的根证书,根证书自签名,是信任的起点;还要检查有效期、吊销状态和域名是否在证书的 Subject Alternative Name 中。",
+ "status": "active",
+ "text": "证书是怎样验证的?根证书有什么用?"
+ },
+ "6ee29f4ce5": {
+ "days": [
+ "java-35"
+ ],
+ "points": "大 Key 是值很大的键(几 MB 的字符串、几十万元素的集合),会阻塞单线程、占带宽、造成数据倾斜,用 --bigkeys 发现,拆成小键,删除用 UNLINK、分批用 HSCAN 读;热 Key 是访问量远高于其它的键,请求集中到一个节点,淘汰策略为 LFU 时用 --hotkeys 发现,否则在客户端或代理中统计,用本地缓存或复制成多个键分散读取。",
+ "status": "active",
+ "text": "什么是大 Key 和热 Key?怎样处理?"
+ },
+ "7032e684b1": {
+ "days": [
+ "java-61"
+ ],
+ "points": "以数据库的库存和订单为准,Redis 库存只是闸门,允许暂时少卖,数据库条件更新始终保留;发送消息明确失败时执行反向脚本回补,发送超时(消息可能已送达)或应用宕机交给对账,也可以用事务消息;订单因业务原因失败时回补 Redis,异常失败靠消息重试,唯一索引防止重复建单;回补先 SREM 已购集合、返回 1 才加库存,允许再次抢购时还要核对请求 ID;Redis 主从切换丢失扣减时由数据库兜底,因数据库库存不足失败的请求,确认这个用户没有订单后只移出已购集合、不加回库存;活动后",
+ "status": "active",
+ "text": "Redis 预扣库存成功、订单却创建失败,怎样保证两边一致?"
+ },
+ "7040328bec": {
+ "days": [
+ "java-54"
+ ],
+ "points": "过期时间取正常耗时的几倍,难以估计时由后台线程续期,例如 Redisson 看门狗:lock() 不指定过期时间时先设 30 秒,每 10 秒检查一次,锁仍是自己的就重置为 30 秒,进程崩溃后最多 30 秒释放;锁内操作都要设超时,否则卡住的线程会被一直续期;GC 暂停时仍可能两个客户端同时持锁,要用递增的栅栏令牌拒绝旧写入。",
+ "status": "active",
+ "text": "锁过期了业务还没执行完怎么办?"
+ },
+ "710b3551d1": {
+ "days": [
+ "java-40"
+ ],
+ "points": "同源指协议、域名、端口都相同,浏览器禁止脚本读取其它源的响应(简单请求仍会发出并在服务器上执行);常用 CORS:服务器返回 Access-Control-Allow-Origin,非简单请求先发 OPTIONS 预检,带 Cookie 时不能用 *;也可以用同源的反向代理转发;JSONP 只支持 GET,已被 CORS 取代。",
+ "status": "active",
+ "text": "什么是跨域?怎样解决?"
+ },
+ "71ab53cab0": {
+ "days": [
+ "java-17"
+ ],
+ "points": "可达性分析要遍历对象之间的引用,用户线程同时修改引用可能漏掉存活的对象;复制和整理算法还要移动对象、修改指向它的引用;用户线程运行到安全点才暂停,CMS、G1 把耗时最长的标记改为并发执行来缩短停顿。",
+ "status": "active",
+ "text": "垃圾收集为什么要 Stop The World?"
+ },
+ "72afd7353e": {
+ "days": [
+ "java-39"
+ ],
+ "points": "长连接指一个 TCP 连接上进行多次 HTTP 通信,省去每次建连的开销;HTTP/1.1 默认长连接,要断开时发送 Connection: close,之前的版本要用 Connection: Keep-Alive;服务器在连接空闲一段时间后关闭它(Nginx 默认 75 秒),这和 TCP 的保活探测不是一回事;同一连接上的请求按顺序收发,流水线因队头阻塞未被浏览器启用,HTTP/2 用多路复用解决。",
+ "status": "active",
+ "text": "什么是长连接?HTTP/1.1 怎样复用连接?"
+ },
+ "72f9842f42": {
+ "days": [
+ "llm-18"
+ ],
+ "points": "Harness 指模型之外的所有代码、配置和执行逻辑;组成包括 Agent 循环与停止条件、上下文组装与压缩、工具与权限、记忆与状态交接、子 Agent、钩子与校验、评测与可观测,MCP 和 Skill 也属于它;核心是动手前的引导(规范、文档、Skill)和动手后的反馈(编译、测试、lint、模型审查)。",
+ "status": "active",
+ "text": "什么是 Agent Harness?由哪些部分组成?"
+ },
+ "731153a537": {
+ "days": [
+ "java-04"
+ ],
+ "points": "运行时多态需要继承或实现接口、重写和向上转型;通过类类型的引用调用编译成 invokevirtual 加符号引用,通过接口类型的引用调用用 invokeinterface,运行时按对象的实际类型选择方法,称为动态分派;HotSpot 为每个类维护虚方法表(vtable),按固定槽位取方法入口,接口调用用 itable;重载在编译时按静态类型确定,称为静态分派。",
+ "status": "active",
+ "text": "多态是怎么实现的?"
+ },
+ "747ba7232d": {
+ "days": [
+ "sprint-13"
+ ],
+ "points": "不丢:发送端收到确认才算成功(如 Kafka acks=all 加重试),可用与业务同事务的本地消息表;存储端持久化加多副本,消费端处理完再提交偏移量或 ack;不重复:处理完才确认意味着可能重复投递,消费要幂等,用订单号加事件类型这类业务键写去重表并与业务同事务,或用状态做条件更新;消息队列的恰好一次代替不了业务幂等。",
+ "status": "active",
+ "text": "怎样保证消息不丢失、不重复消费?"
+ },
+ "752067dae4": {
+ "days": [
+ "java-47"
+ ],
+ "points": "OPT 换出最长时间不再访问的页,缺页最少但无法实现,只作对照;LRU 换出最近最久未使用的页,维护链表代价高;FIFO 换出最先进入的页,可能换出常用页,第二次机会和时钟算法用访问位 R 改进它,NRU 按 R、M 位分类换出;同一序列 3 个物理块时 OPT、LRU、FIFO 分别缺页 9、12、15 次。",
+ "status": "active",
+ "text": "有哪些页面置换算法?"
+ },
+ "75d8147fc6": {
+ "days": [
+ "java-21",
+ "sprint-04"
+ ],
+ "points": "先看错误信息判断是哪块内存:Java heap space、GC overhead limit exceeded、Metaspace、direct buffer memory、unable to create native thread;元空间、直接内存、线程创建失败分别检查类的生成与卸载、堆外内存的释放、线程数和容器的限制,不能靠增大堆解决;Java heap space 时,启动时加 -XX:+HeapDumpOnOutOfMemoryError 保存堆转储,或用 jcm",
+ "status": "active",
+ "text": "线上出现 OOM 怎样排查?"
+ },
+ "7649a66405": {
+ "days": [
+ "java-01"
+ ],
+ "points": "127 时为 true,128 时为 false;自动装箱调用 Integer.valueOf(),值在缓存池范围 -128~127 内时返回 IntegerCache 中的同一个对象,超出范围时 new 一个新对象;启动参数 -XX:AutoBoxCacheMax 可以调大上界。",
+ "status": "active",
+ "text": "Integer a = 127, b = 127 时 a == b 的结果是什么?换成 128 呢?"
+ },
+ "76647ed961": {
+ "days": [
+ "sprint-05"
+ ],
+ "points": "聚簇索引:InnoDB 主键索引的叶子节点保存整行数据,一张表只有一个;二级索引的叶子保存索引列和主键值,查其它列时先拿到主键,再到聚簇索引查出整行,这就是回表;覆盖索引指索引中已包含查询需要的全部列(含主键),不需要回表,Extra 为 Using index。",
+ "status": "active",
+ "text": "什么是聚簇索引、回表和覆盖索引?"
+ },
+ "76eb545f8c": {
+ "days": [
+ "java-24"
+ ],
+ "points": "间隙锁锁住索引记录之间的开区间,不包含记录本身,只阻止插入,间隙锁之间互不冲突;Next-Key Lock 是记录锁加前面的间隙,为前开后闭区间,例如 c 有 10、11、13、20 时为 (10, 11]、(11, 13] 等;c 为普通索引时 c = 11 FOR UPDATE 锁住 (10, 11] 和 (11, 13),唯一索引并且记录存在时只锁这一条记录。",
+ "status": "active",
+ "text": "间隙锁锁住的是什么范围?"
+ },
+ "7796810063": {
+ "days": [
+ "java-18"
+ ],
+ "points": "Serial 单线程、简单高效,适合单核和小内存,只有 1 个处理器时 JDK 21 仍默认用它;Parallel Scavenge 加 Parallel Old 以吞吐量为目标,适合后台计算;CMS 并发收集老年代,停顿短,但有碎片和浮动垃圾,JDK 14 已移除;G1 是 JDK 9 起的默认收集器,按 Region 回收、停顿可预测,适合大内存的在线服务,ZGC 的停顿通常不超过 1 毫秒,但要给堆留出余量。",
+ "status": "active",
+ "text": "有哪些垃圾收集器?各自适合什么场景?"
+ },
+ "79b04fb5b0": {
+ "days": [
+ "java-10"
+ ],
+ "points": "shutdown() 后不再接受新任务,已提交的任务继续执行完,本身不阻塞,需要等待时调用 awaitTermination();shutdownNow() 相当于对每个线程调用 interrupt(),并返回队列中还没有执行的任务。",
+ "status": "active",
+ "text": "shutdown() 和 shutdownNow() 有什么区别?"
+ },
+ "7ce5cde1cd": {
+ "days": [
+ "java-43"
+ ],
+ "points": "使用参数化查询(PreparedStatement、MyBatis 的 #{}),SQL 结构先确定,参数只当作数据;表名、列名、ORDER BY 字段不能用占位符,用白名单校验;数据库账号只授予最小权限。",
+ "status": "active",
+ "text": "怎样防止 SQL 注入?"
+ },
+ "7d1a93e400": {
+ "days": [
+ "java-50"
+ ],
+ "points": "带 @Transactional 的 Bean 被代理,调用时 TransactionInterceptor 读取事务属性;事务管理器从连接池取连接、setAutoCommit(false),绑定到当前线程的 ThreadLocal,JdbcTemplate、MyBatis 优先使用这个连接,所以方法中的 SQL 都在同一个事务里;方法正常返回则提交,抛出符合规则的异常则回滚,最后恢复连接设置并归还。",
+ "status": "active",
+ "text": "Spring 事务的原理是什么?为什么加了 @Transactional 就有事务了?"
+ },
+ "7e5379ca96": {
+ "days": [
+ "java-08"
+ ],
+ "points": "继承 LinkedHashMap,构造时把 accessOrder 设为 true,每次访问把节点移到链表尾部,链表头部就是最久未使用的节点;重写 removeEldestEntry(),size() 超过容量时返回 true,插入后移除头部节点。",
+ "status": "active",
+ "text": "怎样用 LinkedHashMap 实现 LRU 缓存?"
+ },
+ "7ec003a5dd": {
+ "days": [
+ "java-44"
+ ],
+ "points": "保存当前线程的寄存器、程序计数器、栈指针,恢复另一个线程的值;切换到另一个进程还要切换页表,TLB 和 CPU 缓存中原来的映射和数据大多用不上、要重新加载(有 PCID 时 TLB 不必全部清空),这部分间接开销通常更大,工作集很小时两者几乎没有差别;实测一次切换约 1.1 微秒(其中还包含一次 write 和一次 read)。",
+ "status": "active",
+ "text": "上下文切换有哪些开销?"
+ },
+ "7ef8c8b5a2": {
+ "days": [
+ "java-48"
+ ],
+ "points": "LT(默认)只要描述符仍可读,每次 epoll_wait 都会报告;ET 只在有新数据到达时报告一次,必须用非阻塞描述符并循环读到 EAGAIN,否则剩下的数据不会再触发事件,连接停住;ET 减少了重复通知,但要防止一个连接长时间占住线程。",
+ "status": "active",
+ "text": "epoll 的水平触发和边缘触发有什么区别?"
+ },
+ "80afd5ae0b": {
+ "days": [
+ "java-47"
+ ],
+ "points": "分页对程序员透明,分段要显式划分;分页是一维地址空间,分段是二维的;页大小固定,段大小可变;分页为了实现虚拟内存,分段为了按逻辑划分程序和数据、便于共享和保护;分页没有外部碎片,但每个段或区域的最后一页可能用不满,形成内部碎片,分段会留下大小不一的空闲块,形成外部碎片(x86-64 的 Linux 只用分页)。",
+ "status": "active",
+ "text": "分页和分段有什么区别?"
+ },
+ "80d3fed1b5": {
+ "days": [
+ "java-26"
+ ],
+ "points": "给经常出现在 WHERE、JOIN、ORDER BY、GROUP BY 中并且区分度高的列建索引,状态这类区分度低的列单独建索引作用不大,但总是查询占比很少的值时索引仍然有效;多个条件常一起出现时建联合索引,等值列在前、排序列其次、范围列最后;尽量让高频查询用上覆盖索引;索引不是越多越好,有 (a, b) 时 (a) 是冗余的,主键用较短的自增整数。",
+ "status": "active",
+ "text": "建索引有哪些原则?"
+ },
+ "80e07271f2": {
+ "days": [
+ "llm-12"
+ ],
+ "points": "原始问题常不适合直接检索:有指代(“那 ConcurrentHashMap 呢”)时结合历史补全;问题太短时用 HyDE 先生成假设的回答再检索;多个子问题拆开分别检索再合并,RAG-Fusion 改写成多个查询后用 RRF 合并;代价是多一次模型调用。",
+ "status": "active",
+ "text": "为什么要改写用户的查询?有哪些方法?"
+ },
+ "8325776328": {
+ "days": [
+ "sprint-13"
+ ],
+ "points": "用 SET lock_key unique_value NX PX 30000 一条命令加锁并设置过期时间;释放时用 Lua 脚本比较 value 是自己的才删除,避免误删别人的锁;业务超时锁会提前释放,需要续期,Redisson 的看门狗每 10 秒把过期时间重置为 30 秒,并用哈希计数支持可重入;主从是异步复制,切换时锁可能丢失;这种情况和续期失败都会让两个客户端同时持锁,正确性要求高时用栅栏令牌或数据库的唯一约束、条件更新兜底。",
+ "status": "active",
+ "text": "用 Redis 怎样实现分布式锁?有哪些问题?"
+ },
+ "857dd1fbdb": {
+ "days": [
+ "java-62"
+ ],
+ "points": "拉模式(读扩散)发帖只写一次,读时合并所有关注者的发件箱,关注越多读越慢;推模式(写扩散)发帖写入每个粉丝的收件箱,读很快,但粉丝越多写越多(1000 万粉丝单节点约 50 秒);实际推拉结合:普通用户推,大 V 由粉丝读时拉取合并,只推给活跃粉丝,收件箱只保留最近若干条。",
+ "status": "active",
+ "text": "Feed 流用推模式还是拉模式?"
+ },
+ "85d1c43400": {
+ "days": [
+ "java-62"
+ ],
+ "points": "301 永久跳转,没有缓存指令时也可能被浏览器缓存,之后不再请求短链服务,已缓存的跳转无法撤回;302 临时跳转,默认不缓存,统计点击时再加 Cache-Control: private, no-store;需要统计点击、修改目标地址或设置有效期时用 302,只想减轻服务端压力时用 301。",
+ "status": "active",
+ "text": "短链跳转用 301 还是 302?"
+ },
+ "86472abfac": {
+ "days": [
+ "java-54"
+ ],
+ "points": "数据库:实现简单,性能低;Redis:性能最高,适合高并发,但主从切换、进程暂停时可能两个客户端同时持锁;ZooKeeper:写入要同步到多数节点,性能较低但更可靠,还能提供栅栏令牌,适合一致性要求高、并发不大的场景。",
+ "status": "active",
+ "text": "数据库、Redis、ZooKeeper 三种实现怎样选择?"
+ },
+ "874987f0de": {
+ "days": [
+ "java-53"
+ ],
+ "points": "注册与配置:Nacos,或 Eureka 加 Spring Cloud Config;负载均衡和调用:Spring Cloud LoadBalancer、OpenFeign;网关和熔断:Spring Cloud Gateway、Resilience4j 或 Sentinel;分布式事务和链路追踪:Seata、Micrometer Tracing。",
+ "status": "active",
+ "text": "Spring Cloud 有哪些常用组件?"
+ },
+ "875d608421": {
+ "days": [
+ "java-06"
+ ],
+ "points": "ArrayList 基于动态数组,支持随机访问,插入删除要移动元素;LinkedList 基于双向链表,插入删除只改指针,但要先遍历到插入位置;10 万个元素中间插入 1 万次,ArrayList 约 50 ms,LinkedList 约 0.8~1 秒,只有头部插入 LinkedList 明显更快;多数场景用 ArrayList,头尾频繁增删用 ArrayDeque 或 LinkedList。",
+ "status": "active",
+ "text": "ArrayList 和 LinkedList 有什么区别?分别用在什么场景?"
+ },
+ "87aaeeaa89": {
+ "days": [
+ "java-36"
+ ],
+ "points": "保证最后一个 ACK 能到达:ACK 丢失时对方会重传 FIN,2MSL 正好覆盖 ACK 的 1 MSL 加重传 FIN 的 1 MSL;让本连接的旧报文都从网络中消失,不干扰同一四元组的新连接;Linux 中固定为 60 秒。",
+ "status": "active",
+ "text": "为什么 TIME_WAIT 要等待 2MSL?"
+ },
+ "888d45b3b3": {
+ "days": [
+ "llm-07"
+ ],
+ "points": "优先用约束解码:每生成一个 Token 前按 Schema 屏蔽会让输出不合法的 Token;JSON 模式只保证是合法 JSON,不保证字段;不支持时先修复(去掉代码块标记、截取首个 { 到末个 }),再用 JSON Schema 校验,失败时带着具体错误重试 1~2 次,仍失败就返回默认值或转人工。",
+ "status": "active",
+ "text": "怎样让模型稳定地输出合法的 JSON?"
+ },
+ "89a386e6ca": {
+ "days": [
+ "java-64"
+ ],
+ "points": "容器跟随 1 号进程,主进程结束容器就退出;用 docker ps -a 看退出码,docker logs 看报错(如 MySQL 没设 MYSQL_ROOT_PASSWORD),docker inspect 看是否 OOMKilled;常见原因还有启动命令把服务放到后台(nginx 没加 daemon off)、缺少依赖库或 CPU 架构不一致。",
+ "status": "active",
+ "text": "容器启动后马上退出,怎样排查?"
+ },
+ "8a7213cfde": {
+ "days": [
+ "llm-06"
+ ],
+ "points": "检测:有资料时把回答拆成结论逐条检查依据,引文和数字用代码核对;RAG 答错时先看正确的资料是否在提示词中,区分检索和生成的问题;没有资料时多次采样比较,编造的内容往往每次不同;缓解:用 RAG 提供资料并允许回答不知道,降低温度、要求引用,用程序或另一个模型校验,展示来源、高风险结果人工确认。",
+ "status": "active",
+ "text": "怎样检测和缓解幻觉?"
+ },
+ "8c4d592987": {
+ "days": [
+ "llm-07"
+ ],
+ "points": "身份、规则和输出格式放在系统消息中,写清任务、约束、输出格式和示例;要求具体,例如“不超过 100 字”而不是“简洁一些”;说明信息不足时怎么处理,否则模型倾向编造;用少样本示例说明格式,多步推理让模型先写步骤(CoT);改动在评测集上比较,每条运行多次,从失败的样例入手补规则或示例。",
+ "status": "active",
+ "text": "怎样写好一个提示词?"
+ },
+ "8c543dbbcb": {
+ "days": [
+ "java-01"
+ ],
+ "points": "哈希值不变,只需计算一次,适合做 HashMap 的键;String Pool 中多个引用共享同一个对象,只有不可变才能这样共享;作为网络连接等参数时不会被中途改掉;不可变的对象天然线程安全。",
+ "status": "active",
+ "text": "String 为什么设计成不可变的?"
+ },
+ "8d674478d1": {
+ "days": [
+ "java-46"
+ ],
+ "points": "用阻塞式的锁时,死锁线程几乎不耗 CPU(例如等待 synchronized 的线程处于 BLOCKED),现象是 CPU 低而请求全部卡住;用 jstack 或 jcmd \\ Thread.print 打印线程栈,末尾会报告 Found one Java-level deadlock,列出各线程等待的锁和持有者;程序中也可以定时调用 ThreadMXBean.findDeadlockedThreads() 检测并告警,但它检测不到包含虚拟线程的死锁环和 Semap",
+ "status": "active",
+ "text": "线上的 Java 程序发生死锁,怎样定位?"
+ },
+ "8e70e9576a": {
+ "days": [
+ "java-08"
+ ],
+ "points": "元素个数达到 sizeCtl(容量的 0.75 倍)时创建 2 倍的新数组,多个线程用 CAS 修改 transferIndex 领取至少 16 个桶从后往前迁移,迁移完的桶放入 ForwardingNode,put 遇到它会先尝试帮忙迁移,再到新数组中插入;元素个数分散在 baseCount 和多个 CounterCell 中,size() 不加锁求和,期间其它线程可能在增减,所以只是估计值。",
+ "status": "active",
+ "text": "ConcurrentHashMap 是怎样扩容的?size() 为什么不精确?"
+ },
+ "904bc79056": {
+ "days": [
+ "java-69"
+ ],
+ "points": "给出可以验证的目标,例如让某个测试通过,修 bug 时先写一个能复现问题的测试;先让它阅读代码、给出方案,确认后再修改;任务拆小,一次只改一个功能,便于审查;生成的代码同样要做代码审查和测试,测试用例按需求写,由人检查边界;反复纠正的要求写进 CLAUDE.md、AGENTS.md 这类项目约定文件;AI 生成的代码占多大比例不是关键,开发者仍要负责需求、方案、审查和上线的结果,涉及资金和数据安全的修改每一处都要审查。",
+ "status": "active",
+ "text": "你平时怎样用 AI 编程工具?怎样保证生成的代码质量?"
+ },
+ "91d75f1bb2": {
+ "days": [
+ "llm-11"
+ ],
+ "points": "Embedding 模型分别编码问题和文档,文档向量能提前算好,速度快,但细节的对应关系在压成一个向量时丢失;重排模型(交叉编码器)把问题和文档拼在一起编码,每个词之间都能算注意力,更准,但每一对都要重算;所以先快速召回几十条,再重排选出几条;混入 12.5 万条干扰文本时,重排 RRF 的前 50 个候选使 Recall@1 从 0.275 升到 0.700。",
+ "status": "active",
+ "text": "向量检索已经计算了相似度,为什么还要重排?"
+ },
+ "91de874283": {
+ "days": [
+ "java-25"
+ ],
+ "points": "InnoDB 的聚簇索引就是主键索引(没有主键时用第一个非空唯一索引,再没有就用隐藏的行 ID),叶子节点保存整行数据,一张表只有一个;二级索引的叶子节点保存索引列和主键值;通过二级索引查其它列时,先查到主键,再到聚簇索引中查出整行,称为回表;查询的列都在二级索引中时不需要回表(覆盖索引)。",
+ "status": "active",
+ "text": "聚簇索引和二级索引有什么区别?什么是回表?"
+ },
+ "92dc236853": {
+ "days": [
+ "llm-07"
+ ],
+ "points": "不一定,约束解码和 Schema 校验只保证格式;订单是否属于当前用户、退款是否超过实付金额这类业务规则要在代码中校验;还要检查 finish_reason 是否为 length,以及字段类型是否与代码一致。",
+ "status": "active",
+ "text": "输出格式正确,内容就一定正确吗?"
+ },
+ "94fc7c8234": {
+ "days": [
+ "java-45"
+ ],
+ "points": "信号量是整型变量,down(P)在大于 0 时减 1、等于 0 时睡眠,up(V)加 1 并唤醒等待者,两者是原语;取值只有 0、1 的二值信号量可以当作互斥量,进入临界区前 down、离开时 up(但信号量可以由别的进程 up,互斥锁只能由加锁的线程释放);同步用计数信号量,例如生产者-消费者用 empty、full 计数,且要先 down(empty) 再 down(mutex),反过来会死锁。",
+ "status": "active",
+ "text": "信号量是怎样实现同步与互斥的?"
+ },
+ "955ea8cac8": {
+ "days": [
+ "sprint-06"
+ ],
+ "points": "修改时旧版本写入 Undo Log,经回滚指针连成版本链;ReadView 记录创建时未提交的事务列表、其中最小的 TRX_ID_MIN 和下一个要分配的 TRX_ID_MAX;版本的 TRX_ID 是自己的或小于 MIN 时可见,大于等于 MAX 或仍在列表中时不可见,就沿版本链找上一个版本;提交读每次快照读都新建 ReadView,可重复读在第一次快照读时创建、整个事务复用。",
+ "status": "active",
+ "text": "MVCC 的原理是什么?"
+ },
+ "97c02eae0a": {
+ "days": [
+ "java-15",
+ "sprint-03"
+ ],
+ "points": "ThreadLocalMap 的 Entry 对 key(ThreadLocal 对象)是弱引用,对 value 是强引用;ThreadLocal 没有其它强引用时 key 在 GC 后变为 null,value 仍被线程的 Map 引用;这样的陈旧条目要等这个线程之后调用 set()、remove() 或 get() 未命中时才可能被清理,不保证及时,static final 的 ThreadLocal 一直强可达,弱引用不起作用;复用线程还会让后一个请求读到前一个请求的数",
+ "status": "active",
+ "text": "ThreadLocal 为什么可能导致内存泄漏?"
+ },
+ "98a89cafcd": {
+ "days": [
+ "llm-01"
+ ],
+ "points": "几乎都在矩阵中:每层的注意力和前馈网络,以及词嵌入和输出层(Qwen2.5-7B 中各约 7%);Qwen2.5-7B 的前馈网络三个矩阵(3584 → 18944 → 3584)占全部参数的 75%;GQA 使 k_proj、v_proj 只有 q_proj 的七分之一,总计约 76 亿个参数。",
+ "status": "active",
+ "text": "大模型的参数主要分布在哪里?"
+ },
+ "9a4ab4e114": {
+ "days": [
+ "sprint-06"
+ ],
+ "points": "未提交读:三种都可能发生;提交读:解决脏读;可重复读:再解决不可重复读,按 SQL 标准仍可能幻读,InnoDB 用 MVCC 和 Next-Key Locks 在很大程度上避免;可串行化:三种都解决,并发度最低。",
+ "status": "active",
+ "text": "四种隔离级别分别解决了脏读、不可重复读、幻读中的哪些?"
+ },
+ "9c0c516330": {
+ "days": [
+ "sprint-01"
+ ],
+ "points": "ArrayList 基于动态数组,随机访问快,中间插入删除要移动元素,但一次复制一整块连续内存;LinkedList 基于双向链表,插入删除只改指针,但要先 O(N) 遍历到位置;实测 10 万个元素中间插入 1 万次,ArrayList 约 50 ms、LinkedList 约 0.8~1 秒,多数场景用 ArrayList。",
+ "status": "active",
+ "text": "ArrayList 和 LinkedList 有什么区别?"
+ },
+ "9dde8d6033": {
+ "days": [
+ "java-65"
+ ],
+ "points": "各组件都通过 API Server 协作:kubectl 提交 Deployment,API Server 校验后写入 etcd;Deployment 和 ReplicaSet 控制器依次创建 ReplicaSet 和未指定节点的 Pod,调度器过滤、打分后把节点写入 nodeName;该节点的 kubelet 通过 CRI 让 containerd 创建沙箱(pause 容器持有网络命名空间,CNI 分配 IP),拉取镜像并启动容器;就绪探针通过后 Pod 变为 Ready",
+ "status": "active",
+ "text": "创建一个 Pod 时,Kubernetes 内部经过了哪些步骤?"
+ },
+ "9de3a8a2c2": {
+ "days": [
+ "java-33"
+ ],
+ "points": "键空间分为 16384 个哈希槽,CRC16(key) % 16384 计算键所属的槽,每个主节点负责一部分槽;访问了不负责这个槽的节点会收到 MOVED 重定向,客户端缓存槽映射;多键命令要求键在同一个槽,可以用 {} 哈希标签;扩容时按键迁移槽:源节点标记 MIGRATING、目标节点标记 IMPORTING,用 MIGRATE 分批搬键,已迁走的键返回 ASK,客户端先发 ASKING 再访问目标节点,全部迁完后用 CLUSTER SETSLOT 把槽分给新节点。",
+ "status": "active",
+ "text": "Redis Cluster 怎样分配数据?扩容时怎样迁移?"
+ },
+ "9f8d77ff45": {
+ "days": [
+ "llm-21"
+ ],
+ "points": "提示词注入指通过输入的内容改变模型原本的指令,指令也可能藏在网页、文档或工具结果中(间接注入);指令和资料最终拼成同一个 Token 序列,模型无法可靠区分,提示词只能降低风险,重点是限制后果;外部内容标注为不可信,工具按最小权限提供、在代码中按当前用户校验,高风险操作人工确认;读过不可信内容的任务不给对外发送的工具,前端不自动加载输出中的外部图片,防止数据外泄;输出转义、查询参数化,提示词中不放密钥,记录审计日志。",
+ "status": "active",
+ "text": "怎样防御提示词注入?"
+ },
+ "a03e2ca327": {
+ "days": [
+ "sprint-14"
+ ],
+ "points": "离线:解析文档、切片,用 Embedding 模型转为向量存入向量数据库,同时建立关键词索引;在线:改写问题,向量和关键词两路召回,合并后重排,把最相关的几段和问题放进提示词,生成带引用的回答;每一环都影响最终效果,解析错了或资料没被召回,后面无法弥补。",
+ "status": "active",
+ "text": "RAG 的流程是怎样的?"
+ },
+ "a0f17ec6e9": {
+ "days": [
+ "sprint-08"
+ ],
+ "points": "数据在内存中,一次 GET 只查一次哈希表;单线程执行命令,不加锁、没有线程切换;I/O 多路复用让一个线程处理大量连接,数据少时用 listpack 等紧凑编码;执行命令本身很少成为瓶颈,CPU 时间大部分花在网络读写的系统调用上,实测每条命令的用户态时间约 1 微秒,流水线减少系统调用后吞吐量约为原来的 10 倍。",
+ "status": "active",
+ "text": "Redis 为什么快?"
+ },
+ "a1c4bbe5c6": {
+ "days": [
+ "java-23"
+ ],
+ "points": "每行有隐藏列 DB_TRX_ID(最后修改它的事务 ID)和 DB_ROLL_PTR,旧版本通过回滚指针串在 undo log 中,读不加锁;ReadView 记录创建时未提交的事务列表、其中的最小 ID 和下一个要分配的 ID:版本的 TRX_ID 是自己或小于最小 ID 时可见,大于等于下一个 ID 时不可见,在两者之间则不在列表中才可见;不可见就沿回滚指针找上一个版本;提交读每次快照读都创建新的 ReadView,可重复读整个事务复用第一次快照读时创建的那个。",
+ "status": "active",
+ "text": "MVCC 的原理是什么?ReadView 怎样判断可见性?"
+ },
+ "a21036a9de": {
+ "days": [
+ "java-57"
+ ],
+ "points": "作用:异步处理(注册后发验证邮件)、流量削峰(消费者按处理能力拉取)、应用解耦(支付成功发一条消息,积分、物流、短信各自订阅);下一步依赖调用结果、被调用方不可用时调用方也应失败,用 RPC,如支付前扣库存;结果可以稍后产生或有多个接收方,用消息队列,如支付后加积分。",
+ "status": "active",
+ "text": "消息队列有什么作用?什么时候用 RPC,什么时候用消息队列?"
+ },
+ "a2d1d91ef7": {
+ "days": [
+ "llm-14"
+ ],
+ "points": "应用在请求中附上工具的名称、用途和参数 JSON Schema,推理服务按对话模板拼进提示词;模型需要时生成特定格式的文本(如 Qwen2.5 包在 tool_call 标签里的 JSON),服务解析成 tool_calls 返回;应用校验后执行,用 tool 消息和 tool_call_id 发回结果;本质仍是生成文本,模型可能调用不存在的工具、写错参数或编造订单号,严格模式只保证参数符合 Schema,所以必须先校验再执行。",
+ "status": "active",
+ "text": "Function Calling 的原理是什么?"
+ },
+ "a3c215dfb9": {
+ "days": [
+ "java-41",
+ "sprint-10"
+ ],
+ "points": "以 TLS 1.2 的 ECDHE 为例:ClientHello 带客户端随机数、密码套件列表和 SNI;服务器回 ServerHello(选定套件、服务器随机数)、证书链和 ServerKeyExchange(ECDHE 公钥及证书私钥的签名);客户端验证证书和签名后发出自己的 ECDHE 公钥,双方算出预主密钥,结合两个随机数派生会话密钥,再互发 Finished 校验握手未被篡改;TLS 1.2 要 2 个 RTT,TLS 1.3 在 ClientHello 中直接带公",
+ "status": "active",
+ "text": "HTTPS 的握手过程是怎样的?"
+ },
+ "a47f889523": {
+ "days": [
+ "java-03"
+ ],
+ "points": "浅拷贝的引用类型字段与原对象指向同一个对象;深拷贝要把对象图中的可变对象逐层复制,String 这类不可变对象可以共享,例如订单的商品列表要复制列表本身和其中的每个商品;用 clone() 实现时先调用 super.clone(),再为可变的引用字段新建对象并复制内容;clone() 需要实现 Cloneable、处理异常和类型转换,更推荐拷贝构造函数或拷贝工厂。",
+ "status": "active",
+ "text": "浅拷贝和深拷贝有什么区别?怎样实现深拷贝?"
+ },
+ "a4e729949e": {
+ "days": [
+ "java-43"
+ ],
+ "points": "XSS 把恶意脚本注入网页,利用用户对网站的信任,可窃取 Cookie、冒用身份,防范:输出时按上下文转义、富文本白名单过滤、Cookie 设 HttpOnly、CSP 禁止内联脚本;CSRF 诱导浏览器带着登录状态访问目标网站执行操作,利用网站对浏览器的信任,防范:校验随机 Token、SameSite Cookie、检查 Referer 和 Origin、敏感操作要求重新输入密码或短信验证码。",
+ "status": "active",
+ "text": "XSS 和 CSRF 有什么区别?分别怎样防范?"
+ },
+ "a53acc431d": {
+ "days": [
+ "llm-13"
+ ],
+ "points": "检索看 Recall@K(相关切片有多少在前 K 个中)、Precision@K(前 K 个中相关的比例)、MRR(第一个相关结果排名倒数的平均)和 NDCG(分级相关、按排名打折),K 增大时召回率不降、精确率往往下降,放进提示词的切片还要看精确率和端到端正确率;生成看忠实度(结论能否在资料中找到依据)和答案相关性(是否回应了问题),通常由模型裁判拆成小步骤打分,两者要一起看。",
+ "status": "active",
+ "text": "怎样评测一个 RAG 系统?"
+ },
+ "a5d2df43ec": {
+ "days": [
+ "sprint-07"
+ ],
+ "points": "undo log 由 InnoDB 写,记录旧版本,用于回滚和 MVCC;redo log 由 InnoDB 写,记录对数据页的修改,循环写,用于崩溃恢复;binlog 由 Server 层写,记录行修改或语句,追加写,用于主从复制和按时间点恢复;两阶段提交先写 redo log 进入 prepare,再写 binlog,最后 commit,崩溃后 prepare 的事务在 binlog 中有 Xid 就提交、否则回滚,保证主库数据与 binlog 一致,从库才不会不一致。",
+ "status": "active",
+ "text": "redo log、undo log 和 binlog 有什么区别?为什么需要两阶段提交?"
+ },
+ "a61b813949": {
+ "days": [
+ "java-51"
+ ],
+ "points": "#{} 被替换为占位符 ?,参数通过 PreparedStatement 单独传给数据库,不会改变 SQL 结构;${} 把参数直接拼进 SQL,传入 1024 OR 1=1 就让条件恒为真,造成 SQL 注入;${} 只用于表名、列名、ORDER BY 字段,并用白名单校验。",
+ "status": "active",
+ "text": "MyBatis 中 #{} 和 ${} 有什么区别?"
+ },
+ "a645db12eb": {
+ "days": [
+ "llm-15"
+ ],
+ "points": "先规划后执行先一次生成带依赖的步骤列表,按依赖执行、无依赖的步骤并行,失败或结果不符时重规划(次数设上限),进度容易展示和恢复,但计划可能与实际不符;ReAct 每步根据中间结果调整,但每步都要调用一次模型,长任务容易偏离;实际常结合使用,每个步骤内部用 ReAct。",
+ "status": "active",
+ "text": "先规划后执行(Plan-and-Execute)和 ReAct 有什么区别?"
+ },
+ "a79be5580a": {
+ "days": [
+ "sprint-12"
+ ],
+ "points": "单例:线程不安全的懒汉式、饿汉式、方法加锁的懒汉式、双重校验锁(变量要加 volatile)、静态内部类、枚举(可防反射和反序列化);项目中常见:下单校验用责任链,运费和支付渠道用策略加简单工厂,物流回调用模板方法,支付成功后的通知用观察者,事务和分布式锁用代理(Spring AOP)。",
+ "status": "active",
+ "text": "单例模式有哪几种写法?项目中还用过哪些设计模式?"
+ },
+ "a79dbaa169": {
+ "days": [
+ "java-29"
+ ],
+ "points": "undo log 由 InnoDB 写,记录修改前的旧版本,用于回滚和 MVCC;redo log 由 InnoDB 写,记录对数据页的修改,空间固定、循环写,用于崩溃恢复;binlog 由 Server 层写,记录行的修改或 SQL 语句,追加写,用于主从复制和恢复到指定时间点。",
+ "status": "active",
+ "text": "redo log、undo log 和 binlog 有什么区别?"
+ },
+ "a7e52d0151": {
+ "days": [
+ "java-05"
+ ],
+ "points": "类加载时 JVM 根据 .class 文件创建 Class 对象,反射通过它在运行时取得类的信息,类名可以到运行时才确定;用 Field 读写字段,Method 的 invoke() 调用方法,Constructor 的 newInstance() 创建对象;应用:Spring 创建 Bean 并注入、MyBatis 写入结果字段、JUnit 调用 @Test 方法、JDK 动态代理;缺点是有性能开销(框架会缓存 Method 对象),类名和方法名写成字符串、失去编译期检查,",
+ "status": "active",
+ "text": "反射的原理是什么?有哪些应用场景?"
+ },
+ "a7f4a61121": {
+ "days": [
+ "llm-13"
+ ],
+ "points": "按数据流动的顺序从前往后查:解析、切片、检索、重排、生成,前面的错误后面无法弥补;每步分别看解析结果与原文是否一致、答案是否完整在一个切片中、相关切片在两路检索和重排前后的排名、直接给正确资料能否答对;也可以先做最后一步,区分问题在检索还是生成。",
+ "status": "active",
+ "text": "RAG 回答得不好,按什么顺序排查?"
+ },
+ "a806a94773": {
+ "days": [
+ "java-16"
+ ],
+ "points": "类加载检查,类没有加载或初始化就先完成类加载过程;分配内存:内存规整时用指针碰撞,有碎片时用空闲列表,每个线程先在 Eden 中的 TLAB 里分配以避免加锁;把实例字段初始化为零值,再设置对象头(Mark Word 和类型指针);执行构造方法。",
+ "status": "active",
+ "text": "对象的创建过程是怎样的?"
+ },
+ "a85a26685e": {
+ "days": [
+ "java-06"
+ ],
+ "points": "添加元素时容量不够就调用 grow(),新容量为 oldCapacity + (oldCapacity >> 1),约 1.5 倍;无参构造的容量依次为 0、10、15、22、33;扩容用 Arrays.copyOf() 复制整个数组,能预估大小时创建时就指定容量。",
+ "status": "active",
+ "text": "ArrayList 是怎样扩容的?"
+ },
+ "a9201474fc": {
+ "days": [
+ "java-48",
+ "sprint-11"
+ ],
+ "points": "select 每次调用都要把描述符集合复制到内核并逐个检查,开销与描述符数成正比,只能监听编号小于 1024 的描述符;poll 用 pollfd 数组,没有 1024 限制,但同样每次复制和遍历;epoll 用 epoll_ctl 把描述符注册到内核的红黑树中只复制一次,就绪时由回调加入就绪链表,epoll_wait 只返回就绪的事件;监听 1 万个描述符、只有 1 个就绪时,poll 每次约 1.6 ms,epoll_wait 约 0.21 µs,但 epoll 只能在 ",
+ "status": "active",
+ "text": "select、poll 和 epoll 有什么区别?"
+ },
+ "a96d51b500": {
+ "days": [
+ "java-31"
+ ],
+ "points": "字典是拉链法的哈希表,桶数为 2 的幂;键数达到桶数时扩容(有子进程执行 BGSAVE 或 AOF 重写时要达到 4 倍),新表大小为不小于键数加 1 的 2 的幂;采用渐进式 rehash:同时保留新旧两张表,之后每次增删改查顺便迁移一个桶,serverCron 也定期迁移,期间新键只写入新表,查找和删除时两张表都可能要查,迁移完后新表替换旧表。",
+ "status": "active",
+ "text": "Redis 的字典是怎样扩容的?"
+ },
+ "a9fdb0e73d": {
+ "days": [
+ "java-56"
+ ],
+ "points": "先确定幂等键:客户端在确认页生成 request_id,重试都带着它,或用业务唯一标识(如支付交易号);判断和处理要原子:唯一索引(重复插入报 1062,返回第一次的结果)、状态机条件更新、版本号乐观锁(重试沿用第一次读到的版本号)、Token(DEL 返回 1 才处理,只能挡住重复点击,最终去重仍靠业务唯一约束);先查再插、分布式锁都不能代替幂等。",
+ "status": "active",
+ "text": "怎样保证接口的幂等?"
+ },
+ "ab0c96ab4a": {
+ "days": [
+ "java-33"
+ ],
+ "points": "主观下线:某个哨兵在 down-after-milliseconds 内没有收到主节点的有效回复;客观下线:认为它下线的哨兵数达到 quorum,再由哨兵投票选出领头哨兵负责故障转移;选新主:排除断线太久的从节点,依次比较优先级(replica-priority 越小越优先)、复制偏移量(越大数据越新)和运行 ID;对选中的从节点执行 REPLICAOF NO ONE,其它从节点改为复制它,旧主节点恢复后成为从节点。",
+ "status": "active",
+ "text": "哨兵怎样发现主节点下线并选出新的主节点?"
+ },
+ "aeb37c713b": {
+ "days": [
+ "llm-01"
+ ],
+ "points": "Token 是分词器切出的文本片段,模型只处理它在词表中的编号,常用字节级 BPE 反复合并最高频的相邻片段;上下文窗口和费用都按 Token 计算;中英文不同,DeepSeek 估算 1 个中文字符约 0.6 个 Token、1 个英文字符约 0.3 个,准确数量以 usage 为准。",
+ "status": "active",
+ "text": "什么是 Token?同样的内容,中文和英文的 Token 数一样吗?"
+ },
+ "af9254f7f2": {
+ "days": [
+ "java-32"
+ ],
+ "points": "惰性删除:访问键时检查,过期就删除并当作不存在;定期删除:每秒 10 次,每次抽 20 个设置了过期时间的键,过期的超过 10% 就继续下一批,每次最多占 25% 的 CPU;内存达到 maxmemory 后按淘汰策略处理,有 volatile 和 allkeys 两个范围的 lru、lfu、random,以及 volatile-ttl 和默认的 noeviction(SET 这类可能增加内存的命令返回 OOM 错误,GET、DEL 照常执行),Redis 4.0 到 7.x",
+ "status": "active",
+ "text": "过期的键怎样删除?内存满了有哪些淘汰策略?"
+ },
+ "b06acac358": {
+ "days": [
+ "java-59"
+ ],
+ "points": "组内每个分区只分配给一个消费者,多出来的消费者分不到分区,只能空闲,例如 3 个分区启动 4 个消费者,其中一个分到 0 个分区;分区数决定了一个组的最大消费并行度;Kafka 4.2 起的共享组没有这个限制,但不保证顺序。",
+ "status": "active",
+ "text": "消费者组中的消费者比分区多时会怎样?"
+ },
+ "b129be802f": {
+ "days": [
+ "java-46"
+ ],
+ "points": "设计时破坏任一必要条件:一次申请全部资源(破坏占有和等待,利用率低)、申请失败就释放已有资源(破坏不可抢占,如 tryLock 超时)、资源编号后按顺序加锁(破坏环路等待);最常用的是按固定顺序加锁,代价最小,例如转账时总是先锁 id 小的账户。",
+ "status": "active",
+ "text": "怎样避免死锁?实际开发中最常用哪种方法?"
+ },
+ "b1361d5a9a": {
+ "days": [
+ "java-63"
+ ],
+ "points": "先确认流量性质:看来源 IP、User-Agent 和接口分布,区分真实增长、爬虫和重试风暴;先保护再扩容:网关限流、关闭非核心功能、熔断慢依赖;逐层查看 QPS、CPU、连接池、慢 SQL、Redis 延迟,找出瓶颈;扩容无状态的应用实例,事后复盘更新容量模型。",
+ "status": "active",
+ "text": "QPS 突然增长到原来的 10 倍,应该先做什么?"
+ },
+ "b21b2cd3d2": {
+ "days": [
+ "java-36"
+ ],
+ "points": "TIME_WAIT 在主动关闭方,多由短连接引起;发起连接的一方每个 TIME_WAIT 占用本地端口 60 秒,连接同一目标时每秒新建约 470 个连接就会耗尽默认的 28232 个端口(服务器一侧的 TIME_WAIT 共用监听端口,不消耗本地端口),用长连接或连接池、把 tcp_tw_reuse 设为 1(默认值 2 只对回环地址生效)、扩大端口范围解决;CLOSE_WAIT 在被动关闭方,是应用没有调用 close(),多为异常分支没关连接或连接池借出未还,要改代码。",
+ "status": "active",
+ "text": "服务器上出现大量 TIME_WAIT 或 CLOSE_WAIT 是什么原因?"
+ },
+ "b3dc5cbe84": {
+ "days": [
+ "java-64"
+ ],
+ "points": "多阶段构建:编译阶段用 Maven 和 JDK,最终阶段只把 jar 复制到 JRE 镜像,镜像从约 680 MB 降到约 236 MB;用 -alpine、-slim 等小的基础镜像,下载、安装和清理写在同一条 RUN 中;变化少的步骤放前面,先复制 pom.xml 下载依赖再复制源码,改源码后重新构建只需约 6 秒;用 .dockerignore 排除 target、.git。",
+ "status": "active",
+ "text": "怎样写一个体积小、构建快的 Dockerfile?"
+ },
+ "b432826e95": {
+ "days": [
+ "java-18",
+ "sprint-04"
+ ],
+ "points": "回收范围:CMS 只回收老年代,要与 ParNew 配合,G1 用 Region 统一管理新生代和老年代;碎片:CMS 用标记 - 清除,碎片多时只能 Full GC,G1 把存活对象复制到空闲的 Region,普通对象不产生碎片(Humongous 对象需要连续的 Region,可能因找不到连续空间而提前 Full GC);停顿:CMS 的停顿无法控制,G1 按停顿时间目标挑选要回收的 Region;并发标记:CMS 用增量更新,G1 用原始快照(SATB)。",
+ "status": "active",
+ "text": "CMS 和 G1 有什么区别?"
+ },
+ "b50334ebd5": {
+ "days": [
+ "java-11"
+ ],
+ "points": "NEW 是创建后还没有启动,start() 后进入 RUNNABLE,可能正在运行,也可能在等 CPU;等待进入 synchronized 时为 BLOCKED,拿到锁后回到 RUNNABLE;无参的 wait()、join() 和 park() 进入 WAITING,等其它线程唤醒(被 notify() 唤醒后要先重新竞争锁,这时为 BLOCKED),sleep() 和带超时的 wait()、join() 进入 TIMED_WAITING,时间到自动唤醒;执行完或因异常结束",
+ "status": "active",
+ "text": "线程有哪些状态?状态之间怎样转换?"
+ },
+ "b5c9ebc9ee": {
+ "days": [
+ "llm-13"
+ ],
+ "points": "优先从真实用户问题中抽样,不够时用模型生成再人工改写措辞,避免照抄原文高估向量检索;包含没有答案、需要多个切片和新旧版本冲突的问题,没有答案的问题回答不知道算对、编出答案算错;标注标准答案和所需的原文片段而不是切片 ID,线上的错误回答持续补充进来。",
+ "status": "active",
+ "text": "评测集怎样构建?"
+ },
+ "b5eb99096c": {
+ "days": [
+ "sprint-04"
+ ],
+ "points": "线程私有:程序计数器、Java 虚拟机栈(每个方法一个栈帧,-Xss 设置大小)、本地方法栈;线程共享:堆(几乎所有对象,分新生代和老年代,-Xms、-Xmx 设置)和方法区(类的元信息和运行时常量池,JDK 8 起由本地内存中的元空间实现;字符串常量池和静态变量 JDK 7 起已移到堆中,JIT 编译的代码在单独的代码缓存中);NIO 还会使用堆外的直接内存;除程序计数器外都可能 OOM,两种栈还可能 StackOverflowError。",
+ "status": "active",
+ "text": "JVM 的内存区域有哪些?"
+ },
+ "b6e89f7079": {
+ "days": [
+ "java-57"
+ ],
+ "points": "发送端:等待确认并重试(Kafka 用 acks=all,检查回调,退出前 flush),要与业务一致时用本地消息表或事务消息;存储端:持久化加多副本(min.insync.replicas,不允许落后副本当选 Leader);消费端:处理完再提交偏移量或 ack;关键业务再定期对账补发。",
+ "status": "active",
+ "text": "怎样保证消息不丢失?"
+ },
+ "b813e96d80": {
+ "days": [
+ "java-66"
+ ],
+ "points": "不丢、不重:发送方为每条消息生成 msg_id,服务端存储后回复 ACK,超时没收到就用同一个 msg_id 重发,服务端用 (sender_id, msg_id) 唯一索引去重;不乱序:服务端为每个会话分配递增的 seq,接收方按 seq 排序,不按各机器的时间;离线:推送只负责通知,接收方记住连续收齐的最大 seq(例如 58),重连时拉取它之后的消息,收到 61 而本地只到 58 时补拉中间的 59、60;用 INCR 分配 seq 时写入失败会留下补不到的空号,后分配",
+ "status": "active",
+ "text": "聊天消息怎样做到不丢失、不重复、不乱序?离线期间的消息怎样补上?"
+ },
+ "b9bc59a83b": {
+ "days": [
+ "llm-10"
+ ],
+ "points": "分词后输入 Transformer 编码器,每个 Token 得到一个向量,再池化(取 [CLS] 或平均值,与训练时一致)成固定长度的向量并归一化;对比学习使意思相近的文本向量靠近;余弦相似度看方向是否接近,归一化后等于点积、计算更快,排序与欧氏距离等价。",
+ "status": "active",
+ "text": "文本是怎样变成向量的?为什么用余弦相似度?"
+ },
+ "b9fdebae51": {
+ "days": [
+ "java-27",
+ "sprint-07"
+ ],
+ "points": "找出:开启慢查询日志,long_query_time 线上调到 1 秒或更低,对比 Rows_examined 和 Rows_sent,用 mysqldumpslow 或 sys.statement_analysis 按语句归类;分析:用 EXPLAIN 和 EXPLAIN ANALYZE 看执行计划,常见原因有索引失效、回表太多、filesort 和临时表、连接列没有索引、等锁(Lock_time 大)、统计信息不准;修改:建合适的索引或改写语句,再对比执行计划和耗时,例如",
+ "status": "active",
+ "text": "线上出现慢 SQL,怎样排查和优化?"
+ },
+ "babcc095aa": {
+ "days": [
+ "java-31"
+ ],
+ "points": "用 ZSET,成员是用户或文章 ID,分值是分数或点赞数;ZINCRBY 加分,ZREVRANGE key 0 9 WITHSCORES 取前 10 名,ZREVRANK 查名次(从 0 开始),都是 O(logN);分值相同时按成员名的字典序排,要先达到的排前面可以把时间编进分值;前 10 名是热点键时,可以在本地缓存中保存 1 秒。",
+ "status": "active",
+ "text": "怎样用 Redis 实现排行榜?"
+ },
+ "bb799ba354": {
+ "days": [
+ "java-12"
+ ],
+ "points": "CountDownLatch 让一个或多个线程等待其它线程:countDown() 使计数减 1,减到 0 时唤醒 await() 的线程,计数不能重置;CyclicBarrier 让多个线程互相等待,全部调用 await() 到达后一起继续,计数自动恢复,可以用于下一轮,还可以指定全部到达时执行的 barrierAction。",
+ "status": "active",
+ "text": "CountDownLatch 和 CyclicBarrier 有什么区别?"
+ },
+ "bbc2863617": {
+ "days": [
+ "llm-19"
+ ],
+ "points": "主要检查结果(例如数据库中是否真有一条退款记录),轨迹只检查必须满足的约束,优先用代码检查,开放内容才用模型裁判;指标有任务成功率、工具调用正确率和效率,用 pass^k 衡量稳定性;裁判有位置偏差、长度偏差和自我偏好,缓解靠交换顺序评两次、给出参考、拆成逐项判断的问题、与人工标注核对一致率。",
+ "status": "active",
+ "text": "怎样评测一个 Agent?用模型做裁判有哪些偏差?"
+ },
+ "bc38dbf743": {
+ "days": [
+ "llm-16"
+ ],
+ "points": "短期记忆是当前会话的原始消息(含工具调用和结果)、摘要和任务状态,常以会话 ID 为键存在 Redis,请求时放入“摘要 + 最近几轮 + 任务状态”;长期记忆由后台提取或模型调用 save_memory 写入,只写用户明确说过、以后有用、业务系统查不到的事实;使用时按用户 ID 过滤,按相似度和时间检索最相关的几条。",
+ "status": "active",
+ "text": "Agent 的短期记忆和长期记忆分别怎样实现?"
+ },
+ "bc787d309a": {
+ "days": [
+ "sprint-05"
+ ],
+ "points": "B+ 树的一个节点就是一个 16 KB 的页,有几百上千个子节点,树很矮(200 万行的主键索引 3 层,二叉树约 21 层),一次 I/O 读一个节点,读盘次数少;内部节点只存 key 和指针,比 B 树每页放的 key 更多,每次查找都走到叶子、I/O 次数稳定;叶子按顺序用指针相连,范围查询顺序扫描即可。",
+ "status": "active",
+ "text": "MySQL 为什么用 B+ 树做索引?"
+ },
+ "bcb8868d08": {
+ "days": [
+ "java-09"
+ ],
+ "points": "corePoolSize 核心线程数,maximumPoolSize 最大线程数;keepAliveTime 和 unit:超出核心线程数的线程空闲多久后回收;workQueue 保存等待任务的阻塞队列,threadFactory 创建线程,常用来起名字;handler 拒绝策略,线程数达到最大且队列已满时处理新提交的任务。",
+ "status": "active",
+ "text": "线程池的核心参数有哪些?"
+ },
+ "bcd73345b9": {
+ "days": [
+ "java-11"
+ ],
+ "points": "wait() 是 Object 的方法,sleep() 是 Thread 的静态方法;wait() 会释放锁,sleep() 不会;wait() 只能在同步方法或同步块中调用,否则抛出 IllegalMonitorStateException;不带超时的 wait() 要等 notify()、notifyAll() 唤醒(也可能虚假唤醒),sleep() 到时间自动返回,两者都可以被 interrupt() 打断。",
+ "status": "active",
+ "text": "wait() 和 sleep() 有什么区别?"
+ },
+ "bdc1af9732": {
+ "days": [
+ "llm-19"
+ ],
+ "points": "LangChain 提供模型、工具、检索器等组件和现成的 Agent,早期核心是按固定顺序串起来的链;LangGraph 是更底层的编排运行时,用共享状态、节点和条件边组成状态图,LangChain 1.0 的 create_agent 就运行在它之上;Agent 和业务流程需要循环、分支、暂停等人工确认和从中间恢复,用链表达很别扭,图加 checkpoint 可以直接表达。",
+ "status": "active",
+ "text": "LangChain 和 LangGraph 有什么区别?为什么用图来编排?"
+ },
+ "bde7a7dfbe": {
+ "days": [
+ "java-13"
+ ],
+ "points": "volatile 只修饰变量,保证可见性和有序性,不保证复合操作的原子性,线程不会阻塞;synchronized 修饰方法和代码块,同一时刻只有一个线程执行,同时保证原子性、可见性和有序性,竞争时线程阻塞;状态标志、发布数据用 volatile,单个变量的读-改-写可以用原子类,多个变量一起修改通常用锁。",
+ "status": "active",
+ "text": "volatile 和 synchronized 有什么区别?"
+ },
+ "be8cf2c3e0": {
+ "days": [
+ "java-33"
+ ],
+ "points": "执行命令是单线程的,6.0 起可以开启 I/O 多线程处理网络读写,关闭文件、AOF 刷盘、UNLINK 释放大键由后台线程完成;快的原因:数据在内存中,单线程不用加锁、没有线程切换,I/O 多路复用让一个线程处理大量连接,小数据用紧凑的编码;瓶颈通常在网络,50 个连接逐条 GET 约 16 万次/秒,流水线约 160 万次/秒。",
+ "status": "active",
+ "text": "Redis 是单线程的吗?为什么单线程也这么快?"
+ },
+ "beeadc9d28": {
+ "days": [
+ "llm-03"
+ ],
+ "points": "temperature 把分数除以 T 再 softmax,T < 1 时分布更尖、T > 1 时更平,例如最高概率 0.50 在 T = 0.5 时升到 0.75;top-p 只在累计概率刚达到 p 的高概率 Token 中采样,候选数随分布变化;抽取字段、调用工具用低温度,写作可调高。",
+ "status": "active",
+ "text": "temperature 和 top-p 分别控制什么?"
+ },
+ "bf51732677": {
+ "days": [
+ "java-63"
+ ],
+ "points": "逐步增加并发,记录吞吐量、P99、错误率和 CPU,找到吞吐不再增长、延迟陡升的拐点,重复多次取中位数;环境、数据量和请求参数的分布要接近生产,读写先分开压再混合,大促前做带标记、写影子库的全链路压测;结论要说明哪一层先到拐点、原因是什么,饱和的资源不一定是根因,例如连接池占满可能是 SQL 在等行锁;实例数 = 峰值 QPS ÷(单实例拐点 × 目标利用率),再用 CPU 使用率校准。",
+ "status": "active",
+ "text": "怎样做压测和容量规划?"
+ },
+ "bfcd308f00": {
+ "days": [
+ "llm-19"
+ ],
+ "points": "执行前先按 Schema 校验参数,再做业务校验;超时、限流、5xx 由代码按指数退避重试 2~3 次,不交给模型(实测把超时回传给 7B 模型,10 次都没有重新调用),接口持续失败时熔断;参数错误把错在哪、正确格式是什么回传给模型修正;仍失败就如实回传,由模型换办法或告知用户、转人工,不能编造结果;同时限制步数和预算、检测有没有进展(比较工具名、参数和结果,而不只数调用次数),有副作用的操作要幂等,只对涉及资金、无法撤销或对外可见的操作人工确认(确认太频繁,人会不看内容就",
+ "status": "active",
+ "text": "工具调用失败时怎样容错?"
+ },
+ "c0ce068130": {
+ "days": [
+ "java-27"
+ ],
+ "points": "type(const、eq_ref、ref、range、index、ALL,从好到差)、key 和 possible_keys、key_len(用到了联合索引的前几列)、rows(估算扫描行数)、Extra;Using filesort 表示无法利用索引的顺序,需要额外排序;Using temporary 表示用了临时表,常见于 GROUP BY 的列无法利用索引的顺序,或者 GROUP BY 和 ORDER BY 的列不一致。",
+ "status": "active",
+ "text": "EXPLAIN 主要看哪些字段?Using filesort 和 Using temporary 表示什么?"
+ },
+ "c226c6df57": {
+ "days": [
+ "llm-14"
+ ],
+ "points": "工作流的步骤和顺序由代码事先写好,模型只做其中某些步骤,可预测、成本可控,适合步骤固定的业务;Agent 由模型决定下一步,适合无法事先确定步骤的开放任务,但成本高、错误会累积(每步 95% 时连续 10 步约 60%),所以先尝试单次调用或工作流;判断看步骤和步骤之间的转移规则能否事先写清,写不全才交给 Agent;实际常把两者组合,退款这类固定流程走工作流,商品咨询交给 Agent。",
+ "status": "active",
+ "text": "Agent 和工作流有什么区别?分别适合什么场景?"
+ },
+ "c4e5f8bc1a": {
+ "days": [
+ "llm-05"
+ ],
+ "points": "LoRA 冻结原矩阵 W,只训练小矩阵 B、A,h = Wx + (α / r)·BAx,B 初始化为 0,训练开始时与原模型相同;可训练参数很少(Qwen2.5-7B 全部线性层 r = 16 时占 0.53%),训练后合并进 W,推理无额外开销;QLoRA 再把冻结的权重量化为 4 位 NF4 存储、计算时还原为 BF16,7B 权重从 15.2 GB 降到约 5.5 GB(嵌入层和输出层仍为 BF16)。",
+ "status": "active",
+ "text": "LoRA 的原理是什么?LoRA 和 QLoRA 有什么区别?"
+ },
+ "c5494b65a3": {
+ "days": [
+ "java-69"
+ ],
+ "points": "Skill 是一个包含 SKILL.md、脚本和参考资料的文件夹,教 Agent 完成某一类任务;核心是渐进式加载:平时只把名称和描述放进上下文(每个约 100 Token),模型判断与当前任务相关时才读入正文,正文引用的文档和脚本需要时才读取或执行;工具是可调用的函数,提供原子能力,MCP 解决工具怎样接入,Skill 提供做事的步骤和规范,执行时仍要调用工具;需要接入外部系统、要求权限控制的能力做成 MCP 服务端,组合已有工具的流程和规范写成 Skill。",
+ "status": "active",
+ "text": "Skill 是什么?它和工具、MCP 有什么区别?"
+ },
+ "c7974bfd54": {
+ "days": [
+ "sprint-09"
+ ],
+ "points": "哨兵在主从复制的基础上自动故障转移:认为主节点下线的哨兵数达到 quorum 后确认客观下线,再选出得票过半且不少于 quorum 的领头哨兵,按优先级和复制偏移量选新主并切换,但只有一个主节点写入,容量受单机内存限制;Redis Cluster 把 16384 个槽分到多个主节点,每个主节点有从节点,同时解决容量和高可用;两者都是异步复制,切换时可能丢失少量写入。",
+ "status": "active",
+ "text": "哨兵和 Redis Cluster 分别解决什么问题?"
+ },
+ "c79e20122e": {
+ "days": [
+ "java-34",
+ "sprint-09"
+ ],
+ "points": "穿透:查询缓存和数据库都不存在的数据,用参数校验、布隆过滤器拦截,空值缓存兜底;击穿:单个热点键过期,用互斥锁只让一个请求查库,或逻辑过期返回旧值、后台重建;雪崩:大量键同时过期或缓存宕机,用随机过期时间、主从加哨兵或集群、本地缓存、限流熔断。",
+ "status": "active",
+ "text": "缓存穿透、击穿和雪崩有什么区别?分别怎样解决?"
+ },
+ "c82b36ba2a": {
+ "days": [
+ "llm-11"
+ ],
+ "points": "HNSW 删除一般只做标记、检索时跳过,之后统一清理,更新等于删除加插入;Milvus 的删除在位图中标记,Compaction 时才真正去掉,所以更新要批量进行;文档修改后按文档 ID 和内容哈希找出变化的文档,删除它的全部旧切片再写新切片,只改权限或版本状态时只更新元数据;按切片 ID 幂等写入,并与关系数据库定期对账;更换 Embedding 模型或切片方式时,在新集合中重建索引并评测,再把别名切过去,查询用的模型同时切换。",
+ "status": "active",
+ "text": "向量数据库中的数据怎样更新和删除?"
+ },
+ "c8485a2dab": {
+ "days": [
+ "java-65"
+ ],
+ "points": "容器启动后反复退出,重启等待按 10 秒、20 秒、40 秒翻倍,最长 5 分钟;kubectl describe pod 看事件和退出码,kubectl logs --previous 看上一次退出前的日志(如配置错误);退出码 137、原因 OOMKilled 时检查内存 limits 与 JVM 堆和堆外内存。",
+ "status": "active",
+ "text": "Pod 一直处于 CrashLoopBackOff 状态,怎样排查?"
+ },
+ "c93bb3c65b": {
+ "days": [
+ "java-67"
+ ],
+ "points": "关单以支付平台为准,分三步:先用条件更新把订单从待支付改为关单中,影响 0 行就按当前状态处理;再调用平台的关单接口,成功或订单已关闭时改为已关闭,在同一个事务中回补库存;关单失败时查单,已支付就按支付成功处理(与回调走同一段代码),订单不存在可以关闭,结果不明保持关单中,由扫描任务稍后重做;只改本地状态的话,用户打开旧的支付页面仍能付款;下单时把同一个截止时间作为 time_expire 传给平台,否则平台默认 7 天内都能支付;回调在关单中到达时仍然有效。",
+ "status": "active",
+ "text": "订单超时未支付要自动关闭,关单时用户恰好付了款,怎样保证两边一致?"
+ },
+ "c9434cba34": {
+ "days": [
+ "llm-15"
+ ],
+ "points": "ReAct 让推理和行动交替:先思考,再调用工具,把真实结果作为观察放回上下文再思考;与思维链相比,事实来自外部而不是模型的记忆,HotpotQA 的分析中思维链失败案例 56% 是编造事实,ReAct 为 0%;适合下一步取决于上一步结果、步数不多的任务,缺点是只能串行、长任务容易偏离目标。",
+ "status": "active",
+ "text": "ReAct 是什么?为什么用它?"
+ },
+ "cd627f049d": {
+ "days": [
+ "java-66"
+ ],
+ "points": "先明确需求:要支持的功能、规模和哪些数据不能出错,例如点赞不能重复计、点赞数允许延迟几秒;再估算高峰时的读写量和存储:日活 2000 万、每人每天点赞 5 次时,高峰小时写约 1700 次/秒、读约 3.3 万次/秒,点赞关系每年约 4 TB;然后定义接口和表,(user_id, video_id) 作主键让重复点赞插入失败;先给出单库事务的简单方案,再按估算找瓶颈逐个改进:热门视频的计数行锁改为 Redis 计数加合并写回,读多加缓存,数据量大按 user_id 分片;最后",
+ "status": "active",
+ "text": "面试中遇到“设计一个点赞系统”这类开放题,应该按什么顺序回答?"
+ },
+ "cf1562e438": {
+ "days": [
+ "java-04"
+ ],
+ "points": "重写发生在子类与父类之间,方法声明相同,访问权限不能更小,返回类型与父类相同或为其子类型(基本类型必须相同),抛出的受检异常只能是父类方法声明的异常或其子类型;重载指一个类中(包括从父类继承来的方法)方法名相同,参数类型、个数、顺序至少一个不同,只有返回值不同不算重载;重载在编译时按参数的静态类型选定,重写在运行时按对象的实际类型选定。",
+ "status": "active",
+ "text": "重载和重写有什么区别?"
+ },
+ "cff796edde": {
+ "days": [
+ "llm-01"
+ ],
+ "points": "目前的大模型基本是只保留解码器的 Decoder-only 结构;Token 经词嵌入变成向量(Qwen2.5-7B 为 3584 维),依次经过几十个相同的层(28 层),每层先用注意力从前文收集信息,再用前馈网络逐位置加工;子层都带残差连接和归一化(多为 Pre-Norm 的 RMSNorm);最后一个位置经线性层得到 logits,softmax 后得到下一个 Token 的概率。",
+ "status": "active",
+ "text": "Transformer 的结构是怎样的?"
+ },
+ "d0cadbf383": {
+ "days": [
+ "llm-17"
+ ],
+ "points": "MCP 是 AI 应用与外部工具、数据之间的统一协议,工具实现一次服务端就能被所有支持 MCP 的应用使用,消息用 JSON-RPC 2.0,经 stdio 或 Streamable HTTP 传输,tools/list 发现工具、tools/call 调用;Function Calling 是模型侧的能力,模型按工具描述给出调用;两者配合:主机把 MCP 工具转成工具定义交给模型,再通过 MCP 执行模型给出的调用。",
+ "status": "active",
+ "text": "MCP 是什么?MCP 和 Function Calling 有什么区别?"
+ },
+ "d175c9b0a1": {
+ "days": [
+ "java-16"
+ ],
+ "points": "程序计数器、Java 虚拟机栈和本地方法栈是线程私有的,随线程创建和销毁;堆和方法区被所有线程共享,JDK 8 起方法区由本地内存中的元空间实现;另有 NIO 使用的直接内存;除程序计数器外都可能抛出 OutOfMemoryError,两种栈还可能抛出 StackOverflowError。",
+ "status": "active",
+ "text": "JVM 的内存区域有哪些?哪些是线程私有的?"
+ },
+ "d2c8cce9c3": {
+ "days": [
+ "sprint-11"
+ ],
+ "points": "四个条件:互斥、占有和等待、不可抢占、环路等待,同时成立才会死锁;预防就是破坏其中一个,例如一次申请全部资源、申请不到就释放已占有的资源或给锁设过期时间;最常用的是给资源编号、按顺序申请来破坏环路等待,例如转账总是先锁 id 小的账户。",
+ "status": "active",
+ "text": "死锁的必要条件是什么?怎样避免?"
+ },
+ "d30a0abfb5": {
+ "days": [
+ "java-16"
+ ],
+ "points": "栈中是方法调用的栈帧(局部变量、操作数、返回地址),堆中是对象和数组;每个线程有自己的栈,堆被所有线程共享,只有堆中的对象有线程安全问题;栈帧在方法返回时销毁,堆中的对象由垃圾收集器回收;栈大小由 -Xss 设置,太深抛出 StackOverflowError,堆由 -Xms、-Xmx 设置,放不下抛出 OutOfMemoryError。",
+ "status": "active",
+ "text": "堆和栈有什么区别?"
+ },
+ "d3574a433d": {
+ "days": [
+ "java-58"
+ ],
+ "points": "先区分生产突增(峰值后可自然消化)还是消费变慢(下游变慢、反复重试、频繁重平衡),先修复消费变慢的原因,失败消息转入死信队列;扩容消费者(Kafka 普通消费者组、RocketMQ 4.x 中超过分区或队列数的消费者会空闲),或批量写库、线程池并行(同一 key 交给同一线程);分区不够时转发到分区更多的新主题,不重要的消息降级跳过;积压接近保留时间(Kafka 默认 7 天)前先转存。",
+ "status": "active",
+ "text": "消息大量积压时怎样处理?"
+ },
+ "d45f6f5a22": {
+ "days": [
+ "java-37"
+ ],
+ "points": "TCP 面向字节流,不保留应用写入的边界,一次读取可能读到两条消息拼在一起(粘包)或半条消息(拆包);原因有按缓冲区现有数据读取、Nagle 合并小数据、超过 MSS 被分段;在应用层约定边界:固定长度、分隔符或长度字段(长度要限制上限);UDP 面向报文,没有粘包。",
+ "status": "active",
+ "text": "什么是粘包?怎样解决?"
+ },
+ "d47bb94d7b": {
+ "days": [
+ "java-60"
+ ],
+ "points": "QPS 是每秒处理的请求数,衡量吞吐量;TPS 是每秒处理的事务数,一个事务可能包含多次请求或多条 SQL;TP99 是响应时间的 99 分位,即 99% 的请求不超过这个时间,比平均值更能反映慢请求。",
+ "status": "active",
+ "text": "QPS、TPS 和 TP99 分别是什么意思?"
+ },
+ "d4c6da97dc": {
+ "days": [
+ "java-17"
+ ],
+ "points": "从 GC Roots 出发沿强引用可达的对象不会被回收,只是互相强引用、从 GC Roots 不可达的仍会被回收;只被软引用(SoftReference)关联的对象在即将内存溢出前一定被回收,其它时候 HotSpot 按多久没被访问决定是否清除;只被弱引用(WeakReference)关联的对象在下一次回收它所在区域的 GC 时一定被回收,例如 ThreadLocalMap 的 key;虚引用(PhantomReference)不影响对象的存活,get() 总是返回 null",
+ "status": "active",
+ "text": "强引用、软引用、弱引用、虚引用有什么区别?"
+ },
+ "d560ae1052": {
+ "days": [
+ "sprint-05"
+ ],
+ "points": "联合索引先按第一列排序、再按第二列,所以条件要从最左列开始连续使用才能定位,与 WHERE 中的书写顺序无关,范围条件之后的列一般不能再缩小扫描范围(带等号时例外);常见的无法用索引定位的情况:缺少最左列(8.0.13 起少数情况可跳跃扫描)、对索引列用函数(可建函数索引)、字符串列与数字比较、LIKE 以 % 开头、OR 一侧没有索引;匹配的行太多时,优化器按成本选择全表扫描。",
+ "status": "active",
+ "text": "联合索引的最左前缀是什么?哪些情况下索引会失效?"
+ },
+ "d5ef30f656": {
+ "days": [
+ "java-15"
+ ],
+ "points": "同步块编译为 monitorenter 和 monitorexit,另有异常处理器保证抛出异常时也释放锁,同步方法用 ACC_SYNCHRONIZED 标志;没有竞争时是轻量级锁,用 CAS 把 Mark Word 换成指向栈中 Lock Record 的指针;有竞争时膨胀为 ObjectMonitor,_owner 指向持有锁的线程,_recursions 记录重入次数,抢不到锁的线程进入 _cxq、_EntryList 阻塞,调用 wait() 的线程进入 _WaitSe",
+ "status": "active",
+ "text": "synchronized 的底层是怎样实现的?"
+ },
+ "d707267199": {
+ "days": [
+ "java-55"
+ ],
+ "points": "同步阻塞:参与者准备后一直持有锁,等待协调者的决定;单点问题:协调者在提交阶段故障时,已同意的参与者既不能提交也不能回滚,只能等待;数据不一致:协调者只发出部分 Commit 就崩溃;太过保守:准备阶段任一参与者失败、回复中止或超时,整个事务都要回滚;用数据库的 XA 实现时每个参与者都要支持 XA,Redis、Kafka、RocketMQ 和一般的 HTTP 接口无法直接参与。",
+ "status": "active",
+ "text": "2PC 有什么问题?"
+ },
+ "d82af70a7a": {
+ "days": [
+ "java-21"
+ ],
+ "points": "先明确目标(在线服务看最长停顿,批处理看吞吐量),默认用 G1 并开启 GC 日志观察;-Xms 与 -Xmx 设为相同,-Xmx 要给元空间、线程栈、直接内存等堆外内存留出余量,不能等于容器的内存,按需设置 MaxGCPauseMillis、MaxMetaspaceSize、HeapDumpOnOutOfMemoryError;频繁 Full GC 时用 jstat -gcutil 看老年代使用率,降不下来查泄漏,能降下来就增大堆或调整新生代;GC 问题多数是代码造成的,只",
+ "status": "active",
+ "text": "做过哪些 JVM 调优?"
+ },
+ "d85d93a06e": {
+ "days": [
+ "java-02"
+ ],
+ "points": "String Pool 中还没有 \"abc\" 时创建两个:字面量 \"abc\" 在 String Pool 中(HotSpot 通常在第一次执行对应的 ldc 时创建),另一个是 new 在堆中创建的对象;Pool 中已有 \"abc\" 时只新建堆中的一个;两个对象共享同一个 value 数组。",
+ "status": "active",
+ "text": "new String(\"abc\") 会创建几个对象?"
+ },
+ "d97306ed39": {
+ "days": [
+ "java-56"
+ ],
+ "points": "HTTP 是协议,RPC 是调用方式,RPC 可以基于自定义 TCP 协议(Dubbo)或 HTTP(gRPC 基于 HTTP/2);内部服务多用 RPC 框架:序列化更紧凑(同样的参数 Protobuf 5 字节、JSON 25 字节)、长连接复用、内置服务治理、按接口生成代码;HTTP + JSON 通用、可读,适合对外接口。",
+ "status": "active",
+ "text": "RPC 和 HTTP 有什么区别?"
+ },
+ "d977db9dde": {
+ "days": [
+ "java-30"
+ ],
+ "points": "数据位置:Redis 全部在内存中,容量受内存限制,MySQL 在磁盘上,缓冲池只缓存热点页;查询:Redis 只能按键访问数据结构,MySQL 用 SQL 按任意条件查询、联表和聚合;事务和持久性:MySQL 满足 ACID,提交后不丢数据,Redis 事务中的命令出错不回滚,开启 AOF everysec 也可能丢失最近 1 秒的写入;通常 MySQL 存业务数据,Redis 作缓存。",
+ "status": "active",
+ "text": "Redis 和 MySQL 有什么区别?"
+ },
+ "da147d6f3a": {
+ "days": [
+ "java-51"
+ ],
+ "points": "DispatcherServlet 统一接收请求,HandlerMapping 找到处理方法和拦截器;依次执行拦截器的 preHandle,返回 false 就停止;HandlerAdapter 转换参数、调用 Controller,再把返回值转换为响应(如 JSON);倒序执行 postHandle(Controller 抛出异常时跳过);返回视图名时由 ViewResolver 渲染;最后倒序执行 preHandle 返回 true 的拦截器的 afterCompleti",
+ "status": "active",
+ "text": "Spring MVC 处理一个请求的过程是怎样的?"
+ },
+ "da52b096ec": {
+ "days": [
+ "llm-10"
+ ],
+ "points": "逐个比较时,100 万个 1024 维向量每次查询约 10 亿次乘加,数据量和并发大时太慢;ANN 索引用少量精度换速度,可能漏掉个别真正最近的向量。",
+ "status": "active",
+ "text": "为什么需要近似最近邻检索?"
+ },
+ "dd70642b39": {
+ "days": [
+ "java-43"
+ ],
+ "points": "解析 URL,有未过期的缓存直接使用;DNS 解析得到 IP;TCP 三次握手,HTTPS 再做 TLS 握手;发送 HTTP 请求,经负载均衡或反向代理转给应用服务器,响应是重定向时对新地址重走一遍;浏览器解析 HTML 构建 DOM 树、解析 CSS 构建 CSSOM,合成渲染树后布局、绘制,遇到 CSS、JS、图片另发请求,普通脚本会暂停 HTML 的解析;连接保留一段时间供复用。",
+ "status": "active",
+ "text": "从输入 URL 到页面显示经历了哪些过程?"
+ },
+ "e1c4dd69ef": {
+ "days": [
+ "java-67"
+ ],
+ "points": "先验签,核对商户号、订单号、金额与本地订单一致,并确认交易状态是支付成功,再把三件事放在同一个事务中:以交易号为主键插入去重表,重复的通知报 1062 后回滚;用 UPDATE ... WHERE status IN ('UNPAID', 'CLOSING') 改为已支付,影响 1 行才继续;写入发货消息;重复的通知也要应答成功,否则平台会继续重发;先查状态再更新的写法,同一笔通知并发到达 10 次时会发 10 次货。",
+ "status": "active",
+ "text": "支付回调重复到达或并发到达时,怎样保证只发一次货?"
+ },
+ "e2b985d29c": {
+ "days": [
+ "java-13"
+ ],
+ "points": "Java 内存模型用先行发生关系定义可见性:A 先行发生于 B,A 的结果对 B 可见,不要求 A 在时间上先执行;规则有单一线程、管程锁定(unlock 先于之后对同一锁的 lock)、volatile 变量(写先于之后的读)、线程启动、线程加入、线程中断、对象终结和传递性;例如写 data 后再写 volatile 的 ready,读到 ready 的线程一定能看到 data。",
+ "status": "active",
+ "text": "什么是先行发生(happens-before)原则?"
+ },
+ "e2b9c83f1c": {
+ "days": [
+ "java-55"
+ ],
+ "points": "C 指线性一致性,A 指发给未故障节点的请求都能在有限时间内得到非错误响应,P 指出现网络分区时系统仍能运行;网络不可靠,分区无法避免,所以 P 必须满足,CAP 实际是分区发生时在 C 和 A 之间选择:拒绝请求(CP,如 ZooKeeper)或返回可能过期的数据(AP,如 Eureka);没有分区时 C 和 A 可以兼得。",
+ "status": "active",
+ "text": "什么是 CAP?为什么分区容忍性必须满足?"
+ },
+ "e2bec394b4": {
+ "days": [
+ "llm-04"
+ ],
+ "points": "预训练:在海量文本上预测下一个 Token,获得语言能力和知识,但只会续写;监督微调:用指令和人工编写的回答训练,学会按对话格式回答、遵循指令;偏好对齐:用“两个回答哪个更好”的数据让回答更有帮助、更安全,这类目标难写标准答案,但比较容易。",
+ "status": "active",
+ "text": "大模型的训练分为哪几个阶段?"
+ },
+ "e326fb1c48": {
+ "days": [
+ "java-14"
+ ],
+ "points": "悲观锁访问前先加锁,例如 synchronized、ReentrantLock、SELECT ... FOR UPDATE;乐观锁不加锁直接操作,提交时检查是否被改过,改过就重试,例如 CAS 和数据库版本号(UPDATE ... WHERE version = 1 影响 0 行时重试);读多写少、冲突少时用乐观锁,写多、冲突频繁时用悲观锁。",
+ "status": "active",
+ "text": "乐观锁和悲观锁有什么区别?"
+ },
+ "e36c204678": {
+ "days": [
+ "llm-20"
+ ],
+ "points": "接口用 SSE:响应类型为 text/event-stream,连接保持打开,每生成一段内容写一条事件;事件由若干行“字段: 值”组成、以空行结束,data 是内容、id 是编号、event 是类型,冒号开头的行是注释(常作心跳);OpenAI 兼容接口以 data: [DONE] 结束,断线后 EventSource 带着 Last-Event-ID 重连。",
+ "status": "active",
+ "text": "流式输出是怎样实现的?SSE 的格式是怎样的?"
+ },
+ "e4941b7494": {
+ "days": [
+ "sprint-02"
+ ],
+ "points": "参数有核心线程数、最大线程数、空闲线程的存活时间、任务队列、线程工厂和拒绝策略;提交时线程数小于核心数就新建线程,否则放入队列,队列满且小于最大线程数时新建线程执行当前任务,再不行就执行拒绝策略;例如核心 2、最大 4、队列 2 时第 7 个任务被拒绝;不用 Executors 的 newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool(),它们的队列或线程数没有上限,要直接创建 ThreadPoo",
+ "status": "active",
+ "text": "线程池的核心参数有哪些?提交任务后是怎样执行的?"
+ },
+ "e4952a889a": {
+ "days": [
+ "llm-03"
+ ],
+ "points": "因果掩码下前文 Token 的 K、V 不再变化,缓存起来复用就是 KV Cache;预填充一次算好输入的 K、V,解码每步只为新 Token 计算 Q、K、V,再与缓存做注意力,不再重算前文;代价是显存,Qwen2.5-7B 每个 Token 56 KB,32K Token 约 1.75 GB。",
+ "status": "active",
+ "text": "KV Cache 是什么?为什么能加快生成?"
+ },
+ "e4be6236bb": {
+ "days": [
+ "java-49"
+ ],
+ "points": "只能解决字段或 Setter 注入的单例:A 实例化后把生成提前引用的工厂放入三级缓存,注入 B 时 B 通过工厂拿到 A 的提前引用(放入二级缓存),A 初始化完成后进入一级缓存;需要第三级是因为 A 可能要被 AOP 代理,工厂被调用时才提前创建代理,保证 B 拿到的与容器中最终的对象相同;构造器注入和 prototype 的循环依赖无法解决,Spring Boot 2.6 起默认禁止。",
+ "status": "active",
+ "text": "Spring 怎样解决循环依赖?为什么需要三级缓存?"
+ },
+ "e554283074": {
+ "days": [
+ "java-59"
+ ],
+ "points": "每个分区有一个 Leader 和多个 Follower,与 Leader 保持同步的副本组成 ISR,Leader 故障时从 ISR 中选新 Leader;生产者设 acks=all,ISR 中的副本都写入才确认;配合 min.insync.replicas=2,ISR 不足时拒绝写入,常用 3 副本加这两项配置;幂等生产者用生产者 ID 和序号去掉重试造成的重复。",
+ "status": "active",
+ "text": "Kafka 的副本机制怎样保证可靠性?"
+ },
+ "e628ba7bc1": {
+ "days": [
+ "llm-04"
+ ],
+ "points": "标注员把同一提示词的 4~9 个回答按好坏排序,两两组成偏好对(9 个回答 36 对);奖励模型输入提示词和回答、输出一个分数,损失为 -log σ(rw - rl ),使较好回答得分更高,例如分数 1.2 和 0.3 时损失约 0.34。",
+ "status": "active",
+ "text": "RLHF 中的奖励模型是怎样训练的?"
+ },
+ "e6469eab7d": {
+ "days": [
+ "sprint-12"
+ ],
+ "points": "针对字段或 Setter 注入的单例 Bean,用三级缓存提前暴露未初始化完的对象:一级存初始化完成的 Bean,二级存提前暴露的 Bean,三级存生成提前引用的工厂;A 实例化后放入工厂,注入 B 时 B 从工厂拿到 A 的提前引用,A 需要 AOP 代理时工厂提前创建代理,使 B 拿到的就是最终对象;构造器注入和原型 Bean 无法解决,Spring Boot 2.6 起默认禁止循环依赖。",
+ "status": "active",
+ "text": "Spring 怎样解决循环依赖?"
+ },
+ "e8de3674b2": {
+ "days": [
+ "llm-20"
+ ],
+ "points": "前缀缓存降低的是单价:不变的系统提示词和工具定义放最前、工具顺序固定、历史原样追加;减少 Token 的数量:工具先返回摘要或片段,需要时再读全文;设置 max_tokens,输出单价是未命中缓存输入的 4 倍;简单问题交给小模型;历史过长时压缩,但压缩会改变前缀,不宜过于频繁;避免模型反复调用同一个工具。",
+ "status": "active",
+ "text": "怎样降低一次 Agent 任务的 Token 消耗?"
+ },
+ "e9083e85a7": {
+ "days": [
+ "llm-11"
+ ],
+ "points": "向量检索擅长匹配措辞不同的近义内容,但精确匹配不准,E1024 和 E1025 文档的相似度 0.762 与 0.743 几乎分不开;BM25 按词频、逆文档频率和文档长度打分,罕见词 E1024 权重高,两篇得分 1.088 与 0.130;两路同时召回,分数尺度不同,用 RRF 按排名融合;混合不一定比单路好,口语化问题上等权 RRF 的 Recall@3(0.950)低于单独的向量检索(0.975),BM25 的权重要在评测集上确定。",
+ "status": "active",
+ "text": "为什么要把向量检索和关键词检索结合起来?"
+ },
+ "e90dd83642": {
+ "days": [
+ "llm-06"
+ ],
+ "points": "幻觉指生成看似合理、但与事实或给定资料不符的内容,分事实性幻觉和忠实性幻觉;根本原因是训练目标是生成通顺的下一个 Token 而不是核对事实,只按答对比例打分的评测又鼓励猜测;此外还有知识缺失、采样出错后顺着错误生成、上下文过长或资料有噪声。",
+ "status": "active",
+ "text": "什么是大模型的幻觉?为什么会产生?"
+ },
+ "e93ce780bc": {
+ "days": [
+ "java-22"
+ ],
+ "points": "脏读是读到了其它事务还没有提交的数据,对方回滚后读到的就是不存在的值;不可重复读是同一事务两次读同一行结果不同,因为中间有事务修改并提交了这一行;幻读是两次读同一范围时行数变了,因为中间有事务插入了新行。",
+ "status": "active",
+ "text": "脏读、不可重复读和幻读有什么区别?"
+ },
+ "ea588f5661": {
+ "days": [
+ "java-44"
+ ],
+ "points": "进程是资源分配的基本单位,线程是调度的基本单位,同一进程的线程共享地址空间和文件描述符;创建进程的开销远大于线程,切换进程还要切换地址空间,开销更大;线程间直接读写共享数据,进程间要借助 IPC;进程相互隔离,一个线程段错误会终止整个进程。",
+ "status": "active",
+ "text": "进程和线程有什么区别?"
+ },
+ "eab615f7b3": {
+ "days": [
+ "java-50"
+ ],
+ "points": "不经过代理:自调用、对象不是 Spring 管理的;无法代理:CGLIB 无法重写 final、static、private 方法,JDK 动态代理只拦截接口中声明的方法;异常没有传到代理:被捕获后未重新抛出;不符合回滚规则:抛出受检异常而没配 rollbackFor;不在事务连接上:新线程中操作数据库、引擎不支持事务(MyISAM)。",
+ "status": "active",
+ "text": "哪些情况下 @Transactional 会失效?"
+ },
+ "eb8b8fb51f": {
+ "days": [
+ "java-42"
+ ],
+ "points": "二进制分帧:报文拆成 HEADERS 帧和 DATA 帧,一个 TCP 连接承载多个流,帧交错发送,不再需要多个连接;首部压缩:双方维护首部字段表并用 Huffman 编码;服务端推送(Chrome 106 起默认禁用);TCP 层的队头阻塞仍在,HTTP/3 改用基于 UDP 的 QUIC 解决。",
+ "status": "active",
+ "text": "HTTP/2 相比 HTTP/1.1 有哪些改进?"
+ },
+ "ec06220d69": {
+ "days": [
+ "java-68"
+ ],
+ "points": "普通调用是一问一答,模型根据输入生成文本,不能自行查询数据库或执行操作,外部资料要由应用放进输入;Agent 由模型决定下一步调用哪个工具,应用代码执行后把结果交还模型,如此循环直到任务完成;工具调用的过程:请求中附上工具的名称、用途和参数的 JSON Schema,推理服务按对话模板把它们拼进提示词,模型生成一段特定格式的文本,推理服务解析成 tool_calls 返回,应用执行后用 tool 角色的消息把结果发回;模型始终只输出文本,可能写错参数或编造订单号,所以执行前要",
+ "status": "active",
+ "text": "Agent 和普通的大模型调用有什么区别?模型是怎样调用工具的?"
+ },
+ "ed40759232": {
+ "days": [
+ "java-03"
+ ],
+ "points": "基本类型用 == 比较值,没有 equals();引用类型的 == 判断两个变量是否引用同一个对象,equals() 判断两个对象是否等价,例如两个 new Integer(1) 用 == 为 false、equals() 为 true。",
+ "status": "active",
+ "text": "== 和 equals() 有什么区别?"
+ },
+ "ef0a1e3f79": {
+ "days": [
+ "java-09"
+ ],
+ "points": "线程数小于核心线程数时新建线程执行,否则放入队列,队列满且线程数小于最大线程数时新建线程执行当前任务,都不满足就执行拒绝策略;先排队是因为线程比排队的任务昂贵:核心线程承担平时的负载,队列缓冲短时高峰;队列也满了说明负载持续超出处理能力,才临时增加线程,高峰过后空闲的线程被回收。",
+ "status": "active",
+ "text": "提交一个任务后,线程池是怎样处理的?为什么先放进队列,队列满了才创建新线程?"
+ },
+ "efa6604d75": {
+ "days": [
+ "sprint-03"
+ ],
+ "points": "无锁(Mark Word 末尾 01);轻量级锁(00):线程用 CAS 把 Mark Word 换成指向栈中 Lock Record 的指针,重入只多放一个 Lock Record;重量级锁(10):其它线程 CAS 失败时膨胀为 ObjectMonitor,先自适应自旋,仍抢不到再阻塞;JDK 15 起默认禁用偏向锁,空闲的 ObjectMonitor 会被回收,对象恢复为无锁。",
+ "status": "active",
+ "text": "synchronized 的锁升级过程是怎样的?"
+ },
+ "efe411f83c": {
+ "days": [
+ "java-69"
+ ],
+ "points": "同一个工具要接入多个 AI 应用时,原本要在每个应用里分别适配;MCP 定义了 AI 应用与外部工具、数据之间的统一接口,工具实现一次服务端就能被所有支持 MCP 的应用使用;三种角色是主机、客户端(一个客户端只连接一个服务端)和服务端,服务端提供工具、资源和提示词模板,本地用 stdio 通信,远程用 Streamable HTTP;Function Calling 是模型一侧的能力,模型根据工具描述给出调用,MCP 是应用与工具之间的协议,解决工具怎样被发现和调用,两者配",
+ "status": "active",
+ "text": "MCP 解决了什么问题?它和 Function Calling 有什么区别?"
+ },
+ "f2d13da1ee": {
+ "days": [
+ "llm-10"
+ ],
+ "points": "把向量组织成多层图,每个向量连接附近的若干向量,层越高向量越少、连线跨度越大,最下层包含全部向量;查询从最上层的入口开始,上面各层只保留一个最近节点,贪心地走向查询,走不动就下降一层,最下层保留 efSearch 个候选扩展邻居,从中取前 K 个,思路类似跳跃表;上层的长连线让查询几步就到目标附近,12.4 万个向量上每次只算约 1300 次距离(暴力检索的 1.1%);过滤掉大部分向量、大量删除和内存放不下时会变差;M、efConstruction、efSearch 在速度",
+ "status": "active",
+ "text": "HNSW 的原理是什么?"
+ },
+ "f2ed00f34c": {
+ "days": [
+ "llm-05"
+ ],
+ "points": "需要私有资料或经常更新的知识、要给出依据时用 RAG;要固定风格和格式、学会某种专业任务,提示词仍常出错时用微调;只是补少量规则和示例时用提示词;一般先提示词、再 RAG、最后微调,写进参数的知识难更新、无法给出来源。",
+ "status": "active",
+ "text": "什么时候用微调,什么时候用 RAG?"
+ },
+ "f308884a22": {
+ "days": [
+ "java-40"
+ ],
+ "points": "强缓存:在 max-age(或 Expires)规定的有效期内直接使用本地缓存,不发请求;协商缓存:缓存过期或响应带 no-cache 时,带上 If-None-Match 或 If-Modified-Since 向服务器确认,没变化返回 304、只传首部;常见做法是 HTML 用 no-cache,带内容哈希的 JS、CSS 设很长的 max-age。",
+ "status": "active",
+ "text": "强缓存和协商缓存有什么区别?"
+ },
+ "f311870155": {
+ "days": [
+ "java-25"
+ ],
+ "points": "主键索引一张表只有一个,列不能为 NULL,在 InnoDB 中就是聚簇索引;唯一索引的值不能重复,但可以有多个 NULL,普通索引没有唯一约束,两者在 InnoDB 中通常是二级索引;查询速度差别很小,唯一索引等值查询找到一条就停止,普通索引要多读一条;写入时普通索引可以用 change buffer,唯一索引要先读出页检查是否重复。",
+ "status": "active",
+ "text": "主键索引、唯一索引和普通索引有什么区别?"
+ },
+ "f319983c2c": {
+ "days": [
+ "llm-04"
+ ],
+ "points": "PPO 在线生成回答,奖励模型打分、价值模型给出预期,加载 4 个模型、训练 2 个;DPO 不训练奖励模型、不在训练中生成回答,直接用离线偏好对训练,只需策略模型和参考模型,简单稳定;GRPO 去掉价值模型,每题生成一组回答、以组内平均分为预期,适合能用规则判对错的推理训练;PPO 和 GRPO 能探索出训练数据之外的更好回答。",
+ "status": "active",
+ "text": "PPO、DPO 和 GRPO 有什么区别?"
+ },
+ "f38768f09b": {
+ "days": [
+ "java-64"
+ ],
+ "points": "虚拟机模拟硬件并运行完整的操作系统,启动慢,占用大量资源,一台机器只能运行几十个;容器是宿主机上用命名空间隔离、用 cgroup 限制资源的普通进程,启动快,一台机器可运行成千上万个;容器共享宿主机内核,更轻量,但隔离性更弱。",
+ "status": "active",
+ "text": "Docker 和虚拟机有什么区别?"
+ },
+ "f399525472": {
+ "days": [
+ "java-28"
+ ],
+ "points": "InnoDB 支持事务和外键,MyISAM 不支持;MyISAM 只有表锁,InnoDB 还支持行锁;InnoDB 支持在线热备份,MyISAM 崩溃后更容易损坏,恢复也更慢;InnoDB 的主键索引是聚簇索引,MyISAM 的索引都是非聚簇索引,叶子节点保存行的地址。",
+ "status": "active",
+ "text": "InnoDB 和 MyISAM 有什么区别?"
+ },
+ "f3cbce1b55": {
+ "days": [
+ "java-10",
+ "sprint-02"
+ ],
+ "points": "synchronized 由 JVM 实现,ReentrantLock 由 JDK 基于 AQS 实现,性能大致相同;ReentrantLock 支持等待可中断、公平锁和绑定多个 Condition,synchronized 都不支持;ReentrantLock 必须在 finally 中 unlock(),synchronized 由 JVM 保证释放;不需要这些功能时优先用 synchronized,但 JDK 21 的虚拟线程在 synchronized 中阻塞会固定载",
+ "status": "active",
+ "text": "synchronized 和 ReentrantLock 有什么区别?"
+ },
+ "f5bc9de8b8": {
+ "days": [
+ "java-61"
+ ],
+ "points": "金额用整数分表示;二倍均值法:剩余 R 分、还有 k 份时,本份在 1 到 2R ÷ k − 1 分之间均匀随机,最后一份拿走剩余全部,每份至少 1 分;每个位置的期望相同,越靠后波动越大;可以发红包时预先拆好放入 Redis 列表,抢时 LPOP。",
+ "status": "active",
+ "text": "抢红包时金额怎样拆分?"
+ },
+ "f62bbcd15d": {
+ "days": [
+ "sprint-14"
+ ],
+ "points": "组成:模型决定下一步,工具、记忆、规划提供能力,控制循环执行工具并限制步数、校验参数和权限;Function Calling:请求附上工具的名称、用途和参数 JSON Schema,推理服务拼进提示词,模型需要时生成特定格式的文本,服务解析为 tool_calls 返回;应用校验后执行,以 tool 消息和 tool_call_id 发回结果,本质仍是生成文本,必须先校验。",
+ "status": "active",
+ "text": "Agent 由哪些部分组成?Function Calling 的原理是什么?"
+ },
+ "f76f14f53c": {
+ "days": [
+ "java-53"
+ ],
+ "points": "Spring Cloud LoadBalancer 默认轮询,也可以改为随机,两者都不考虑机器差异;启用 NacosLoadBalancer 后优先选同集群的实例,再按权重随机(权重 3:1 时 400 次约为 303:97);还可以实现 ReactorServiceInstanceLoadBalancer 自定义规则。",
+ "status": "active",
+ "text": "有哪些负载均衡策略?"
+ },
+ "f79e6d406d": {
+ "days": [
+ "java-38"
+ ],
+ "points": "请求报文:请求行(方法、URL、协议版本)、请求首部、空行、请求主体;响应报文:状态行(协议版本、状态码、原因短语)、响应首部、空行、响应主体。",
+ "status": "active",
+ "text": "HTTP 请求报文和响应报文由哪几部分组成?"
+ },
+ "f806988135": {
+ "days": [
+ "java-21"
+ ],
+ "points": "top 找到 CPU 占用高的 Java 进程;top -H -p \\ 找到占用最高的线程 ID;jstack \\ 导出线程栈,按 nid 找到这个线程(JDK 18 前 nid 是十六进制,要先用 printf \"%x\" 转换);查看它正在执行的代码,常见是死循环或频繁 GC。",
+ "status": "active",
+ "text": "CPU 占用过高怎样定位到具体的代码?"
+ },
+ "f87401d466": {
+ "days": [
+ "sprint-10"
+ ],
+ "points": "握手:A 发 SYN(序号 x),B 回 SYN + ACK(确认号 x + 1、序号 y),A 回 ACK(确认号 y + 1),双方进入 ESTABLISHED,三次是为了让双方的初始序号都得到确认,并防止滞留的旧请求建立连接;挥手:A 发 FIN,B 确认后进入 CLOSE_WAIT 发完数据再发 FIN,A 确认后进入 TIME_WAIT;等 2MSL 一是确保最后的 ACK 能到达(丢失时 B 会重传 FIN),二是让本连接的旧报文从网络中消失。",
+ "status": "active",
+ "text": "三次握手和四次挥手的过程是怎样的?为什么 TIME_WAIT 要等 2MSL?"
+ },
+ "f9317cc094": {
+ "days": [
+ "java-19"
+ ],
+ "points": "经过多次 Minor GC 后年龄达到 MaxTenuringThreshold;动态年龄判定(以 Serial 为例,G1 和 Parallel 的计算方式不同):Minor GC 结束时从小到大累加 Survivor 中各年龄对象的大小,超过 Survivor 一半时,取这个年龄和 MaxTenuringThreshold 中较小的值作为新阈值,下一次 Minor GC 时大于或等于它的对象晋升;Minor GC 时 Survivor 放不下的存活对象直接进入老年代;大对",
+ "status": "active",
+ "text": "对象在什么情况下进入老年代?"
+ },
+ "f9d6fbbf46": {
+ "days": [
+ "java-39"
+ ],
+ "points": "HttpOnly 禁止 JavaScript 读取,减少 XSS 窃取;Secure 只通过 HTTPS 发送;SameSite 限制跨站请求携带(Strict 都不带,Lax 只允许顶级导航的 GET,None 都带但必须同时设置 Secure),防范 CSRF;Domain、Path 限定发送范围,Expires、Max-Age 控制有效期。",
+ "status": "active",
+ "text": "Cookie 有哪些和安全相关的属性?"
+ },
+ "fa62f2e19c": {
+ "days": [
+ "java-60"
+ ],
+ "points": "限流保护自己,拒绝超出处理能力的请求;熔断针对下游,持续失败时暂停调用,避免线程被拖住、故障扩散;降级决定这时返回什么,如缓存、默认值或关闭非核心功能;熔断器失败率达到阈值时打开,打开一段时间后的下一次调用使其进入半开,放行少量试探请求,失败率低于阈值则关闭,否则重新打开。",
+ "status": "active",
+ "text": "限流、熔断和降级有什么区别?熔断器怎样从打开恢复到关闭?"
+ },
+ "fa683c8ea1": {
+ "days": [
+ "llm-05"
+ ],
+ "points": "模型在新数据上训练后原有能力明显下降,例如只用客服对话微调后写代码变差,1B~7B 模型的实验中模型越大越严重;缓解:混入通用指令数据,用 LoRA、较小的学习率和较少的轮数并早停,训练前后都在通用评测集上评测。",
+ "status": "active",
+ "text": "什么是灾难性遗忘?怎样缓解?"
+ },
+ "fb32f47b96": {
+ "days": [
+ "java-32",
+ "sprint-08"
+ ],
+ "points": "RDB 由 fork 出的子进程把某一时刻的全部数据写入紧凑的二进制文件,适合备份、恢复快,但会丢失最后一次快照之后的数据,可能有几分钟;AOF 把写命令追加到文件,everysec 时约丢 1 秒,文件更大,需要定期重写;开启 AOF 后,Redis 5.0 起默认使用混合持久化,重写生成的基础文件用 RDB 格式;数据重要时两种都开启,重启时用 AOF 恢复。",
+ "status": "active",
+ "text": "RDB 和 AOF 有什么区别?"
+ },
+ "fb5644c7bf": {
+ "days": [
+ "java-11"
+ ],
+ "points": "用共享变量记录轮到哪个线程,没轮到的线程 wait(),轮到的线程打印后改变轮次并 notifyAll();等待条件用 while 而不是 if,被唤醒时要重新检查是否轮到自己;也可以用两个 Semaphore 传递许可,第一个初始为 1、第二个为 0,打印后释放对方的许可。",
+ "status": "active",
+ "text": "两个线程交替打印 1~100 怎样实现?"
+ },
+ "fc65cb524a": {
+ "days": [
+ "java-22"
+ ],
+ "points": "全局读锁(FLUSH TABLES WITH READ LOCK);表级的表锁、元数据锁(MDL)、意向锁和自增锁;行级的记录锁、间隙锁、Next-Key Lock 和插入意向锁,加在索引上;意向锁 IS、IX 是表锁,加行锁前先加,要锁整张表时只需检查表上有没有意向锁,不必逐行检查。",
+ "status": "active",
+ "text": "MySQL 中有哪些锁?意向锁有什么用?"
+ },
+ "fd090d3343": {
+ "days": [
+ "java-38"
+ ],
+ "points": "PUT 创建或完全替换目标资源;PATCH 只对资源做部分修改。",
+ "status": "active",
+ "text": "PUT 和 PATCH 有什么区别?"
+ },
+ "fd1182bd45": {
+ "days": [
+ "java-28"
+ ],
+ "points": "主库把修改写入 binlog,由 binlog dump 线程发给从库;从库的 I/O 线程写入中继日志,SQL 线程读取并重放,8.0.27 起默认 4 个工作线程并行重放;读写分离的问题是默认异步复制带来的复制延迟,刚写完就读从库可能读到旧数据;写后的读和实时性要求高的读走主库,或者写入后记下 GTID,读从库前用 WAIT_FOR_EXECUTED_GTID_SET 等从库执行完,再在新开始的事务或快照中读取,超时就改读主库;监控延迟,延迟过大的从库暂停分配读请求;半同",
+ "status": "active",
+ "text": "主从复制的过程是怎样的?读写分离有什么问题?"
+ },
+ "fddac344d2": {
+ "days": [
+ "java-27"
+ ],
+ "points": "LIMIT 1000000, 10 要先找出前 1000010 行再丢弃前 100 万行,走二级索引要回表上百万次,优化器改为全表扫描加排序,200 万行约 1.5 秒;延迟关联先在覆盖索引上查出这一页的主键,只回表 10 次,约 81 ms;游标分页记住上一页最后一行的排序值和主键,从它之后取 10 行,约 0.2 ms,但不能跳页;也可以限制翻页深度。",
+ "status": "active",
+ "text": "深分页为什么慢?怎样优化?"
+ }
+ },
+ "routes": {
+ "java": {
+ "status": "active",
+ "title": "Java 后端"
+ },
+ "llm": {
+ "status": "active",
+ "title": "大模型应用开发"
+ },
+ "sprint": {
+ "status": "active",
+ "title": "面试冲刺"
+ }
+ },
+ "version": 1
+}
diff --git a/learn/content.py b/learn/content.py
new file mode 100644
index 000000000..be54df9af
--- /dev/null
+++ b/learn/content.py
@@ -0,0 +1,211 @@
+"""题目和天的稳定编号。
+
+读者的复习记录按题目编号保存、完成情况按天的编号保存,所以笔记更新后编号要尽量不变:
+- 文字没变的题沿用原编号;改了措辞的题按文字和参考要点的相似度找回原编号(记为修改);
+- 新增的题用文字的哈希作编号,与 learn/build.py 早期的规则一致;
+- 从路线中删掉的题和天标记为下线,编号不再分配给别的内容,重新加回来时恢复原编号;
+- 天先按标题对应,标题改了再按题目的重合程度对应;在路线中间插入一天不会让后面的天换编号。
+
+对应关系保存在 content.lock.json 中并提交到仓库,构建只读取它;改了路线后在本机运行 learn/maintain.py 查看变化并写入。
+"""
+from __future__ import annotations
+
+import difflib
+import hashlib
+import json
+import re
+from pathlib import Path
+
+LOCK_VERSION = 1
+PUNCT = re.compile(r'[\s,。、;:?!“”‘’()()《》,.;:?!"\'`·\-—…]+')
+# 相似度阈值:题目文字 0.55 以上,或参考要点 0.7 以上,才认为是同一道题改了写法
+TEXT_SIMILAR, POINTS_SIMILAR = 0.55, 0.7
+
+
+def normalize(text: str) -> str:
+ return PUNCT.sub('', text).lower()
+
+
+def similarity(a: str, b: str) -> float:
+ a, b = normalize(a), normalize(b)
+ if not a or not b:
+ return 0.0
+ return difflib.SequenceMatcher(None, a, b, autojunk=False).ratio()
+
+
+def text_id(text: str, taken=()) -> str:
+ base = re.sub(r'\s+', '', text)
+ qid, salt = hashlib.sha1(base.encode()).hexdigest()[:10], 0
+ while qid in taken:
+ salt += 1
+ qid = hashlib.sha1(f'{base}#{salt}'.encode()).hexdigest()[:10]
+ return qid
+
+
+def empty_lock():
+ return {'version': LOCK_VERSION, 'questions': {}, 'days': {}, 'routes': {}, 'history': [], 'manual': {'map': {}, 'separate': []}}
+
+
+def load_lock(path: Path):
+ if not path.is_file():
+ return empty_lock()
+ lock = json.loads(path.read_text())
+ if lock.get('version') != LOCK_VERSION:
+ raise SystemExit(f'{path} 的版本是 {lock.get("version")},当前只支持 {LOCK_VERSION}')
+ for key, value in empty_lock().items():
+ lock.setdefault(key, value)
+ return lock
+
+
+def resolve(routes, days, questions, lock):
+ """把解析得到的临时编号换成稳定编号。
+
+ routes:[{id, stages: [{title, days: [临时天编号]}]}],按路线顺序;
+ days:{临时天编号: {route, title, questions: [临时题号]}};
+ questions:{临时题号: {text, points}},临时题号就是文字的哈希。
+ 返回 (题号对照, 天编号对照, 变化, 新的 lock);变化中的编号都是稳定编号。
+ """
+ old_q, old_d = lock['questions'], lock['days']
+ manual = lock.get('manual', {})
+ qmap, dmap, claimed_q, claimed_d = {}, {}, set(), set()
+ edited_q, edited_d = [], []
+
+ # 1. 题目:文字相同的直接对应;手工指定的对应优先
+ by_text = {}
+ for qid, entry in old_q.items():
+ by_text.setdefault(normalize(entry['text']), qid)
+ for tmp, q in questions.items():
+ forced = manual.get('map', {}).get(tmp)
+ if forced in old_q and forced not in claimed_q:
+ qmap[tmp] = forced
+ elif tmp not in manual.get('separate', []):
+ hit = by_text.get(normalize(q['text']))
+ if hit and hit not in claimed_q:
+ qmap[tmp] = hit
+ if tmp in qmap:
+ claimed_q.add(qmap[tmp])
+ old = old_q[qmap[tmp]]
+ # 问法或参考要点有改动都算修改,读者可以在更新说明里看到
+ if normalize(old['text']) != normalize(q['text']) or normalize(old.get('points', '')) != normalize(q.get('points', '')[:240]):
+ edited_q.append(qmap[tmp])
+
+ # 2. 天:同一路线中标题相同的对应,否则按已经对应上的题目重合度对应
+ for route in routes:
+ rid = route['id']
+ pending = []
+ candidates = {d: e for d, e in old_d.items() if e['route'] == rid}
+ for stage in route['stages']:
+ for tmp in stage['days']:
+ title = days[tmp]['title']
+ hit = next((d for d, e in candidates.items() if d not in claimed_d and e['title'] == title), None)
+ if hit:
+ dmap[tmp] = hit
+ claimed_d.add(hit)
+ else:
+ pending.append(tmp)
+ for tmp in pending:
+ mine = {qmap[q] for q in days[tmp]['questions'] if q in qmap}
+ best, best_score = None, 0.0
+ for d, e in candidates.items():
+ if d in claimed_d:
+ continue
+ theirs = set(e.get('questions', []))
+ score = len(mine & theirs) / len(mine | theirs) if mine | theirs else 0
+ if score > best_score:
+ best, best_score = d, score
+ if best and best_score >= 0.5:
+ dmap[tmp] = best
+ claimed_d.add(best)
+ edited_d.append(best)
+ # 新的天:编号接着这条路线用过的最大序号,下线的编号不复用
+ used = [int(m[1]) for d in [*old_d, *dmap.values()] if (m := re.match(rf'^{re.escape(rid)}-(\d+)$', d))]
+ next_number = max(used, default=0) + 1
+ for stage in route['stages']:
+ for tmp in stage['days']:
+ if tmp not in dmap:
+ dmap[tmp] = f'{rid}-{next_number:02d}'
+ next_number += 1
+
+ # 3. 题目:剩下的按文字和要点的相似度对应,同一天里的题加分;从最相似的一对开始
+ pairs = []
+ for tmp, q in questions.items():
+ if tmp in qmap or tmp in manual.get('separate', []):
+ continue
+ here = {dmap[d] for d in q.get('days', [])}
+ for qid, entry in old_q.items():
+ if qid in claimed_q:
+ continue
+ ts, ps = similarity(q['text'], entry['text']), similarity(q.get('points', ''), entry.get('points', ''))
+ if ts < TEXT_SIMILAR and ps < POINTS_SIMILAR:
+ continue
+ bonus = 0.1 if here & set(entry.get('days', [])) else 0
+ pairs.append((max(ts, ps * 0.9) + bonus, tmp, qid))
+ for _, tmp, qid in sorted(pairs, reverse=True):
+ if tmp in qmap or qid in claimed_q:
+ continue
+ qmap[tmp] = qid
+ claimed_q.add(qid)
+ edited_q.append(qid)
+
+ # 4. 新题
+ taken = set(old_q) | set(qmap.values())
+ added_q = []
+ for tmp, q in questions.items():
+ if tmp not in qmap:
+ qmap[tmp] = text_id(q['text'], taken)
+ taken.add(qmap[tmp])
+ added_q.append(qmap[tmp])
+
+ # 新的 lock:现在的内容都是 active,没对应上的旧条目下线
+ new = {'version': LOCK_VERSION, 'questions': {}, 'days': {}, 'routes': {}, 'history': lock.get('history', []), 'manual': manual}
+ for tmp, q in questions.items():
+ qid = qmap[tmp]
+ new['questions'][qid] = {'text': q['text'], 'points': q.get('points', '')[:240],
+ 'days': sorted({dmap[d] for d in q.get('days', [])}), 'status': 'active'}
+ reactivated_q = [qid for qid in new['questions'] if old_q.get(qid, {}).get('status') == 'retired' and qid not in edited_q]
+ retired_q = [qid for qid, e in old_q.items() if e.get('status') == 'active' and qid not in new['questions']]
+ for qid in retired_q + [q for q, e in old_q.items() if e.get('status') == 'retired' and q not in new['questions']]:
+ new['questions'][qid] = {**old_q[qid], 'status': 'retired'}
+ for route in routes:
+ for stage in route['stages']:
+ for tmp in stage['days']:
+ did = dmap[tmp]
+ new['days'][did] = {'route': route['id'], 'title': days[tmp]['title'], 'stage': stage['title'],
+ 'questions': [qmap[q] for q in days[tmp]['questions']], 'status': 'active'}
+ # 导读和阅读范围的指纹:变了说明这一天要读的内容有调整,学过的读者会看到提示;旧的 lock 中没有指纹时只记下,不算修改
+ if days[tmp].get('sig'):
+ new['days'][did]['sig'] = days[tmp]['sig']
+ old_sig = old_d.get(did, {}).get('sig')
+ if old_sig and old_sig != days[tmp]['sig'] and old_d[did].get('status') == 'active':
+ edited_d.append(did)
+ added_d = [d for d in new['days'] if d not in old_d or old_d[d].get('status') == 'retired']
+ edited_d = [d for d in edited_d if d in new['days'] and d not in added_d]
+ retired_d = [d for d, e in old_d.items() if e.get('status') == 'active' and d not in new['days']]
+ for did, entry in old_d.items():
+ if did not in new['days']:
+ new['days'][did] = {**entry, 'status': 'retired'}
+ for route in routes:
+ new['routes'][route['id']] = {'title': route['title'], 'status': 'active'}
+ for rid, entry in lock.get('routes', {}).items():
+ if rid not in new['routes']:
+ new['routes'][rid] = {**entry, 'status': 'retired'}
+ current = {r['id'] for r in routes}
+ retired_r = [r for r, e in lock.get('routes', {}).items() if e.get('status') == 'active' and r not in current]
+ added_r = [r['id'] for r in routes if lock.get('routes', {}).get(r['id'], {}).get('status') != 'active']
+ changes = {
+ 'added': {'questions': added_q + reactivated_q, 'days': added_d, 'routes': added_r},
+ 'edited': {'questions': sorted(set(edited_q)), 'days': sorted(set(edited_d))},
+ 'retired': {'questions': retired_q, 'days': retired_d, 'routes': retired_r},
+ }
+ return qmap, dmap, changes, new
+
+
+def has_changes(changes) -> bool:
+ return any(v for group in changes.values() for v in group.values())
+
+
+def record(lock, changes, date: str):
+ """把一次变化记入历史(maintain.py 写入 lock 时调用)。"""
+ if has_changes(changes):
+ lock['history'].append({'date': date, **changes})
+ return lock
diff --git a/learn/maintain.py b/learn/maintain.py
new file mode 100644
index 000000000..4c2f9299c
--- /dev/null
+++ b/learn/maintain.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+"""学习站的日常维护:改了学习路线或笔记后,查看学习站的内容有哪些变化,确认后写入 content.lock.json。
+
+用法(在仓库根目录):
+ python3 learn/maintain.py 查看变化:新增、修改、下线的题和天,链接问题,没有放进学习站的路线文件
+ python3 learn/maintain.py --write 确认无误后写入 lock,并把这次变化记入更新记录(读者在首页看到)
+ python3 learn/maintain.py --map 新=旧 自动判断错了:把新题(临时编号)当作旧题的修改,沿用旧编号和复习记录
+ python3 learn/maintain.py --separate 新 自动判断错了:新题和相似的旧题是两道题,旧题下线
+ python3 learn/maintain.py --init 第一次使用:按现在的内容建立 lock,不记更新
+ python3 learn/maintain.py --find 题号 处理读者反馈:按题号找到题目、所在的天和路线文件
+ python3 learn/maintain.py --baseline 还没发布过时:写入现在的内容并清空更新记录,第一次发布时读者不会看到“内容更新”;编号照常沿用
+--map 和 --separate 写入 lock 的 manual 中,之后的构建都按它处理;可以和 --write 一起用。
+发现问题时退出码为 1(严格检查失败,或有链接、题目问题),可以放在提交前的检查中。
+"""
+from __future__ import annotations
+
+import argparse
+import datetime
+import json
+import sys
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+sys.path.insert(0, str(HERE))
+import build # noqa: E402
+import content # noqa: E402
+
+
+def describe(changes, old_lock, new_lock, questions, tmp_of):
+ lines = []
+ q_text = lambda lock, q: lock['questions'].get(q, {}).get('text', q) # noqa: E731
+ d_text = lambda lock, d: f"{d}({lock['days'].get(d, {}).get('title', '')})" # noqa: E731
+ for q in changes['added']['questions']:
+ state = '恢复' if old_lock['questions'].get(q, {}).get('status') == 'retired' else '新增'
+ lines.append(f' {state}题目 {q}:{q_text(new_lock, q)}')
+ for q in changes['edited']['questions']:
+ old, new = old_lock['questions'][q], new_lock['questions'][q]
+ same = content.normalize(old['text']) == content.normalize(new['text'])
+ if same:
+ lines.append(f' 修改要点 {q}:{new["text"]}')
+ else:
+ score = content.similarity(old['text'], new['text'])
+ lines.append(f' 修改题目 {q}(文字相似度 {score:.2f},新题临时编号 {tmp_of.get(q, "?")}):\n 原:{old["text"]}\n 新:{new["text"]}')
+ for q in changes['retired']['questions']:
+ lines.append(f' 下线题目 {q}:{q_text(old_lock, q)}')
+ for d in changes['added']['days']:
+ lines.append(f' 新增天 {d_text(new_lock, d)}')
+ for d in changes['edited']['days']:
+ old_t, new_t = old_lock['days'][d]['title'], new_lock['days'][d]['title']
+ lines.append(f" 修改天 {d}:{old_t} → {new_t}" if old_t != new_t else f" 修改天 {d}({new_t}):导读或阅读范围有调整,学过这一天的读者会看到提示")
+ for d in changes['retired']['days']:
+ lines.append(f' 下线天 {d_text(old_lock, d)}')
+ for r in changes['added']['routes']:
+ lines.append(f' 新增路线 {r}')
+ for r in changes['retired']['routes']:
+ lines.append(f' 下线路线 {r}(读者会看到“这条路线已下线”,记录保留)')
+ return lines
+
+
+def main():
+ parser = argparse.ArgumentParser(description='查看并写入学习站的内容变化')
+ parser.add_argument('--source', type=Path, default=build.REPO)
+ parser.add_argument('--lock', type=Path, default=build.LOCK)
+ parser.add_argument('--routes', type=Path, default=build.CONFIG)
+ parser.add_argument('--write', action='store_true')
+ parser.add_argument('--init', action='store_true')
+ parser.add_argument('--baseline', action='store_true')
+ parser.add_argument('--find', metavar='题号')
+ parser.add_argument('--date', default=datetime.date.today().isoformat())
+ parser.add_argument('--map', action='append', default=[], metavar='新=旧')
+ parser.add_argument('--separate', action='append', default=[], metavar='新')
+ args = parser.parse_args()
+
+ lock = content.load_lock(args.lock)
+ if args.find:
+ entry = lock['questions'].get(args.find)
+ if not entry:
+ print(f'lock 中没有题号 {args.find}')
+ return 1
+ config = json.loads(args.routes.read_text())
+ files = {r['id']: r['file'] for r in config['routes']}
+ print(f"{args.find}({'在用' if entry['status'] == 'active' else '已下线'}):{entry['text']}")
+ for did in entry.get('days', []):
+ d = lock['days'].get(did, {})
+ print(f" {did}:{d.get('title', '')},在 notes/{files.get(d.get('route'), '?')}.md 中")
+ return 0
+ for pair in args.map:
+ new, _, old = pair.partition('=')
+ lock['manual']['map'][new.strip()] = old.strip()
+ for tmp in args.separate:
+ if tmp not in lock['manual']['separate']:
+ lock['manual']['separate'].append(tmp)
+
+ # 严格检查一遍,再宽松解析一遍收集全部警告
+ problems = []
+ try:
+ routes, days, questions, notes = build.parse(args.source, '/#/notes/', config_path=args.routes)
+ except SystemExit as error:
+ print(error)
+ routes, days, questions, notes = build.parse(args.source, '/#/notes/', lenient=True, config_path=args.routes)
+ problems = notes.warnings
+ tmp_ids = {q: e for q, e in questions.items()}
+ old_lock = json.loads(json.dumps(lock))
+ routes, days, questions, changes, new_lock = build.stabilize(routes, days, questions, lock)
+ tmp_of = {}
+ for tmp, entry in tmp_ids.items():
+ stable = next((q for q, e in questions.items() if e['text'] == entry['text']), None)
+ if stable:
+ tmp_of[stable] = tmp
+
+ config = json.loads(args.routes.read_text())
+ listed = {r['file'] for r in config['routes']} | set(config.get('ignore', []))
+ unlisted = sorted(p.stem for p in (args.source / 'notes').glob('学习路线 - *.md') if p.stem not in listed)
+
+ total = len(questions)
+ print(f'学习站:{len(routes)} 条路线、{len(days)} 天、{total} 道题。')
+ if args.init:
+ if old_lock['questions']:
+ raise SystemExit('lock 中已有内容,不能重新初始化;要重建请先删除 ' + str(args.lock))
+ args.lock.write_text(json.dumps(new_lock, ensure_ascii=False, indent=1, sort_keys=True) + '\n')
+ print(f'已建立 {args.lock.name}。')
+ return 0
+ lines = describe(changes, old_lock, new_lock, questions, tmp_of)
+ print('与 lock 相比的变化:' if lines else '与 lock 相比没有变化。')
+ print('\n'.join(lines))
+ for name in unlisted:
+ print(f' 提示:{name}.md 没有放进学习站,需要的话在 learn/routes.json 中加上,不需要的加到 ignore。')
+ for where, href, why in problems:
+ print(f' 问题:{where}:{href}({why})')
+ if args.baseline:
+ if problems:
+ print('有问题没有解决,没有写入。')
+ return 1
+ new_lock['history'] = []
+ args.lock.write_text(json.dumps(new_lock, ensure_ascii=False, indent=1, sort_keys=True) + '\n')
+ print(f'已写入 {args.lock.name},更新记录已清空(只在第一次发布前使用)。')
+ return 0
+ if args.write:
+ if problems:
+ print('有问题没有解决,没有写入。')
+ return 1
+ content.record(new_lock, changes, args.date)
+ args.lock.write_text(json.dumps(new_lock, ensure_ascii=False, indent=1, sort_keys=True) + '\n')
+ print(f'已写入 {args.lock.name}' + (f',更新记录第 {len(new_lock["history"])} 条({args.date})。' if content.has_changes(changes) else '。'))
+ elif lines:
+ print('确认无误后运行 python3 learn/maintain.py --write。')
+ return 1 if problems else 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/learn/make_images.py b/learn/make_images.py
new file mode 100644
index 000000000..907db4131
--- /dev/null
+++ b/learn/make_images.py
@@ -0,0 +1,69 @@
+"""生成学习站的图标和链接预览图(src/icons/、src/og.png),改了配色或文字后重新运行:python3 learn/make_images.py"""
+from pathlib import Path
+from PIL import Image, ImageDraw, ImageFont
+
+HERE = Path(__file__).resolve().parent
+THEME, BG, INK, MUTED, SOFT = (91, 91, 214), (246, 246, 248), (21, 21, 28), (94, 94, 106), (238, 238, 253)
+CJK = '/System/Library/Fonts/Hiragino Sans GB.ttc'
+LATIN = '/System/Library/Fonts/Helvetica.ttc'
+
+
+def icon(size, maskable=False):
+ # 绿色底上一张白色题卡,卡上三行“要点”和一个对勾;maskable 版本留出安全区
+ scale = 4
+ s = size * scale
+ img = Image.new('RGB', (s, s), THEME)
+ d = ImageDraw.Draw(img)
+ pad = s * (0.2 if maskable else 0.14)
+ if not maskable:
+ img = Image.new('RGBA', (s, s), (0, 0, 0, 0))
+ d = ImageDraw.Draw(img)
+ d.rounded_rectangle([0, 0, s - 1, s - 1], radius=s * 0.22, fill=THEME)
+ x0, y0, x1, y1 = pad, pad * 1.1, s - pad, s - pad * 1.1
+ d.rounded_rectangle([x0, y0, x1, y1], radius=s * 0.07, fill=(255, 255, 255))
+ w = x1 - x0
+ for i, frac in enumerate((0.62, 0.48, 0.38)):
+ y = y0 + (y1 - y0) * (0.26 + i * 0.2)
+ d.rounded_rectangle([x0 + w * 0.14, y, x0 + w * (0.14 + frac), y + s * 0.045], radius=s * 0.0225, fill=SOFT if i else (201, 201, 245))
+ cx, cy = x0 + w * 0.72, y0 + (y1 - y0) * 0.7
+ d.line([(cx - s * 0.07, cy), (cx - s * 0.02, cy + s * 0.05), (cx + s * 0.08, cy - s * 0.06)], fill=THEME, width=int(s * 0.04), joint='curve')
+ return img.resize((size, size), Image.LANCZOS)
+
+
+def og(count):
+ w, h = 1200, 630
+ img = Image.new('RGB', (w, h), BG)
+ d = ImageDraw.Draw(img)
+ d.rounded_rectangle([40, 40, w - 40, h - 40], radius=28, fill=(255, 255, 255), outline=(228, 228, 234), width=2)
+ img.paste(icon(96), (96, 96), icon(96))
+ d.text((216, 112), 'CS-Notes 学习', font=ImageFont.truetype(CJK, 40), fill=THEME)
+ d.text((96, 236), '先测水平,再按天学,', font=ImageFont.truetype(CJK, 64), fill=INK)
+ d.text((96, 322), '记到面试那天', font=ImageFont.truetype(CJK, 64), fill=INK)
+ d.text((96, 432), f'{count} 道面试题 · 自评定级 · 每日计划 · 间隔复习', font=ImageFont.truetype(CJK, 30), fill=MUTED)
+ d.text((96, 490), '免费、无需注册,学习记录只保存在自己的浏览器中', font=ImageFont.truetype(CJK, 30), fill=MUTED)
+ # 右侧一张示意题卡
+ x, y = 820, 150
+ d.rounded_rectangle([x, y, x + 300, y + 330], radius=20, fill=SOFT)
+ for i, frac in enumerate((0.8, 0.62, 0.7, 0.45)):
+ d.rounded_rectangle([x + 32, y + 48 + i * 46, x + 32 + 236 * frac, y + 68 + i * 46], radius=10, fill=(201, 201, 245) if i == 0 else (255, 255, 255))
+ for i, (label, color) in enumerate((('不会', (180, 42, 51)), ('模糊', (138, 80, 0)), ('会', THEME))):
+ bx = x + 24 + i * 88
+ d.rounded_rectangle([bx, y + 250, bx + 76, y + 300], radius=12, fill=(255, 255, 255))
+ f = ImageFont.truetype(CJK, 24)
+ tw = d.textlength(label, font=f)
+ d.text((bx + 38 - tw / 2, y + 260), label, font=f, fill=color)
+ return img
+
+
+if __name__ == '__main__':
+ out = HERE / 'src' / 'icons'
+ out.mkdir(exist_ok=True)
+ for size in (180, 192, 512):
+ icon(size).save(out / f'icon-{size}.png', optimize=True)
+ icon(512, maskable=True).save(out / 'maskable-512.png', optimize=True)
+ icon(32).save(out / 'favicon-32.png', optimize=True)
+ # 题数取自构建结果,先运行 build.py
+ import json
+ count = len(json.loads((HERE / 'dist' / 'data.json').read_text())['questions'])
+ og(count).save(HERE / 'src' / 'og.png', optimize=True)
+ print('done')
diff --git a/learn/routes.json b/learn/routes.json
new file mode 100644
index 000000000..4db443525
--- /dev/null
+++ b/learn/routes.json
@@ -0,0 +1,9 @@
+{
+ "comment": "学习站使用的路线:file 是 notes/ 下的文件名(不含 .md)。新增路线在这里加一项;下线时删掉对应的项,读者的记录会保留并提示换路线。ignore 中的路线文件不是按天的格式,不放进学习站。",
+ "routes": [
+ {"id": "java", "file": "学习路线 - Java 后端", "title": "Java 后端", "summary": "从 Java 基础到分布式和系统设计,按天读笔记、做自测"},
+ {"id": "llm", "file": "学习路线 - 大模型应用开发", "title": "大模型应用开发", "summary": "从模型原理到 RAG、Agent 和工程实践"},
+ {"id": "sprint", "file": "学习路线 - 面试冲刺", "title": "面试冲刺", "summary": "两周过一遍后端面试最常问的内容"}
+ ],
+ "ignore": ["学习路线 - 零基础"]
+}
diff --git a/learn/serve.py b/learn/serve.py
new file mode 100644
index 000000000..7deb2a843
--- /dev/null
+++ b/learn/serve.py
@@ -0,0 +1,18 @@
+"""只在本机提供 learn/dist 的预览:python3 learn/serve.py [--port 8770]"""
+import argparse
+import functools
+from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
+from pathlib import Path
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--port', type=int, default=8770)
+args = parser.parse_args()
+handler = functools.partial(SimpleHTTPRequestHandler, directory=str(Path(__file__).resolve().parent / 'dist'))
+server = ThreadingHTTPServer(('127.0.0.1', args.port), handler)
+print(f'学习站预览:http://127.0.0.1:{args.port}/', flush=True)
+try:
+ server.serve_forever()
+except KeyboardInterrupt:
+ pass
+finally:
+ server.server_close()
diff --git a/learn/src/app.css b/learn/src/app.css
new file mode 100644
index 000000000..2c71d51f7
--- /dev/null
+++ b/learn/src/app.css
@@ -0,0 +1,686 @@
+/* 学习站的样式。颜色、圆角、阴影都是变量,三套配色方案(data-skin)只改变量,深色模式跟随系统 */
+:root {
+ --bg: #f6f6f8; --surface: #fff; --surface-2: #f1f1f5; --border: #e4e4ea; --border-strong: #cfcfd8;
+ --text: #3b3b46; --heading: #15151c; --muted: #5e5e6a; --faint: #9a9aa6; --hover: rgba(21, 21, 28, .045);
+ --accent: #5b5bd6; --accent-hover: #4c4cc4; --accent-soft: #eeeefd; --accent-text: #4747b8; --on-accent: #fff;
+ --known: #187a42; --known-soft: #e5f5ec; --fuzzy: #8a5000; --fuzzy-soft: #fdf1df; --weak: #b42a33; --weak-soft: #fdebec;
+ --flame: #f2681c; --flame-soft: #fff0e6; --flame-text: #c2410c; --badge: #cf3a3f;
+ --radius-sm: 8px; --radius: 12px; --radius-lg: 18px;
+ --shadow-sm: 0 1px 2px rgba(20, 20, 40, .05);
+ --shadow: 0 1px 2px rgba(20, 20, 40, .04), 0 4px 14px rgba(20, 20, 40, .05);
+ --shadow-lg: 0 2px 6px rgba(20, 20, 40, .05), 0 16px 40px rgba(20, 20, 40, .09);
+ --hero-bg: linear-gradient(135deg, #f3f2ff 0%, #fff 55%);
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
+ --font-head: var(--font);
+ --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+ --depth: 0px; --depth-color: transparent; --head-weight: 680;
+ color-scheme: light;
+}
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #111114; --surface: #1a1a1f; --surface-2: #22222a; --border: #2c2c35; --border-strong: #3d3d48;
+ --text: #c9c9d3; --heading: #f1f1f5; --muted: #9393a1; --faint: #6c6c79; --hover: rgba(255, 255, 255, .05);
+ --accent: #8b8bf0; --accent-hover: #9d9df4; --accent-soft: #23233f; --accent-text: #a9a9f6; --on-accent: #0f0f24;
+ --known: #4cc584; --known-soft: #16291f; --fuzzy: #e3a64a; --fuzzy-soft: #2c2214; --weak: #f07a80; --weak-soft: #331a1d;
+ --flame: #ff8a43; --flame-soft: #33201a; --flame-text: #ff9d5c;
+ --shadow-sm: none; --shadow: 0 1px 0 rgba(255, 255, 255, .03); --shadow-lg: 0 16px 40px rgba(0, 0, 0, .45);
+ --hero-bg: linear-gradient(135deg, #1e1e36 0%, #1a1a1f 60%);
+ color-scheme: dark;
+ }
+}
+/* 方案二:暖色纸面,衬线标题,像读一本书 */
+[data-skin="paper"] {
+ --bg: #f5f1ea; --surface: #fffdf8; --surface-2: #efe9df; --border: #e4dccf; --border-strong: #cfc3b1;
+ --text: #3e3a33; --heading: #1f1b16; --muted: #6a6155; --faint: #a39886; --hover: rgba(60, 45, 20, .05);
+ --accent: #0f766e; --accent-hover: #0c625b; --accent-soft: #e2f0ec; --accent-text: #0c625b; --on-accent: #fff;
+ --flame: #e0601a; --flame-soft: #fcebdc; --flame-text: #b23a0a;
+ --radius-sm: 10px; --radius: 14px; --radius-lg: 22px;
+ --shadow: 0 1px 2px rgba(80, 60, 30, .05), 0 6px 18px rgba(80, 60, 30, .06);
+ --shadow-lg: 0 2px 6px rgba(80, 60, 30, .05), 0 18px 44px rgba(80, 60, 30, .1);
+ --hero-bg: linear-gradient(135deg, #e6f2ee 0%, #fffdf8 60%);
+ --font-head: "Songti SC", "Noto Serif SC", "Source Han Serif SC", STSong, serif; --head-weight: 700;
+}
+@media (prefers-color-scheme: dark) {
+ [data-skin="paper"] {
+ --bg: #17150f; --surface: #1f1c16; --surface-2: #29251d; --border: #353026; --border-strong: #4a4335;
+ --text: #d3cbbd; --heading: #f3ede2; --muted: #a39a8a; --faint: #756d5f; --hover: rgba(255, 245, 225, .05);
+ --accent: #3fb7a9; --accent-hover: #55c4b7; --accent-soft: #15302c; --accent-text: #6fd0c4; --on-accent: #062a26;
+ --hero-bg: linear-gradient(135deg, #15302c 0%, #1f1c16 60%);
+ --flame: #ff8a43; --flame-soft: #33201a; --flame-text: #ff9d5c;
+ --shadow: 0 1px 0 rgba(255, 255, 255, .03); --shadow-lg: 0 16px 40px rgba(0, 0, 0, .45);
+ }
+}
+/* 方案三:活泼,厚按钮、大圆角、亮绿色,接近游戏化的学习应用 */
+[data-skin="play"] {
+ --bg: #f7f9fb; --surface: #fff; --surface-2: #eef3f6; --border: #e1e8ee; --border-strong: #c9d4dd;
+ --text: #3c4650; --heading: #17222c; --muted: #5d6974;
+ --accent: #18874a; --accent-hover: #147a42; --accent-soft: #e3f6ea; --accent-text: #146f3a; --on-accent: #fff;
+ --radius-sm: 12px; --radius: 16px; --radius-lg: 22px;
+ --shadow: none; --shadow-sm: none; --shadow-lg: 0 10px 30px rgba(20, 50, 80, .08);
+ --hero-bg: linear-gradient(135deg, #e3f6ea 0%, #fff 65%);
+ --depth: 4px; --depth-color: #0f5f31; --head-weight: 800;
+}
+@media (prefers-color-scheme: dark) {
+ [data-skin="play"] {
+ --bg: #0f171d; --surface: #16212a; --surface-2: #1d2a35; --border: #263642; --border-strong: #354958;
+ --text: #c6d2dc; --heading: #f0f5f9; --muted: #8b9ba8;
+ --accent: #3ccb74; --accent-hover: #52d585; --accent-soft: #143024; --accent-text: #5fdb8f; --on-accent: #052414;
+ --hero-bg: linear-gradient(135deg, #143024 0%, #16212a 65%); --depth-color: #1f8a4a;
+ }
+}
+
+/* ---------- 基础 ---------- */
+* { box-sizing: border-box; }
+html { -webkit-text-size-adjust: 100%; scroll-padding-top: 80px; }
+body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.75 var(--font); -webkit-font-smoothing: antialiased; }
+a { color: var(--accent-text); text-decoration: none; }
+a:hover { text-decoration: underline; text-underline-offset: 3px; }
+h1, h2, h3 { color: var(--heading); font-family: var(--font-head); font-weight: var(--head-weight); line-height: 1.3; letter-spacing: -.01em; text-wrap: balance; }
+h1 { font-size: 1.9rem; margin: 0 0 .5em; }
+h2 { font-size: 1.2rem; margin: 1.6em 0 .6em; }
+h3 { font-size: 1.05rem; margin: 1.2em 0 .4em; }
+p { margin: .5em 0; }
+:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
+[hidden] { display: none !important; }
+.i { width: 1.2em; height: 1.2em; flex: none; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; vertical-align: -.22em; }
+.i.sm { width: 1em; height: 1em; vertical-align: -.14em; }
+.i .fill { fill: currentColor; stroke: none; }
+.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
+.loading, .muted { color: var(--muted); }
+.small { font-size: 14px; }
+.center { text-align: center; }
+.skip { position: fixed; left: 12px; top: -48px; z-index: 50; padding: 8px 14px; border-radius: 8px; background: var(--accent); color: var(--on-accent); }
+.skip:focus { top: 12px; }
+
+/* ---------- 顶栏 ---------- */
+.top { position: sticky; top: 0; z-index: 10; background: var(--bg); background: color-mix(in srgb, var(--bg) 82%, transparent); -webkit-backdrop-filter: saturate(1.6) blur(14px); backdrop-filter: saturate(1.6) blur(14px); border-bottom: 1px solid var(--border); }
+.top-inner { display: flex; align-items: center; gap: 20px; max-width: 1120px; margin: 0 auto; padding: 10px 24px; }
+.brand { display: inline-flex; align-items: center; gap: 9px; color: var(--heading); font-weight: 700; letter-spacing: -.01em; white-space: nowrap; }
+.brand:hover { text-decoration: none; }
+.brand .logo { color: var(--accent); flex: none; }
+.brand em { font-style: normal; color: var(--accent-text); }
+.top nav { display: flex; gap: 2px; margin: 0 auto; padding: 3px; border-radius: 12px; }
+.top nav a { position: relative; display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px; border-radius: 9px; color: var(--muted); font-size: 15px; font-weight: 500; }
+.top nav a .i { width: 18px; height: 18px; }
+.top nav a:hover { background: var(--hover); color: var(--heading); text-decoration: none; }
+.top nav a[aria-current="page"] { background: var(--surface); color: var(--heading); box-shadow: var(--shadow-sm), 0 0 0 1px var(--border); }
+.top nav a[aria-current="page"] .i { color: var(--accent); }
+.badge { display: inline-block; min-width: 19px; padding: 0 6px; border-radius: 10px; background: var(--badge); color: #fff; font-size: 11.5px; line-height: 19px; text-align: center; font-weight: 700; font-variant-numeric: tabular-nums; }
+.top-end { display: flex; align-items: center; gap: 6px; }
+.streak-chip { display: inline-flex; align-items: center; gap: 4px; padding: 5px 11px 5px 9px; border-radius: 20px; background: var(--surface-2); color: var(--faint); font-weight: 700; font-variant-numeric: tabular-nums; }
+.streak-chip:hover { text-decoration: none; background: var(--hover); }
+.streak-chip.lit { background: var(--flame-soft); color: var(--flame); }
+.streak-chip b { color: var(--heading); }
+.notes-link { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 8px; color: var(--muted); font-size: 15px; }
+.notes-link:hover { color: var(--heading); background: var(--hover); text-decoration: none; }
+
+main { max-width: 1120px; margin: 0 auto; padding: 32px 24px 72px; outline: none; }
+/* 换页时把焦点移到 main 上供读屏软件朗读,不显示焦点框 */
+main:focus, main:focus-visible { outline: none; }
+main[data-view="plan"], main[data-view="bank"], main[data-view="data"], main[data-view="placement"] { max-width: 860px; }
+main[data-view="q"], main[data-view="review"], main[data-view="mock"], main[data-view="new"] { max-width: 760px; }
+main > * { animation: enter .28s cubic-bezier(.2, .7, .3, 1) both; }
+main > :nth-child(2) { animation-delay: .03s; }
+main > :nth-child(3) { animation-delay: .06s; }
+@keyframes enter { from { opacity: 0; transform: translateY(6px); } }
+.foot { max-width: 1120px; margin: 0 auto; padding: 24px 24px 48px; border-top: 1px solid var(--border); color: var(--muted); font-size: 13px; }
+.foot p { margin: 4px 0; }
+.foot-brand { color: var(--heading); font-weight: 650; }
+.foot a { color: inherit; text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--border-strong); }
+
+/* ---------- 按钮 ---------- */
+.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 42px; padding: 8px 18px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface);
+ color: var(--heading); font: inherit; font-size: 15px; font-weight: 600; line-height: 1.3; cursor: pointer; text-decoration: none; box-shadow: var(--shadow-sm), 0 var(--depth) 0 var(--border-strong);
+ transition: background .15s, border-color .15s, transform .08s, box-shadow .15s; -webkit-tap-highlight-color: transparent; }
+.btn:hover { text-decoration: none; background: var(--surface-2); }
+.btn:active { transform: translateY(min(var(--depth), 1px)); box-shadow: 0 0 0 var(--border-strong); }
+.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); box-shadow: 0 1px 2px rgba(0, 0, 0, .08), 0 var(--depth) 0 var(--depth-color); }
+.btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
+.btn.quiet { border-color: transparent; background: none; color: var(--muted); box-shadow: none; font-weight: 500; }
+.btn.quiet:hover { color: var(--heading); background: var(--hover); }
+.btn.big { min-height: 50px; padding: 10px 24px; font-size: 16px; border-radius: calc(var(--radius-sm) + 2px); }
+.btn.sm, .small-btn { min-height: 34px; padding: 4px 12px; font-size: 14px; }
+.small-btn { margin-left: 6px; }
+.btn .i { width: 18px; height: 18px; }
+.btn.primary .i:last-child { transition: transform .15s; }
+.btn.primary:hover .i:last-child { transform: translateX(2px); }
+.btn:disabled { opacity: .5; cursor: not-allowed; }
+.icon-btn { display: inline-grid; place-items: center; width: 40px; height: 40px; border-radius: 10px; color: var(--muted); }
+.icon-btn:hover { background: var(--hover); color: var(--heading); }
+.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 18px 0 0; }
+.actions.inline { margin: 10px 0 18px; }
+
+/* ---------- 通用组件 ---------- */
+.page-head { margin-bottom: 22px; }
+.page-head h1 { margin-bottom: .15em; }
+.page-head h1 .small { font-family: var(--font); font-weight: 500; margin-left: 6px; letter-spacing: 0; }
+.eyebrow { display: flex; align-items: center; gap: 6px; margin: 0 0 6px; color: var(--accent-text); font-size: 13px; font-weight: 650; letter-spacing: .02em; }
+.lead { font-size: 1.1rem; color: var(--text); line-height: 1.7; }
+.panel { margin: 16px 0; padding: 20px 22px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
+.panel > :first-child { margin-top: 0; }
+.panel > :last-child { margin-bottom: 0; }
+.panel.notice { background: var(--fuzzy-soft); border-color: color-mix(in srgb, var(--fuzzy) 25%, transparent); box-shadow: none; }
+.notice-line { padding: 10px 14px; border-radius: var(--radius-sm); background: var(--known-soft); color: var(--text); }
+.block { margin: 28px 0; }
+.block-head > a.small { padding: 6px 8px; margin: -6px -8px; border-radius: 7px; }
+.block-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
+.block-head h2 { margin: 0; }
+details.panel summary { display: flex; align-items: center; gap: 8px; padding: 4px 0; cursor: pointer; color: var(--heading); font-weight: 600; }
+details.panel[open] summary { margin-bottom: 8px; }
+.tag { display: inline-flex; align-items: center; gap: 3px; padding: 0 8px; border-radius: 6px; font-size: 12px; font-weight: 600; line-height: 22px; white-space: nowrap; background: var(--surface-2); color: var(--muted); }
+.tag.learn { background: var(--accent-soft); color: var(--accent-text); }
+.tag.mastered { background: var(--known-soft); color: var(--known); }
+.tag.partial { background: var(--fuzzy-soft); color: var(--fuzzy); }
+.tag.weak, .tag.unknown { background: var(--weak-soft); color: var(--weak); }
+.tag.unknown { background: var(--surface-2); color: var(--muted); }
+.tag.hot { background: var(--flame-soft); color: var(--flame-text); }
+.chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 0; }
+.chip-meta { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 20px; background: var(--surface-2); color: var(--muted); font-size: 13px; }
+a.chip-meta:hover { color: var(--heading); text-decoration: none; background: var(--hover); }
+.bar { height: 6px; border-radius: 6px; background: var(--surface-2); overflow: hidden; }
+.bar i { display: block; height: 100%; background: var(--accent); border-radius: 6px; transition: width .4s cubic-bezier(.2, .7, .3, 1); }
+.ring { position: relative; width: var(--size); height: var(--size); flex: none; }
+.ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
+.ring circle { fill: none; stroke-width: 9; }
+.ring .track { stroke: var(--surface-2); }
+.ring .value { stroke: var(--accent); stroke-linecap: round; transition: stroke-dasharray .6s cubic-bezier(.2, .7, .3, 1); }
+.ring > span { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; line-height: 1.2; }
+.ring b { color: var(--heading); font-size: calc(var(--size) * .22); font-weight: 750; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
+.ring small { max-width: 80%; margin-top: 3px; color: var(--muted); font-size: 11.5px; }
+.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 16px 0; }
+.stat { position: relative; padding: 16px 18px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
+.stat b { display: block; color: var(--heading); font-size: 1.65rem; font-weight: 750; line-height: 1.2; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
+.stat b small { font-size: .55em; font-weight: 550; color: var(--muted); letter-spacing: 0; }
+.stat > span:not(.stat-icon) { color: var(--muted); font-size: 13px; }
+.stat-icon { display: inline-grid; place-items: center; width: 32px; height: 32px; margin-bottom: 10px; border-radius: 9px; background: var(--accent-soft); color: var(--accent-text); }
+.stat-icon .i { width: 18px; height: 18px; }
+.stat.streak .stat-icon { background: var(--surface-2); color: var(--faint); }
+.stat.streak.lit .stat-icon { background: var(--flame-soft); color: var(--flame); }
+.empty { padding: 48px 20px; text-align: center; }
+.empty .actions { justify-content: center; }
+.empty-icon { display: inline-grid; place-items: center; width: 64px; height: 64px; margin-bottom: 14px; border-radius: 50%; background: var(--known-soft); color: var(--known); }
+.empty-icon .i { width: 30px; height: 30px; stroke-width: 2.2; }
+.small-empty { padding: 28px 12px; }
+.small-empty .empty-icon { width: 48px; height: 48px; background: var(--surface-2); color: var(--muted); }
+.small-empty .empty-icon .i { width: 22px; height: 22px; }
+.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 0; margin-bottom: 14px; color: var(--muted); font-size: 14px; }
+.crumbs a { display: inline-flex; align-items: center; gap: 4px; margin-right: 6px; padding: 5px 10px 5px 6px; border-radius: 7px; color: var(--muted); }
+.crumbs a:hover { background: var(--hover); color: var(--heading); text-decoration: none; }
+.crumbs span + span::before { content: "/"; margin: 0 8px; color: var(--border-strong); }
+
+/* ---------- 首页 ---------- */
+.home { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 28px; align-items: start; }
+.home-side { position: sticky; top: 84px; }
+.home-side .stats { grid-template-columns: 1fr 1fr; margin-top: 0; }
+.home-side .stat.streak { grid-column: 1 / -1; display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto auto; column-gap: 12px; align-items: center; }
+.home-side .stat.streak .stat-icon { grid-row: 1 / 3; margin: 0; width: 44px; height: 44px; border-radius: 12px; }
+.home-side .stat.streak .stat-icon .i { width: 24px; height: 24px; }
+.home-side .stat.streak .week { grid-column: 1 / -1; margin-top: 14px; }
+.home-side .stat.streak .streak-note { grid-column: 1 / -1; }
+.forecast { display: flex; gap: 8px; margin: 4px 2px 16px; line-height: 1.6; }
+.forecast .i { margin-top: 3px; }
+.hero { position: relative; display: flex; align-items: center; gap: 24px; margin: 0 0 8px; padding: 28px 30px; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--hero-bg); box-shadow: var(--shadow-lg); overflow: hidden; }
+.hero-body { flex: 1; min-width: 0; }
+.hero h2 { margin: 2px 0 8px; font-size: 1.55rem; }
+.hero p { margin: 4px 0; }
+.hero .actions { margin-top: 20px; }
+.hero-side { flex: none; }
+.hero.done { --hero-bg: linear-gradient(135deg, var(--known-soft) 0%, var(--surface) 60%); }
+.hero.done .eyebrow { color: var(--known); }
+.hero.done .ring .value { stroke: var(--known); }
+.hero.celebrate { margin-top: 18px; animation: pop .45s cubic-bezier(.2, 1.4, .4, 1) both; }
+@keyframes pop { from { opacity: 0; transform: scale(.96) translateY(6px); } }
+.tasks { display: grid; gap: 8px; }
+.task { display: flex; align-items: center; gap: 14px; padding: 14px 16px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); transition: border-color .15s, transform .15s, box-shadow .15s; }
+.task:hover { text-decoration: none; border-color: var(--border-strong); box-shadow: var(--shadow); transform: translateY(-1px); }
+.task-check { display: grid; place-items: center; width: 32px; height: 32px; flex: none; border: 2px solid var(--border-strong); border-radius: 50%; color: var(--muted); font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
+.task.done .task-check { border-color: var(--known); background: var(--known); color: #fff; }
+.task.done .task-check .i { stroke-width: 2.6; width: 16px; height: 16px; }
+.task-body { display: flex; flex-direction: column; flex: 1; min-width: 0; gap: 3px; }
+.task-title { color: var(--heading); font-weight: 600; line-height: 1.45; }
+.task.done .task-title { color: var(--muted); text-decoration: line-through; text-decoration-color: var(--border-strong); }
+.task-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; color: var(--muted); font-size: 13px; }
+.task-meta > span:not(.tag) { display: inline-flex; align-items: center; gap: 4px; }
+.task-chev { color: var(--faint); width: 18px; height: 18px; transition: transform .15s; }
+.task:hover .task-chev { transform: translateX(2px); color: var(--muted); }
+.path { margin: 0; padding: 4px 0; list-style: none; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
+.path-stage { position: relative; display: flex; gap: 14px; padding: 12px 18px; }
+.path-stage + .path-stage { border-top: 1px solid var(--border); }
+.path-dot { display: grid; place-items: center; width: 28px; height: 28px; flex: none; margin-top: 1px; border-radius: 50%; background: var(--surface-2); color: var(--muted); font-size: 12.5px; font-weight: 700; }
+.path-stage.complete .path-dot { background: var(--known); color: #fff; }
+.path-stage.complete .path-dot .i { width: 15px; height: 15px; stroke-width: 2.6; }
+.path-stage.current .path-dot { background: var(--accent); color: var(--on-accent); box-shadow: 0 0 0 4px var(--accent-soft); }
+.path-more { padding: 6px 18px 6px 60px; border-top: 1px solid var(--border); font-size: 14px; }
+.path-more a { display: inline-block; padding: 6px 0; color: var(--muted); }
+.path-body { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 6px 14px; flex: 1; min-width: 0; }
+.path-title { display: flex; align-items: center; gap: 8px; color: var(--heading); font-weight: 600; font-size: 15px; }
+.path-stage:not(.current):not(.complete):not(.started) .path-title { color: var(--muted); font-weight: 500; }
+.path-meta { color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; grid-row: 1; grid-column: 2; }
+.path-body .bar { grid-column: 1 / -1; height: 5px; }
+.path-stage.complete .bar i { background: var(--known); }
+.week { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; margin: 0; padding: 0; list-style: none; text-align: center; font-size: 12px; color: var(--muted); }
+.week li { display: flex; flex-direction: column; align-items: center; gap: 4px; }
+.week i { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; background: var(--surface-2); color: #fff; }
+.week i .i { width: 14px; height: 14px; stroke-width: 2.8; }
+.week .on i { background: var(--flame); }
+.week .now span { color: var(--heading); font-weight: 700; }
+.week .now i { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--border-strong); }
+.week .now.on i { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px color-mix(in srgb, var(--flame) 45%, transparent); }
+.quick { display: grid; margin: 0; padding: 4px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
+.quick a { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: calc(var(--radius) - 4px); color: var(--heading); font-weight: 600; font-size: 15px; }
+.quick a:hover { background: var(--hover); text-decoration: none; }
+.quick a > .i:first-child { width: 20px; height: 20px; color: var(--accent-text); }
+.quick a > span { display: flex; flex-direction: column; flex: 1; line-height: 1.35; }
+.quick small { color: var(--muted); font-weight: 400; font-size: 12.5px; }
+.update { border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); background: linear-gradient(135deg, var(--accent-soft), var(--surface) 70%); }
+.update h2 { display: flex; align-items: center; gap: 8px; margin: 0 0 4px; font-size: 1.05rem; }
+.update h2 .i { color: var(--accent-text); }
+.update h2 .small { font-weight: 400; }
+.update ul { margin: 6px 0 0; padding-left: 1.2em; }
+.update li { margin: 6px 0; }
+
+/* ---------- 题卡 ---------- */
+.card { margin: 16px 0; padding: 24px 26px; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); box-shadow: var(--shadow); transition: border-color .2s, box-shadow .2s; }
+.card .meta { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; margin-bottom: 12px; }
+.meta-text { display: inline-flex; align-items: center; gap: 4px; color: var(--muted); font-size: 13px; }
+.card .q { margin: 0 0 16px; color: var(--heading); font-size: 1.2rem; font-weight: 650; line-height: 1.55; letter-spacing: -.005em; }
+.answer-label { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-bottom: 6px; color: var(--heading); font-size: 14px; font-weight: 600; }
+.answer-label span { color: var(--muted); font-weight: 400; font-size: 12.5px; }
+textarea { display: block; width: 100%; min-height: 72px; padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); color: var(--heading); font: inherit; line-height: 1.7; resize: vertical; transition: border-color .15s, background .15s, box-shadow .15s; }
+textarea::placeholder { color: var(--muted); opacity: .8; }
+textarea:focus { outline: none; border-color: var(--accent); background: var(--surface); box-shadow: 0 0 0 3px var(--accent-soft); }
+.card .actions { margin-top: 14px; }
+.card .answer { margin-top: 18px; padding: 18px 20px; border-radius: var(--radius); background: var(--surface-2); outline: none; animation: reveal .3s cubic-bezier(.2, .7, .3, 1) both; }
+@keyframes reveal { from { opacity: 0; transform: translateY(-4px); } }
+.answer-title { display: flex; align-items: center; gap: 6px; margin: 0 0 6px; color: var(--accent-text); font-size: 13px; font-weight: 700; letter-spacing: .02em; }
+.card .points { margin: 0; white-space: pre-line; color: var(--text); }
+.card .refs { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 12px 0 0; padding: 0; list-style: none; font-size: 14px; }
+.card .refs a { display: inline-flex; align-items: center; gap: 4px; }
+.card .refs .i { color: var(--faint); }
+.rate-prompt { margin: 18px 0 8px; color: var(--heading); font-size: 14px; font-weight: 600; }
+.rate { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
+.rate button { position: relative; display: flex; align-items: center; gap: 10px; min-height: 58px; padding: 8px 14px; border: 1.5px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--heading);
+ font: inherit; font-weight: 650; text-align: left; cursor: pointer; box-shadow: 0 var(--depth) 0 var(--border); transition: border-color .15s, background .15s, transform .08s; -webkit-tap-highlight-color: transparent; }
+.rate button:active { transform: translateY(min(var(--depth), 1px)) scale(.98); }
+.rate-icon { display: grid; place-items: center; width: 30px; height: 30px; flex: none; border-radius: 50%; }
+.rate-icon .i { width: 17px; height: 17px; stroke-width: 2.4; }
+.rate-text { display: flex; flex-direction: column; line-height: 1.3; }
+.rate button small { font-weight: 400; font-size: 12px; color: var(--muted); }
+.rate-key { position: absolute; top: 8px; right: 10px; }
+.rate .r0 .rate-icon { background: var(--weak-soft); color: var(--weak); }
+.rate .r1 .rate-icon { background: var(--fuzzy-soft); color: var(--fuzzy); }
+.rate .r2 .rate-icon { background: var(--known-soft); color: var(--known); }
+.rate .r0:hover { border-color: var(--weak); background: var(--weak-soft); }
+.rate .r1:hover { border-color: var(--fuzzy); background: var(--fuzzy-soft); }
+.rate .r2:hover { border-color: var(--known); background: var(--known-soft); }
+.card-tools { display: flex; flex-wrap: wrap; gap: 4px; margin: 14px -8px -6px; }
+.card-tools .btn { color: var(--muted); }
+.card.is-rated { padding-top: 18px; padding-bottom: 18px; }
+.card.is-rated .q { margin-bottom: 6px; font-size: 1.05rem; }
+.card.rated-2 { border-color: color-mix(in srgb, var(--known) 40%, var(--border)); }
+.card.rated-1 { border-color: color-mix(in srgb, var(--fuzzy) 40%, var(--border)); }
+.card.rated-0 { border-color: color-mix(in srgb, var(--weak) 40%, var(--border)); }
+.rated { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 4px 0 0; color: var(--muted); font-size: 14px; animation: reveal .3s both; }
+.rated .btn { margin-left: auto; }
+.rated-icon { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; }
+.rated-icon .i { width: 14px; height: 14px; stroke-width: 2.8; }
+.rated-2 .rated-icon { background: var(--known); color: #fff; }
+.rated-1 .rated-icon { background: var(--fuzzy); color: #fff; }
+.rated-0 .rated-icon { background: var(--weak); color: #fff; }
+.keys { color: var(--muted); font-size: 13px; text-align: center; }
+@media (pointer: coarse) { .keys, .rate-key { display: none; } }
+kbd { display: inline-block; min-width: 20px; padding: 2px 6px; border: 1px solid var(--border-strong); border-bottom-width: 2px; border-radius: 5px; background: var(--surface); color: var(--muted); font: 600 11.5px/1.2 var(--mono); text-align: center; }
+.progress-line { display: flex; align-items: center; gap: 14px; color: var(--muted); font-size: 14px; font-variant-numeric: tabular-nums; }
+.progress-line .bar { flex: 1; }
+
+/* ---------- 复习与模拟面试 ---------- */
+.session-top { display: flex; align-items: center; gap: 12px; margin: -8px 0 20px; }
+.session-top .progress-line { flex: 1; }
+.bar.segs { display: flex; gap: 4px; height: 8px; background: none; overflow: visible; }
+.bar.segs i { flex: 1; height: 100%; border-radius: 4px; background: var(--surface-2); }
+.bar.segs i.done { background: var(--accent); }
+.bar.segs i.now { background: var(--accent-soft); background: color-mix(in srgb, var(--accent) 35%, var(--surface-2)); }
+.bar.segs.dense { gap: 1px; }
+.session-tally { display: flex; gap: 10px; font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }
+.session-tally span { display: inline-flex; align-items: center; gap: 3px; }
+.session-tally .r2 { color: var(--known); }
+.session-tally .r0 { color: var(--weak); }
+.session-title { font-size: 1.1rem; color: var(--muted); font-family: var(--font); font-weight: 600; margin: 0 0 4px; }
+.session .card { margin-top: 10px; }
+.session-summary { display: flex; align-items: center; gap: 28px; margin: 8px 0 18px; padding: 26px 28px; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--hero-bg); box-shadow: var(--shadow-lg); animation: pop .45s cubic-bezier(.2, 1.4, .4, 1) both; }
+.session-summary h1 { margin: 0 0 12px; font-size: 1.6rem; }
+.score { display: flex; flex-wrap: wrap; gap: 8px; }
+.score-item { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 10px; background: var(--surface); border: 1px solid var(--border); font-size: 14px; color: var(--muted); }
+.score-item b { color: var(--heading); font-size: 1.15rem; font-variant-numeric: tabular-nums; }
+.score-item.r2 .i { color: var(--known); } .score-item.r1 .i { color: var(--fuzzy); } .score-item.r0 .i { color: var(--weak); }
+.missed { margin: 0; padding: 0; list-style: none; }
+.missed a { padding: 4px 0; }
+.missed li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; padding: 10px 0; border-bottom: 1px solid var(--border); }
+.missed li:last-child { border-bottom: 0; }
+.missed li > :first-child { flex: 1 1 260px; color: var(--heading); }
+.missed a { display: inline-flex; align-items: center; gap: 4px; }
+
+/* ---------- 计划 ---------- */
+.plan-stats { margin-top: 0; }
+.plan-stats .stat b { font-size: 1.4rem; }
+.timeline { position: relative; margin-top: 22px; }
+.timeline-stage { display: flex; align-items: center; gap: 8px; margin: 26px 0 8px 112px; color: var(--accent-text); font-size: 13px; font-weight: 700; letter-spacing: .02em; }
+.timeline-stage:first-child { margin-top: 0; }
+.plan-day { position: relative; display: grid; grid-template-columns: 96px 1fr; gap: 16px; padding: 6px 0; }
+.plan-day::before { content: ""; position: absolute; left: 104px; top: 0; bottom: 0; width: 2px; background: var(--border); }
+.plan-day::after { content: ""; position: absolute; left: 99px; top: 26px; width: 12px; height: 12px; border-radius: 50%; background: var(--surface); border: 2px solid var(--border-strong); }
+.plan-day.today::after { background: var(--accent); border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
+.plan-day .tasks { padding-left: 16px; }
+.plan-date { display: flex; flex-direction: column; align-items: flex-end; padding: 14px 14px 0 0; line-height: 1.3; text-align: right; }
+.plan-date time { color: var(--heading); font-weight: 650; font-size: 15px; font-variant-numeric: tabular-nums; }
+.plan-week { color: var(--muted); font-size: 12.5px; }
+.plan-day.today time { color: var(--accent-text); }
+
+/* ---------- 每天的内容 ---------- */
+.day-head h1 { font-size: 2rem; }
+.guide { max-width: 720px; padding: 16px 20px; border-left: 3px solid var(--accent); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; background: var(--accent-soft); color: var(--text); }
+.day-layout { display: grid; grid-template-columns: minmax(0, 1fr) 220px; gap: 40px; align-items: start; margin-top: 8px; }
+.day-main { max-width: 760px; }
+.step { margin: 32px 0; }
+.step-head { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
+.step-head h2 { margin: 0; flex: 1; }
+.step-num { display: grid; place-items: center; width: 36px; height: 36px; flex: none; border-radius: 10px; background: var(--accent-soft); color: var(--accent-text); }
+.step-num .i { width: 19px; height: 19px; }
+.step-count { padding: 2px 10px; border-radius: 20px; background: var(--surface-2); color: var(--muted); font-size: 13px; font-weight: 650; font-variant-numeric: tabular-nums; }
+.sources { display: grid; gap: 8px; margin: 12px 0 0; padding: 0; list-style: none; }
+.source { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--heading); font-weight: 600; box-shadow: var(--shadow-sm); transition: border-color .15s, transform .15s; }
+.source:hover { text-decoration: none; border-color: var(--accent); transform: translateY(-1px); }
+.source > .i:first-child { width: 22px; height: 22px; color: var(--accent-text); }
+.source > span:not(.tag) { display: flex; flex-direction: column; flex: 1; min-width: 0; line-height: 1.4; }
+.source small { color: var(--muted); font-weight: 400; font-size: 12.5px; }
+.source > .i:last-child { color: var(--faint); }
+.finish { margin: 36px 0 0; padding-top: 24px; border-top: 1px solid var(--border); }
+.finish .actions { margin: 0; }
+.pager { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 28px; }
+.pager a { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border: 1px solid var(--border); border-radius: var(--radius); color: var(--heading); font-weight: 600; font-size: 14.5px; }
+.pager a:hover { text-decoration: none; border-color: var(--border-strong); background: var(--surface); }
+.pager a span { display: flex; flex-direction: column; min-width: 0; line-height: 1.4; }
+.pager small { color: var(--muted); font-weight: 400; font-size: 12.5px; }
+.pager .next { grid-column: 2; justify-content: flex-end; text-align: right; }
+.day-side { position: sticky; top: 92px; padding-left: 20px; border-left: 1px solid var(--border); }
+.side-title { margin: 0 0 6px; color: var(--muted); font-size: 12.5px; font-weight: 700; letter-spacing: .04em; }
+.toc { margin: 0 0 14px; padding: 0; list-style: none; }
+.toc button { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; margin-left: -8px; border: 0; border-radius: 7px; background: none; color: var(--text); font: inherit; font-size: 14.5px; text-align: left; cursor: pointer; }
+.toc button:hover { background: var(--hover); color: var(--heading); }
+.toc .i { color: var(--muted); }
+.toc-count { margin-left: auto; color: var(--muted); font-size: 12.5px; font-variant-numeric: tabular-nums; }
+.day-side .bar { height: 5px; }
+.day-side p { margin-top: 8px; }
+
+/* ---------- 介绍页与设置 ---------- */
+.landing { margin-bottom: 28px; }
+.landing-hero { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, .9fr); gap: 48px; align-items: center; padding: 28px 0 36px; }
+.landing h1 { font-size: clamp(2rem, 4.2vw, 3.1rem); line-height: 1.15; letter-spacing: -.025em; margin: 6px 0 18px; }
+.landing .lead { max-width: 560px; }
+.landing .actions { margin-top: 26px; }
+.proof { display: flex; gap: 32px; margin: 30px 0 0; padding: 0; list-style: none; color: var(--muted); font-size: 14px; }
+.proof b { display: block; color: var(--heading); font-size: 1.6rem; font-weight: 750; line-height: 1.2; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
+.demo { position: relative; padding: 44px 30px 40px 0; }
+.demo-card { padding: 22px 24px; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); box-shadow: var(--shadow-lg); transform: rotate(-1.2deg); }
+.demo-meta { display: flex; gap: 10px; align-items: center; margin: 0 0 10px; }
+.demo-q { margin: 0 0 14px; color: var(--heading); font-weight: 650; font-size: 1.05rem; line-height: 1.5; }
+.demo-lines i { display: block; height: 8px; margin: 8px 0; border-radius: 4px; background: var(--surface-2); }
+.demo-lines .short { width: 60%; }
+.demo-rate { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 16px; }
+.demo-rate span { display: flex; align-items: center; justify-content: center; gap: 4px; flex-wrap: wrap; min-height: 42px; padding: 4px; border: 1.5px solid var(--border); border-radius: 10px; color: var(--heading); font-size: 14px; font-weight: 650; }
+.demo-rate small { width: 100%; text-align: center; color: var(--known); font-weight: 500; font-size: 11px; line-height: 1; }
+.demo-rate .r0 .i { color: var(--weak); } .demo-rate .r1 .i { color: var(--fuzzy); } .demo-rate .r2 .i { color: var(--known); }
+.demo-rate .on { border-color: var(--known); background: var(--known-soft); }
+.demo-streak { position: absolute; left: -22px; bottom: 6px; display: flex; align-items: center; gap: 8px; padding: 10px 16px 10px 12px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow-lg); color: var(--flame); font-size: 14px; }
+.demo-streak .i { width: 26px; height: 26px; }
+.demo-streak span { color: var(--muted); }
+.demo-streak b { color: var(--heading); font-size: 1.1rem; }
+.demo-ring { position: absolute; right: -6px; top: -36px; padding: 10px; border: 1px solid var(--border); border-radius: 50%; background: var(--surface); box-shadow: var(--shadow-lg); }
+.demo-ring .ring small { font-size: 9.5px; }
+.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin: 8px 0 18px; padding: 0; list-style: none; }
+.steps li { padding: 20px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
+.step-icon { display: inline-grid; place-items: center; width: 40px; height: 40px; margin-bottom: 12px; border-radius: 11px; background: var(--accent-soft); color: var(--accent-text); }
+.steps b { display: block; color: var(--heading); font-size: 1.05rem; }
+.steps li > span:last-child { display: block; color: var(--muted); font-size: 14px; line-height: 1.65; }
+.landing .center .i { margin-right: 4px; }
+.setup-form.panel { padding: 28px 30px; }
+.form-title { margin: 0 0 4px; }
+fieldset { border: 0; padding: 0; margin: 24px 0; }
+legend { display: flex; align-items: center; gap: 10px; padding: 0; margin-bottom: 12px; color: var(--heading); font-weight: 650; }
+legend .num { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; background: var(--accent); color: var(--on-accent); font-size: 12.5px; font-weight: 700; }
+.routes { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
+.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; }
+.grid.minutes { grid-template-columns: repeat(4, 1fr); gap: 8px; }
+.choice { position: relative; display: block; padding: 16px 18px; border: 1.5px solid var(--border); border-radius: var(--radius); background: var(--surface); cursor: pointer; transition: border-color .15s, background .15s, box-shadow .15s; }
+.choice:hover { border-color: var(--border-strong); }
+.choice input { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
+.choice:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
+.choice:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 1px var(--accent); }
+.choice strong { display: block; color: var(--heading); }
+.choice > span { display: block; color: var(--muted); font-size: 14px; line-height: 1.6; }
+.route-choice { padding: 18px 18px 16px; }
+.route-choice::after { content: ""; position: absolute; top: 16px; right: 16px; width: 20px; height: 20px; border: 2px solid var(--border-strong); border-radius: 50%; background: var(--surface); }
+.route-choice:has(input:checked)::after { border: 6px solid var(--accent); }
+.route-icon { display: grid !important; place-items: center; width: 40px; height: 40px; margin-bottom: 12px; border-radius: 11px; background: var(--surface-2); color: var(--accent-text) !important; }
+.route-choice:has(input:checked) .route-icon { background: var(--surface); }
+.route-icon .i { width: 22px; height: 22px; }
+.route-choice strong { font-size: 1.05rem; margin-bottom: 2px; }
+.route-meta { margin-top: 10px; color: var(--heading) !important; font-size: 13px !important; font-weight: 600; }
+.choice.seg { padding: 12px 8px; text-align: center; }
+.row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
+input[type="date"] { min-height: 42px; padding: 8px 12px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--heading); font: inherit; }
+
+/* 外观选择:色块用各方案自己的变量画,选项本身套上对应方案 */
+.skins { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
+.skin-choice { padding: 14px 16px; background: var(--surface); }
+.swatch { display: flex !important; gap: 4px; margin-bottom: 10px; }
+.swatch i { width: 22px; height: 22px; border-radius: 50%; border: 1px solid var(--border); }
+.skin-picker { margin: 0; }
+@media (max-width: 640px) { .skins { grid-template-columns: 1fr; } }
+
+/* ---------- 定级 ---------- */
+.stage-dots { display: flex; gap: 6px; margin: 10px 0 0; }
+.stage-dots i { width: 8px; height: 8px; border-radius: 50%; background: var(--border-strong); }
+.stage-dots i.done { background: var(--accent); }
+.stage-dots i.now { width: 22px; border-radius: 4px; background: var(--accent); }
+.stage-title { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
+.stage-title .small { font-family: var(--font); font-weight: 500; }
+.result { display: grid; grid-template-columns: 1fr auto; gap: 8px 12px; align-items: center; padding: 12px 0; border-bottom: 1px solid var(--border); }
+.result:last-child { border-bottom: 0; }
+.result b { color: var(--heading); font-weight: 600; }
+
+/* ---------- 进度 ---------- */
+.progress-top { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); gap: 16px; align-items: stretch; }
+.progress-top .stats { margin: 0; }
+.progress-ring { display: flex; align-items: center; gap: 22px; margin: 0; }
+.big-num { color: var(--heading); font-size: 1.8rem; font-weight: 750; letter-spacing: -.02em; }
+.big-num small { font-size: .55em; color: var(--muted); font-weight: 550; }
+.heat { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 1fr); grid-template-columns: repeat(16, 1fr); gap: 4px; max-width: 520px; margin: 8px 0 4px; }
+.heat i, .heat-legend i { display: block; aspect-ratio: 1; border-radius: 4px; background: var(--surface-2); }
+.heat i.future { visibility: hidden; }
+.heat-legend span { display: inline-flex; align-items: center; gap: 3px; margin-left: 10px; }
+.heat-legend i { width: 11px; }
+.l1 { background: var(--accent-soft); background: color-mix(in srgb, var(--accent) 28%, var(--surface-2)) !important; }
+.l2 { background: var(--accent); background: color-mix(in srgb, var(--accent) 52%, var(--surface-2)) !important; }
+.l3 { background: var(--accent); background: color-mix(in srgb, var(--accent) 76%, var(--surface-2)) !important; }
+.l4 { background: var(--accent) !important; }
+.mastery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); column-gap: 32px; }
+.mastery { padding: 10px 0; }
+.heat-wrap { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 20px 40px; }
+.heat-wrap > :first-child { flex: 1 1 360px; max-width: 520px; }
+.heat-wrap .heat-legend { flex-basis: 100%; }
+.heat-summary { display: grid; gap: 14px; margin: 8px 0 0; flex: 0 0 160px; }
+.heat-summary dt { color: var(--muted); font-size: 13px; }
+.heat-summary dd { margin: 0; color: var(--heading); font-size: 1.5rem; font-weight: 750; line-height: 1.2; font-variant-numeric: tabular-nums; }
+.heat-summary small { font-size: .55em; color: var(--muted); font-weight: 550; }
+.mastery-head { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 6px; color: var(--heading); font-weight: 550; }
+.stack { display: flex; height: 8px; border-radius: 5px; background: var(--surface-2); overflow: hidden; }
+.stack i, .legend i { display: block; height: 100%; }
+.stack .solid, .legend .solid { background: var(--accent); }
+.stack .learning, .legend .learning { background: var(--accent-soft); background: color-mix(in srgb, var(--accent) 35%, var(--surface-2)); }
+.legend i { display: inline-block; width: 10px; height: 10px; margin: 0 4px 0 12px; border-radius: 3px; background: var(--surface-2); vertical-align: -1px; }
+.legend i:first-child { margin-left: 0; }
+.share { display: flex; flex-wrap: wrap; gap: 28px; align-items: center; }
+.share img { width: 240px; height: 320px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface-2); box-shadow: var(--shadow-lg); transform: rotate(-1.5deg); }
+.share-body { flex: 1 1 280px; }
+.share-body h2 { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; }
+
+/* ---------- 题库 ---------- */
+.tabs { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
+.chip { min-height: 36px; padding: 4px 15px; border: 1px solid var(--border); border-radius: 20px; background: var(--surface); color: var(--text); font: inherit; font-size: 14px; font-weight: 500; cursor: pointer; transition: background .15s, border-color .15s; }
+.chip:hover { border-color: var(--border-strong); }
+.chip[aria-pressed="true"] { background: var(--heading); border-color: var(--heading); color: var(--bg); }
+.bank-tools { margin: 14px 0 4px; }
+.search { position: relative; }
+.search .i { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: var(--faint); pointer-events: none; }
+.search input { width: 100%; min-height: 48px; padding: 10px 16px 10px 44px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--heading); font: inherit; box-shadow: var(--shadow-sm); }
+.search input:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-soft); }
+.bank-stage { font-size: 1rem; margin: 26px 0 8px; }
+.bank { margin: 0; padding: 4px; list-style: none; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow-sm); }
+.bank li { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; padding: 9px 12px; border-radius: calc(var(--radius) - 4px); }
+.bank li:hover { background: var(--hover); }
+.bank li a { flex: 1 1 260px; color: var(--heading); }
+.bank li a::after { content: ""; position: absolute; inset: 0; }
+.bank li a:hover { text-decoration: none; }
+.bank li + li::before { content: ""; position: absolute; top: 0; left: 12px; right: 12px; border-top: 1px solid var(--border); }
+.bank li:hover::before, .bank li:hover + li::before { border-color: transparent; }
+
+/* ---------- 浮层 ---------- */
+.update-bar { position: fixed; left: 50%; bottom: 20px; z-index: 45; display: flex; align-items: center; gap: 14px; transform: translateX(-50%); width: max-content; max-width: calc(100vw - 24px);
+ padding: 10px 12px 10px 18px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow-lg); font-size: 14px; animation: rise .3s both; }
+.toast { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 40; max-width: calc(100vw - 32px); padding: 10px 18px; border-radius: 12px; background: var(--heading); color: var(--bg); font-size: 14px; box-shadow: var(--shadow-lg); animation: rise .25s both; }
+@keyframes rise { from { opacity: 0; transform: translate(-50%, 8px); } }
+.noscript { padding: 24px; text-align: center; }
+
+/* ---------- 方案三的厚边框 ---------- */
+[data-skin="play"] .card, [data-skin="play"] .panel, [data-skin="play"] .stat, [data-skin="play"] .task, [data-skin="play"] .path, [data-skin="play"] .quick,
+[data-skin="play"] .source, [data-skin="play"] .steps li, [data-skin="play"] .bank, [data-skin="play"] .hero, [data-skin="play"] .session-summary { border-width: 2px; border-bottom-width: 4px; }
+[data-skin="play"] .btn:not(.quiet) { border-width: 2px; text-transform: none; }
+[data-skin="play"] .btn.primary:active, [data-skin="play"] .rate button:active { transform: translateY(3px); box-shadow: 0 1px 0 var(--depth-color); }
+[data-skin="play"] .rate button { border-width: 2px; }
+[data-skin="play"] .rate .r0 { box-shadow: 0 4px 0 color-mix(in srgb, var(--weak) 35%, var(--border)); }
+[data-skin="play"] .rate .r1 { box-shadow: 0 4px 0 color-mix(in srgb, var(--fuzzy) 35%, var(--border)); }
+[data-skin="play"] .rate .r2 { box-shadow: 0 4px 0 color-mix(in srgb, var(--known) 35%, var(--border)); }
+[data-skin="play"] .ring circle { stroke-width: 11; }
+[data-skin="play"] .top nav a[aria-current="page"] { background: var(--accent-soft); color: var(--accent-text); box-shadow: none; }
+
+/* ---------- 窄屏 ---------- */
+@media (max-width: 1000px) {
+ .home { grid-template-columns: 1fr; gap: 8px; }
+ .home-side { position: static; }
+ .home-side .stats { grid-template-columns: repeat(3, 1fr); }
+ .home-side .stat.streak { grid-column: auto; display: block; }
+ .home-side .stat.streak .stat-icon { width: 32px; height: 32px; margin-bottom: 10px; }
+ .home-side .stat.streak .week, .home-side .stat.streak .streak-note { display: none; }
+ .quick { grid-template-columns: 1fr 1fr; }
+ .day-layout { grid-template-columns: 1fr; }
+ .day-side { display: none; }
+ .progress-top { grid-template-columns: 1fr; }
+}
+@media (max-width: 860px) {
+ .landing-hero { grid-template-columns: 1fr; gap: 20px; padding-top: 8px; }
+ .demo { max-width: 460px; margin: 0 auto; padding: 18px 26px 40px; }
+ .demo-streak { left: 0; }
+ .demo-ring { right: 0; }
+}
+@media (max-width: 640px) {
+ body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); font-size: 15.5px; }
+ /* backdrop-filter 会让 fixed 的子元素相对顶栏定位,手机上去掉,底部标签栏才能贴到屏幕底部 */
+ .top { background: var(--bg); -webkit-backdrop-filter: none; backdrop-filter: none; }
+ .top-inner { padding: 8px 14px; gap: 8px; justify-content: space-between; }
+ .brand .logo { width: 22px; height: 22px; }
+ /* 手机上导航改为底部标签栏,单手可以够到 */
+ .top nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; display: grid; grid-template-columns: repeat(5, 1fr); gap: 0; margin: 0; padding: 4px 6px calc(4px + env(safe-area-inset-bottom)); border-radius: 0;
+ background: var(--surface); background: color-mix(in srgb, var(--surface) 92%, transparent); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); border-top: 1px solid var(--border); }
+ .top nav a { flex-direction: column; justify-content: center; gap: 2px; min-height: 52px; padding: 4px 0; font-size: 11.5px; border-radius: 10px; }
+ .top nav a .i { width: 22px; height: 22px; }
+ .top nav a[aria-current="page"] { background: none; box-shadow: none; color: var(--accent-text); }
+ .top nav a[aria-current="page"] .i { stroke-width: 2.2; }
+ .badge { position: absolute; top: 3px; left: calc(50% + 5px); }
+ .notes-link { padding: 6px 8px; font-size: 14px; }
+ main { padding: 20px 16px 56px; }
+ h1 { font-size: 1.5rem; }
+ .page-head { margin-bottom: 16px; }
+ .page-head h1 .small { display: block; margin: 4px 0 0; font-size: 14px; }
+ .hero { flex-direction: column-reverse; align-items: stretch; gap: 0; padding: 22px 20px; }
+ .hero-side { position: absolute; top: 18px; right: 16px; }
+ .hero-side .ring { --size: 64px !important; }
+ .hero-side .ring small { display: none; }
+ .hero-body { padding-right: 0; }
+ .hero .eyebrow, .hero h2 { padding-right: 72px; }
+ .hero h2 { font-size: 1.3rem; }
+ .hero .btn.big { width: 100%; }
+ .stats { gap: 8px; }
+ .stat { padding: 12px; }
+ .stat b { font-size: 1.3rem; }
+ .stat-icon { display: none !important; }
+ .quick { grid-template-columns: 1fr; }
+ .card { padding: 18px 16px; border-radius: var(--radius); }
+ .card .q { font-size: 1.1rem; }
+ .card .answer { padding: 14px; margin-left: -4px; margin-right: -4px; }
+ .rate { gap: 8px; }
+ .rate button { flex-direction: column; justify-content: center; gap: 4px; min-height: 76px; padding: 8px 4px; text-align: center; }
+ .rate-text { align-items: center; }
+ .session-summary { flex-direction: column; align-items: flex-start; gap: 16px; padding: 20px; }
+ .timeline-stage { margin-left: 0; }
+ .plan-day { grid-template-columns: 58px 1fr; gap: 10px; }
+ .plan-day::before { left: 66px; }
+ .plan-day::after { left: 61px; }
+ .plan-day .tasks { padding-left: 12px; }
+ .plan-date time { font-size: 14px; }
+ .plan-stats .stat b { font-size: 1.15rem; }
+ .d-long { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
+ .d-short { display: inline !important; }
+ .task { padding: 12px; gap: 10px; }
+ .task-check { width: 28px; height: 28px; }
+ .landing h1 { font-size: 2rem; }
+ .proof { gap: 22px; }
+ .steps { grid-template-columns: 1fr; gap: 8px; }
+ .steps li { display: grid; grid-template-columns: auto 1fr; column-gap: 14px; padding: 14px 16px; }
+ .steps .step-icon { grid-row: 1 / 3; margin: 0; }
+ .setup-form.panel { padding: 20px 16px; margin-left: -4px; margin-right: -4px; }
+ .grid.minutes { grid-template-columns: repeat(2, 1fr); }
+ .pager { grid-template-columns: 1fr; }
+ .pager .next { grid-column: 1; }
+ .share { justify-content: center; }
+ .share img { width: 100%; height: auto; max-width: 280px; aspect-ratio: 3 / 4; }
+ .update-bar { bottom: calc(76px + env(safe-area-inset-bottom)); }
+ .toast { bottom: calc(80px + env(safe-area-inset-bottom)); }
+ .demo-ring { display: none; }
+}
+@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; animation: none !important; } }
+@media print { .top, .foot, .actions, .rate, .skip, .day-side { display: none !important; } }
+
+/* 学习记录:导入确认和拖放 */
+.drop-zone { transition: border-color .15s, background .15s; }
+.drop-zone.dragging { border-color: var(--accent); border-style: dashed; background: var(--accent-soft); }
+.compare { display: grid; gap: 10px; margin: 8px 0 12px; }
+.compare dt { color: var(--muted); font-size: 13px; }
+.compare dd { margin: 2px 0 0; color: var(--heading); text-wrap: pretty; }
+#import-title:focus { outline: none; }
+.field-label { display: block; margin: 12px 0 6px; color: var(--heading); font-size: 14px; font-weight: 600; }
+#paste-text { font: 13px/1.5 var(--mono); }
+.backup-note { margin-top: 18px; }
+.backup-note .i { margin-right: 4px; vertical-align: -2px; }
+.prose h2 { font-size: 1.05rem; margin: 1.6em 0 .5em; }
+.prose ul { padding-left: 1.2em; }
+.prose li { margin: 6px 0; }
+
+/* 连续学习的保护和里程碑 */
+.week .ice i { background: var(--accent-soft); color: var(--accent); }
+.week .ice i .i { stroke-width: 2.2; }
+.streak-note { margin: 10px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
+.streak-note .i { margin-right: 4px; vertical-align: -2px; color: var(--accent); }
+.streak-note.ice { color: var(--accent); }
+.milestone { display: inline-flex; align-items: center; gap: 6px; margin: 10px 0 0; padding: 4px 12px; border-radius: 999px; background: var(--flame-soft, rgba(234, 88, 12, .1)); color: var(--flame-text); font-weight: 700; animation: pop .5s cubic-bezier(.2, 1.4, .4, 1) both; }
+.milestone .i { color: var(--flame); }
+@keyframes pop { from { transform: scale(.6); opacity: 0; } to { transform: none; opacity: 1; } }
+
+/* 回答对照 */
+.term-check { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 12px; font-size: 13px; }
+.term-check .muted { flex-basis: 100%; }
+.term { display: inline-flex; align-items: center; gap: 3px; padding: 1px 9px; border-radius: 999px; font: 12.5px/1.8 var(--mono); }
+.term.hit { background: var(--known-soft); color: var(--known); }
+.term.miss { background: var(--surface); color: var(--muted); border: 1px dashed var(--border-strong); }
+.term .i { width: 12px; height: 12px; }
+.d-short { display: none; }
diff --git a/learn/src/app.js b/learn/src/app.js
new file mode 100644
index 000000000..fdb2f973f
--- /dev/null
+++ b/learn/src/app.js
@@ -0,0 +1,1499 @@
+// 学习站的页面:今天、设置、定级、计划、每天的内容、复习和数据。
+// 所有记录只写入 localStorage,不发出除 data.json 之外的任何请求。
+(function () {
+ 'use strict';
+ var C = window.LearnCore;
+ var KEY = 'cs-learn-v1';
+ var main = document.getElementById('main');
+ var data = null, state = null, storageOk = true;
+ // 当前页面的点击处理:main 上只注册一次监听,换页时替换处理函数
+ var onClick = null;
+
+ // ---------- 工具 ----------
+ function esc(value) {
+ return String(value == null ? '' : value).replace(/[&<>"']/g, function (c) {
+ return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
+ });
+ }
+ // 测试时可以用 ?today=YYYY-MM-DD 模拟日期
+ function now() {
+ var forced = new URLSearchParams(location.search).get('today');
+ return C.isDate(forced) ? forced : C.today();
+ }
+ var WEEK = '日一二三四五六';
+ function dateLabel(d, withWeek) {
+ var p = d.split('-').map(Number), date = new Date(p[0], p[1] - 1, p[2]);
+ return p[1] + ' 月 ' + p[2] + ' 日' + (withWeek ? ' 周' + WEEK[date.getDay()] : '');
+ }
+ function relative(d) {
+ var n = C.daysBetween(now(), d);
+ return n <= 0 ? '今天' : n === 1 ? '明天' : n + ' 天后';
+ }
+ // 计划的时间线上用:宽屏写“11 月 2 日”,手机上写“11/2”,读屏软件读完整日期
+ function shortDate(d) {
+ var p = d.split('-').map(Number);
+ return '' + dateLabel(d) + ' ' + p[1] + '/' + p[2] + ' ';
+ }
+ function ago(d) {
+ var n = C.daysBetween(d, now());
+ return n <= 0 ? '今天' : n === 1 ? '昨天' : n + ' 天前';
+ }
+ var toastTimer;
+ function toast(text) {
+ var box = document.getElementById('toast');
+ box.textContent = text;
+ box.hidden = false;
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(function () { box.hidden = true; }, 2600);
+ }
+ function route(id) { return data.routes.filter(function (r) { return r.id === id; })[0]; }
+ function day(id) { return data.days[id]; }
+ function stageOf(d) { return route(d.route).stages[d.stage]; }
+ function totalDays(r) { return r.stages.reduce(function (n, s) { return n + s.days.length; }, 0); }
+ function doneCount(r) { return r.stages.reduce(function (n, s) { return n + s.days.filter(function (d) { return state.done[d]; }).length; }, 0); }
+
+ // ---------- 图标与小部件 ----------
+ // 线条图标,24×24,颜色跟随文字;以 < 开头的是完整的元素,其余是 path 的 d
+ var ICONS = {
+ home: 'M4 11.5 12 5l8 6.5V20h-5v-5h-6v5H4z',
+ plan: ' ',
+ review: 'M19 12a7 7 0 1 1-2.05-4.95M19 4.5V9h-4.5',
+ bank: ' ',
+ progress: 'M5 19v-6M10 19V9M15 19v-4M20 19V5',
+ flag: ' ',
+ shield: ' ',
+ flame: ' ',
+ check: 'M5 12.5l4.5 4.5L19 7.5',
+ x: 'M6.5 6.5l11 11M17.5 6.5l-11 11',
+ wave: 'M4 13.5c2-3.5 4-3.5 6 0s4 3.5 6 0 3-2.5 4-1.5',
+ arrow: 'M5 12h14M13 6l6 6-6 6',
+ back: 'M19 12H5M11 6l-6 6 6 6',
+ clock: ' ',
+ book: 'M12 6.5C10.5 5 8 4.5 4 4.5v13c4 0 6.5.5 8 2 1.5-1.5 4-2 8-2v-13c-4 0-6.5.5-8 2zM12 6.5v13',
+ quiz: ' ',
+ code: 'M8.5 8 4.5 12l4 4M15.5 8l4 4-4 4M13.5 5.5l-3 13',
+ algo: ' ',
+ share: 'M12 4v11M7.5 8.5 12 4l4.5 4.5M5 13v5.5A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5V13',
+ copy: ' ',
+ sparkle: 'M12 3.5l1.9 5.1 5.1 1.9-5.1 1.9L12 17.5l-1.9-5.1L5 10.5l5.1-1.9zM18.5 16l.7 1.8 1.8.7-1.8.7-.7 1.8-.7-1.8-1.8-.7 1.8-.7z',
+ mic: ' ',
+ trophy: 'M8 4.5h8v4.5a4 4 0 0 1-8 0zM8 6.5H5.5a2.5 2.5 0 0 0 2.6 3.4M16 6.5h2.5a2.5 2.5 0 0 1-2.6 3.4M12 13v3.5M8.5 20h7M9.5 16.5h5V20h-5z',
+ search: ' ',
+ chev: 'M9.5 6l6 6-6 6',
+ sliders: ' ',
+ download: 'M12 4v11M7.5 10.5 12 15l4.5-4.5M5 19.5h14',
+ external: 'M14 4.5h5.5V10M19.5 4.5 11 13M17.5 14v4.5a1.5 1.5 0 0 1-1.5 1.5H6a1.5 1.5 0 0 1-1.5-1.5v-10A1.5 1.5 0 0 1 6 7h4.5',
+ bell: 'M6.5 16.5V11a5.5 5.5 0 0 1 11 0v5.5l1.5 1.5H5zM10 20.5h4',
+ layers: 'M12 4 3.5 8.5 12 13l8.5-4.5zM3.5 12.5 12 17l8.5-4.5',
+ target: ' ',
+ lock: ' ',
+ notes: 'M7 4.5h7.5L18 8v11.5H7zM14 4.5V8.5h4M10 12.5h5M10 16h5'
+ };
+ function icon(name, cls) {
+ var body = ICONS[name] || '';
+ return '' +
+ (body.charAt(0) === '<' ? body : ' ') + ' ';
+ }
+ // 环形进度:value / max,中间写 label
+ function ring(value, max, label, sub, size) {
+ size = size || 112;
+ var r = 44, c = 2 * Math.PI * r, pct = max ? Math.min(1, value / max) : 0;
+ return '' +
+ ' ' +
+ (pct > 0 ? ' ' : '') + ' ' +
+ '' + esc(label) + ' ' + (sub ? '' + esc(sub) + ' ' : '') + '
';
+ }
+ // ---------- 配色方案 ----------
+ // 方案只存在本机的 localStorage(cs-learn-skin),不进入学习记录;页面加载前由 index.html 中的脚本先应用,避免闪烁
+ var SKINS = [['', '清爽', '靛蓝主色、白底细边框,默认', ['#5b5bd6', '#eeeefd', '#f6f6f8']], ['paper', '纸面', '暖白纸色、墨绿主色、衬线标题', ['#0f766e', '#e2f0ec', '#f5f1ea']],
+ ['play', '活泼', '亮绿主色、厚边框和立体按钮', ['#18874a', '#e3f6ea', '#f7f9fb']]];
+ var SKIN_THEME = { '': ['#f6f6f8', '#111114'], paper: ['#f5f1ea', '#17150f'], play: ['#f7f9fb', '#0f171d'] };
+ function skin() { var k = document.documentElement.dataset.skin || ''; return SKIN_THEME[k] ? k : ''; }
+ function setSkin(k) {
+ if (!SKIN_THEME[k]) k = '';
+ document.documentElement.dataset.skin = k;
+ try { if (k) localStorage.setItem('cs-learn-skin', k); else localStorage.removeItem('cs-learn-skin'); } catch (e) { /* 不能保存时只对当前页面生效 */ }
+ var metas = document.querySelectorAll('meta[name="theme-color"]');
+ if (metas[0]) metas[0].content = SKIN_THEME[k][0];
+ if (metas[1]) metas[1].content = SKIN_THEME[k][1];
+ }
+ function skinPicker() {
+ var current = skin();
+ return '外观 ' + SKINS.map(function (x) {
+ return ' ' +
+ '' + x[3].map(function (c) { return ' '; }).join('') + ' ' + x[1] + ' ' + x[2] + ' ';
+ }).join('') + '
只影响这个浏览器中的显示,立即生效;深色模式跟随系统。
';
+ }
+ function greeting() {
+ var h = new Date().getHours();
+ return h < 5 ? '夜深了' : h < 11 ? '早上好' : h < 13 ? '中午好' : h < 18 ? '下午好' : '晚上好';
+ }
+
+ // ---------- 存储 ----------
+ // 本页的代码比记录旧(另一个标签页已经用新版本保存过)时只读,不保存,避免用旧格式覆盖新记录
+ var readOnly = false;
+ function load() {
+ var raw = null;
+ try { raw = localStorage.getItem(KEY); } catch (error) { storageOk = false; }
+ if (!raw) return C.emptyState();
+ var parsed = null;
+ try { parsed = JSON.parse(raw); } catch (error) { parsed = null; }
+ if (parsed && typeof parsed.version === 'number' && parsed.version > C.STORE_VERSION) {
+ readOnly = true;
+ showUpdateBar('学习记录已被新版本的学习站更新,刷新页面后继续使用。');
+ return C.emptyState();
+ }
+ try {
+ var state = C.sanitize(parsed, data);
+ // 旧版本的记录或下线内容的整理结果立即写回,之后的读取都是新格式
+ if (JSON.stringify(state) !== raw) { try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (e) { /* 保存失败时下次再试 */ } }
+ return state;
+ } catch (error) {
+ // 记录损坏时保留原文,避免下一次保存覆盖掉
+ try { localStorage.setItem(KEY + '-broken-' + Date.now(), raw); } catch (e) { /* 忽略 */ }
+ return C.emptyState();
+ }
+ }
+ function save() {
+ if (readOnly) { updateBadge(); return; }
+ try { localStorage.setItem(KEY, JSON.stringify(state)); storageOk = true; askPersist(); } catch (error) {
+ if (storageOk) toast('浏览器不允许保存记录(可能是隐私模式),关闭页面后记录会丢失');
+ storageOk = false;
+ }
+ updateBadge();
+ }
+
+ // ---------- 计划与复习 ----------
+ function currentPlan() {
+ if (!state.profile) return null;
+ return C.buildPlan({ route: state.profile.route, minutes: state.profile.minutes, deadline: state.profile.deadline,
+ start: now(), placement: state.placement, done: state.done }, data);
+ }
+ function modes() {
+ return state.profile ? C.dayModes(route(state.profile.route), data, state.placement) : {};
+ }
+ function due() { return C.dueList(state.cards, data.questions, now()); }
+ function updateBadge() {
+ var chip = document.getElementById('streak-chip');
+ if (chip) {
+ var streakDays = state && state.profile ? C.streak(state.log, now(), state.done) : 0;
+ chip.hidden = !state || !state.profile;
+ chip.className = 'streak-chip' + (state && C.activeDays(state.log, state.done)[now()] ? ' lit' : '');
+ chip.innerHTML = icon('flame') + '' + streakDays + ' 天连续学习 ';
+ chip.title = '连续学习 ' + streakDays + ' 天';
+ }
+ var badge = document.getElementById('due-badge'), n = state ? due().length : 0;
+ badge.hidden = !n;
+ badge.textContent = n > 99 ? '99+' : n;
+ badge.setAttribute('aria-label', n + ' 道题到期');
+ }
+ function rate(qid, rating) {
+ var today = now();
+ state.cards[qid] = C.schedule(state.cards[qid] || C.newCard(), rating, today);
+ state.log[today] = (state.log[today] || 0) + 1;
+ save();
+ return state.cards[qid];
+ }
+
+ // ---------- 题卡 ----------
+ var RATINGS = [['不会', C.FORGOT, 'x'], ['模糊', C.FUZZY, 'wave'], ['会', C.KNOWN, 'check']];
+ function cardHtml(q, options) {
+ options = options || {};
+ var card = state.cards[q.id];
+ var meta = [];
+ if (q.heat) meta.push('' + icon('flame', 'sm') + (q.heat > 1 ? '面试最常问' : '面试常问') + ' ');
+ if (options.source) meta.push('' + esc(options.source) + ' ');
+ if (card && card.due && !options.hideDue) meta.push('' + icon('clock', 'sm') + (C.isDue(card, now()) ? '今天复习' : '下次复习:' + relative(card.due)) + ' ');
+ var refs = q.links.map(function (l) {
+ return '' + esc(l.text) + icon('external', 'sm') + ' ';
+ }).join('');
+ var buttons = RATINGS.map(function (r) {
+ var next = C.schedule(card || C.newCard(), r[1], now());
+ return '' +
+ '' + icon(r[2]) + ' ' + r[0] +
+ '' + relative(next.due) + '再看 ' + (r[1] + 1) + ' ';
+ }).join('');
+ return '' +
+ (meta.length ? '' + meta.join('') + '
' : '') +
+ '' + esc(q.text) + '
' +
+ '先写下你的回答可选,用来对照要点和复制给 AI,不保存 ' +
+ '' +
+ '看参考要点
' +
+ '' +
+ '
' + icon('sparkle', 'sm') + '参考要点
' +
+ '
' + esc(q.points) + '
' +
+ '
' +
+ '
对照参考要点,你能答出多少?
' +
+ '
' + buttons + '
' +
+ '
' +
+ '
';
+ }
+ // 反馈题目:打开 GitHub 的新建议题页,预填题号、题目和要点;只有读者自己提交时才会发出,不含学习记录
+ var ISSUES = 'https://github.com/CyC2018/CS-Notes/issues/new';
+ function feedbackUrl(q) {
+ var body = '题号:' + q.id + '\n题目:' + q.text + '\n\n参考要点:\n' + q.points.slice(0, 600) + '\n\n问题(哪里不对、应该怎样):\n';
+ return ISSUES + '?title=' + encodeURIComponent('学习站题目反馈:' + q.text.slice(0, 40)) + '&body=' + encodeURIComponent(body);
+ }
+ function reveal(cardEl) {
+ var answer = cardEl.querySelector('.answer'), button = cardEl.querySelector('[data-action="reveal"]');
+ termCheck(cardEl);
+ answer.hidden = false;
+ button.setAttribute('aria-expanded', 'true');
+ button.hidden = true;
+ // 焦点放在要点上而不是自评按钮上:按空格展开时,松开空格会点到获得焦点的按钮
+ answer.focus({ preventScroll: true });
+ }
+ // 写了回答再看要点时,列出要点中的术语哪些提到了、哪些没提到,帮助自评;没写回答时不显示
+ function termCheck(cardEl) {
+ var box = cardEl.querySelector('.term-check'), q = data.questions[cardEl.dataset.q], input = cardEl.querySelector('[data-answer]');
+ if (box) box.remove();
+ if (!q || !input || input.value.trim().length < 4) return;
+ var terms = C.keyTerms(q.text, q.points);
+ if (terms.length < 2) return;
+ var r = C.compareAnswer(input.value, terms);
+ // 只对照英文术语和带引号的说法,中文表述说对了同样算,所以没提到时不给分数,只提示留意
+ var head = r.hit.length ? '要点里的术语,你的回答提到了 ' + r.hit.length + ' 个:' : '对照要点时留意这些术语(用中文说对了同样算):';
+ var html = '' + head + ' ' +
+ r.hit.map(function (t) { return '' + icon('check', 'sm') + esc(t) + ' '; }).join('') +
+ r.miss.map(function (t) { return '' + esc(t) + ' '; }).join('') + '
';
+ cardEl.querySelector('.answer').insertAdjacentHTML('afterbegin', html);
+ }
+ function copyText(text) {
+ if (navigator.clipboard && window.isSecureContext) return navigator.clipboard.writeText(text);
+ return new Promise(function (resolve, reject) {
+ var area = document.createElement('textarea');
+ area.value = text;
+ area.setAttribute('readonly', '');
+ area.style.cssText = 'position:fixed;top:-200px;opacity:0';
+ document.body.appendChild(area);
+ area.select();
+ var ok = false;
+ try { ok = document.execCommand('copy'); } catch (error) { ok = false; }
+ area.remove();
+ (ok ? resolve : reject)();
+ });
+ }
+ function copyForAi(cardEl) {
+ var q = data.questions[cardEl.dataset.q];
+ copyText(C.aiPrompt(q, cardEl.querySelector('[data-answer]').value)).then(function () {
+ toast('已复制,粘贴到你常用的 AI 应用中追问');
+ }, function () { toast('复制失败,请检查浏览器的剪贴板权限'); });
+ }
+ // 自评后把题卡换成一行结果;onDone 用于复习页切换到下一题
+ function rateCard(cardEl, rating, onDone) {
+ // 记下自评前的卡片,“重新作答”时撤销这一次
+ cardEl._undo = { card: state.cards[cardEl.dataset.q] || null, day: now() };
+ var card = rate(cardEl.dataset.q, rating);
+ cardEl.querySelector('.answer').hidden = true;
+ cardEl.querySelector('textarea').hidden = true;
+ cardEl.querySelector('.answer-label').hidden = true;
+ var note = cardEl.querySelector('.rated');
+ note.hidden = false;
+ cardEl.classList.add('is-rated', 'rated-' + rating);
+ var actions = cardEl.querySelector('.actions');
+ if (actions) actions.hidden = true;
+ note.innerHTML = '' + icon(RATINGS[rating][2]) + ' 已记录:' + RATINGS[rating][0] + ',' + esc(relative(card.due)) + '再复习。 ' +
+ '重新作答 ';
+ if (onDone) onDone(card);
+ }
+ function resetCard(cardEl) {
+ var undo = cardEl._undo, qid = cardEl.dataset.q;
+ if (undo) {
+ if (undo.card) state.cards[qid] = undo.card; else delete state.cards[qid];
+ if (state.log[undo.day] > 1) state.log[undo.day] -= 1; else delete state.log[undo.day];
+ save();
+ }
+ var holder = document.createElement('div');
+ holder.innerHTML = cardHtml(data.questions[cardEl.dataset.q], { source: cardEl.dataset.source });
+ cardEl.replaceWith(holder.firstChild);
+ }
+ // 题卡上的通用操作,各页面共用
+ function handleCard(event, onRated) {
+ var button = event.target.closest('[data-action]');
+ var cardEl = button && button.closest('.card');
+ if (!cardEl) return false;
+ var action = button.dataset.action;
+ if (action === 'reveal') reveal(cardEl);
+ else if (action === 'copy-ai') copyForAi(cardEl);
+ else if (action === 'share-q') shareLink('面试题:' + data.questions[cardEl.dataset.q].text, '这道面试题你能答上来吗?' + data.questions[cardEl.dataset.q].text, questionUrl(cardEl.dataset.q));
+ else if (action === 'rate') {
+ var rating = Number(button.dataset.rating);
+ rateCard(cardEl, rating, onRated && function (card) { onRated(cardEl, card, rating); });
+ }
+ else if (action === 'again') resetCard(cardEl);
+ else return false;
+ return true;
+ }
+
+ // ---------- 页面:设置 ----------
+ var MINUTES = [[60, '1 小时'], [90, '1.5 小时'], [120, '2 小时'], [180, '3 小时']];
+ var ROUTE_ICON = { java: 'code', llm: 'sparkle', sprint: 'target' };
+ function viewSetup() {
+ var p = state.profile || { route: presetRoute, minutes: 90, deadline: null };
+ var routes = data.routes.map(function (r) {
+ var qn = {};
+ r.stages.forEach(function (st) { st.days.forEach(function (id) { day(id).questions.forEach(function (q) { qn[q] = 1; }); }); });
+ return ' ' +
+ '' + icon(ROUTE_ICON[r.id] || 'book') + ' ' +
+ '' + esc(r.title) + ' ' + esc(r.summary) + ' ' +
+ '' + totalDays(r) + ' 天 · ' + r.stages.length + ' 个阶段 · ' + Object.keys(qn).length + ' 道题 ';
+ }).join('');
+ var minutes = MINUTES.map(function (m) {
+ return ' ' +
+ '' + m[1] + ' ';
+ }).join('');
+ var changing = !!state.profile;
+ var retiredRoute = !changing && state.archive.profile ? (data.retired.routes[state.archive.profile.route] || { title: state.archive.profile.route }) : null;
+ var sample = data.questions[data.days['sprint-01'].questions[0]];
+ var qCount = Object.keys(data.questions).length, dCount = data.routes.reduce(function (n, r) { return n + totalDays(r); }, 0);
+ var demo = '' + icon('flame', 'sm') + '面试最常问 面试冲刺 第 1 天
' +
+ '
' + esc(sample.text) + '
' +
+ '
' + icon('x', 'sm') + '不会 ' + icon('wave', 'sm') + '模糊 ' + icon('check', 'sm') + '会3 天后再看
' +
+ '
' + icon('flame') + '12 天连续学习
' +
+ '
' + ring(68, 100, '68%', '路线进度', 84) + '
';
+ var intro = '' + icon('sparkle', 'sm') + 'CS-Notes 学习 · 免费 · 无需注册
先测水平,再按天学, 记到面试那天 ' +
+ '
' + qCount + ' 道面试中的真实问题,分为 ' + data.routes.map(function (r) { return r.title; }).join('、') + '三条路线。已经会的跳过,不会的按天学,学过的按间隔复习。
' +
+ '
选一条路线开始' + icon('arrow') + ' 先做一道示例题 ' +
+ '
' + qCount + ' 道题' + data.routes.length + ' 条路线' + dCount + ' 天的内容' + demo + '
' +
+ '' + icon('target') + ' 定级 每个阶段两三道题,看参考要点后自评,约 10 分钟 ' +
+ '' + icon('plan') + ' 按天学 已经掌握的内容只做自测,其余按路线顺序排进每天 ' +
+ '' + icon('review') + ' 间隔复习 答“会”的题间隔拉长,答“不会”的明天再来 ' +
+ '' + icon('lock', 'sm') + '记录只保存在你的浏览器中(数据说明 )。想先看看:一道示例题 、面试冲刺第 1 天 。
';
+ if (retiredRoute) {
+ intro = '选一条新的路线 你之前学的“' + esc(retiredRoute.title) + '”路线已经下线。已经练过的题和复习安排都保留着,换一条路线后,路线中相同的题会接着按原来的间隔复习。
';
+ }
+ return (changing ? '' + icon('sliders', 'sm') + '设置
调整学习设置 已完成的天和复习记录保留;更换路线后需要重新定级。
' : intro) +
+ '' + (changing ? '' + skinPicker() + '
' : '');
+ }
+ function bindSetup() {
+ var form = document.getElementById('setup');
+ form.addEventListener('click', function (event) {
+ if (event.target.closest('[data-action="clear-date"]')) form.deadline.value = '';
+ });
+ form.addEventListener('submit', function (event) {
+ event.preventDefault();
+ var go = event.submitter ? event.submitter.value : 'save';
+ var routeId = (form.querySelector('input[name="route"]:checked') || {}).value;
+ if (!routeId) { toast('先选一条学习路线'); return; }
+ var deadline = form.deadline.value;
+ if (deadline && (!C.isDate(deadline) || deadline <= now())) { toast('面试日期要晚于今天'); form.deadline.focus(); return; }
+ var minutes = Number((form.querySelector('input[name="minutes"]:checked') || {}).value) || 90;
+ var changedRoute = state.profile && state.profile.route !== routeId;
+ if (!state.profile && !state.archive.profile && !Object.keys(state.cards).length) state.seenUpdate = latestUpdate();
+ state.profile = { route: routeId, minutes: minutes, deadline: deadline || null, start: (state.profile && state.profile.start) || now() };
+ state.archive.profile = null;
+ if (changedRoute || go === 'place') state.placement = C.createPlacement(route(routeId), data);
+ if (go === 'skip') state.placement = null;
+ save();
+ location.hash = go === 'place' || (changedRoute && go === 'save') ? '#/placement' : '#/plan';
+ });
+ }
+
+ // ---------- 页面:定级 ----------
+ var RESULT = { mastered: ['掌握', '只做自测'], partial: ['部分掌握', '答“会”的天只做自测'], weak: ['薄弱', '按路线学习'], unknown: ['未学', '按路线学习'] };
+ function viewPlacement() {
+ if (!state.profile) return viewSetup();
+ if (!state.placement) state.placement = C.createPlacement(route(state.profile.route), data);
+ var r = route(state.profile.route), next = C.nextPlacementQuestion(state.placement);
+ if (!next) return viewPlacementResult();
+ var progress = C.placementProgress(state.placement), stage = r.stages[next.stage], st = state.placement.stages[next.stage];
+ var q = data.questions[next.id];
+ var dots = r.stages.map(function (x, i) { var res = C.stageResult(state.placement.stages[i]); return ' '; }).join('');
+ return '' + icon('target', 'sm') + '定级 · 约 10 分钟
定级:' + esc(r.title) + ' ' +
+ '第 ' + (progress.done + 1) + ' / ' + progress.total + ' 个阶段 ' +
+ '
' +
+ '' + dots + '
' +
+ '' + icon('layers', 'sm') + esc(stage.title) + '本阶段第 ' + (st.asked.length + 1) + ' 题 ' +
+ '按真实情况自评:能说出要点中的主要内容算“会”,只记得一部分算“模糊”。
' +
+ cardHtml(q, { hideDue: true }) +
+ '这个阶段没学过,跳过 ' +
+ '结束定级,剩下的按未学处理
';
+ }
+ function bindPlacement() {
+ onClick = function (event) {
+ var button = event.target.closest('[data-action]');
+ if (!button) return;
+ var action = button.dataset.action;
+ if (action === 'rate') {
+ var cardEl = button.closest('.card'), next = C.nextPlacementQuestion(state.placement);
+ if (!next || next.id !== cardEl.dataset.q) return;
+ C.answerPlacement(state.placement, next.stage, next.id, Number(button.dataset.rating));
+ // 答“会”“模糊”的题加入复习,过几天确认是不是真的记得;答“不会”的还没学,学到那一天时再练
+ if (Number(button.dataset.rating) !== C.FORGOT) rate(next.id, Number(button.dataset.rating));
+ else { state.log[now()] = (state.log[now()] || 0) + 1; save(); } // 仍算作今天的学习量
+ render();
+ } else if (action === 'skip-stage') {
+ C.skipStage(state.placement, Number(button.dataset.stage));
+ save();
+ render();
+ } else if (action === 'finish-placement') {
+ state.placement.stages.forEach(function (s) { if (!C.stageResult(s)) s.skipped = true; });
+ save();
+ render();
+ } else if (action === 'redo-placement') {
+ state.placement = C.createPlacement(route(state.profile.route), data);
+ save();
+ render();
+ } else {
+ handleCard(event);
+ }
+ };
+ }
+ function viewPlacementResult() {
+ var r = route(state.profile.route), plan = currentPlan();
+ var rows = r.stages.map(function (stage, i) {
+ var result = C.stageResult(state.placement.stages[i]) || 'unknown';
+ return '' + esc(stage.title) + ' ' + stage.days.length + ' 天 · ' + RESULT[result][1] + ' ' +
+ '' + RESULT[result][0] + '
';
+ }).join('');
+ return '' + icon('check', 'sm') + '定级完成
定级结果 ' +
+ '' + esc(r.title) + '共 ' + totalDays(r) + ' 天:按路线学习 ' + plan.counts.learn + ' 天,已掌握、只做自测 ' + plan.counts.check + ' 天。' +
+ (plan.dropped.length ? '到面试日期为止时间不够,有 ' + plan.dropped.length + ' 天没有排进计划。' : '') + '
' +
+ '' + rows + '
' +
+ (plan.counts.learn === 0 ? '定级显示所有阶段都已掌握,计划中只有自测。自测时答不上的题,记得打开原文读一遍;也可以先做一轮模拟面试 ,确认自评没有偏乐观。
' : '') +
+ '定级中答“会”和“模糊”的题已经加入复习,过几天会再出现;答“不会”的题学到那一天时再练。
' +
+ '';
+ }
+
+ // ---------- 页面:今天 ----------
+ var MODE = { learn: ['学习', 'learn'], check: ['只做自测', 'mastered'] };
+ function planItemHtml(item, d) {
+ var done = state.done[item.day];
+ return '' +
+ '' + (done ? icon('check') : d.n) + ' ' +
+ '第 ' + d.n + ' 天:' + esc(d.title) + (done ? '(已完成) ' : '') + ' ' +
+ '' + MODE[item.mode][0] + ' ' +
+ '' + esc(stageOf(d).title) + ' ' + icon('clock', 'sm') + '约 ' + item.minutes + ' 分钟 ' +
+ icon('chev', 'task-chev') + ' ';
+ }
+ // 最近 7 天的学习情况,今天在最右边
+ // 连续学习下面的一行说明:保护的次数、刚用掉保护、离下一次保护还差几天
+ function streakNote(today) {
+ var info = C.streakInfo(state.log, today, state.done), last = info.used[info.used.length - 1];
+ if (!info.days) return '学满 ' + C.FREEZE_EVERY + ' 天得到 1 次保护,偶尔中断一天不会清零。
';
+ if (last && C.daysBetween(last, today) <= 2) return '' + icon('shield', 'sm') + dateLabel(last) + '没有学习,用掉 1 次保护,连续天数保住了。
';
+ return '' + icon('shield', 'sm') + (info.freezes ? '有 ' + info.freezes + ' 次保护,中断一天会自动使用' : '再学 ' + (C.FREEZE_EVERY - (info.days % C.FREEZE_EVERY || 0)) + ' 天得到 1 次保护') + '
';
+ }
+ // 连续天数到了值得纪念的数字时,完成页上多一句
+ function milestoneLine() {
+ var n = C.streak(state.log, now(), state.done);
+ return C.activeDays(state.log, state.done)[now()] && C.isMilestone(n) ? '' + icon('flame', 'sm') + '连续学习 ' + n + ' 天' + (n >= 30 ? ',已经是习惯了' : n >= 7 ? ',坚持住了一周以上' : '') + '。
' : '';
+ }
+ function weekStrip(today) {
+ var active = C.activeDays(state.log, state.done), frozen = C.streakInfo(state.log, today, state.done).used, cells = '';
+ for (var i = 6; i >= 0; i--) {
+ var d = C.addDays(today, -i), on = !!active[d], ice = frozen.indexOf(d) !== -1, p = d.split('-').map(Number);
+ var wd = WEEK[new Date(p[0], p[1] - 1, p[2]).getDay()];
+ cells += '' + (i === 0 ? '今' : wd) + ' ' +
+ '' + (on ? icon('check') : ice ? icon('shield') : '') + ' ' + dateLabel(d) + (on ? '学习了' : ice ? '没有学习,用了一次保护' : '没有学习') + ' ';
+ }
+ return '' + cells + ' ';
+ }
+ function latestUpdate() {
+ var list = data.updates || [];
+ return list.length ? list[list.length - 1].seq : 0;
+ }
+ // 内容更新后,按读者自己的记录说明对他有什么影响;和他无关的更新直接记为已读
+ function updatePanel(r) {
+ var u = C.updateSummary(state, data);
+ if (!u) return '';
+ if (!u.relevant) { state.seenUpdate = u.latest; save(); return ''; }
+ var items = [];
+ if (u.fresh.length) items.push('你学过的天里新增了 ' + u.fresh.length + ' 道题 练习新题 ');
+ if (u.newDays.length) {
+ items.push(r.title + '新增了 ' + u.newDays.length + ' 天:' + u.newDays.slice(0, 3).map(function (id) { var d = day(id); return '第 ' + d.n + ' 天:' + esc(d.title) + ' '; }).join('、') +
+ (u.newDays.length > 3 ? ' 等' : '') + ',已经排进计划');
+ }
+ var qLinks = function (ids) { return ids.slice(0, 3).map(function (id) { return '' + esc(data.questions[id].text) + ' '; }).join('、') + (ids.length > 3 ? ' 等' : ''); };
+ if (u.edited.length) items.push('你练过的题中有 ' + u.edited.length + ' 道改了问法或参考要点:' + qLinks(u.edited) + '。复习记录不变,下次复习时以新内容为准');
+ if (u.changedDays.length) items.push('你学过的 ' + u.changedDays.length + ' 天调整了导读或阅读范围:' + u.changedDays.slice(0, 3).map(function (id) { var d = day(id); return '第 ' + d.n + ' 天:' + esc(d.title) + ' '; }).join('、') +
+ (u.changedDays.length > 3 ? ' 等' : '') + ',有空可以再看一眼,不影响完成状态');
+ if (u.missedOlder) items.push('距离你上次打开,学习站更新过多次,更早的调整不再逐条列出;记录都已经按新内容对应好');
+ if (u.retired.length) items.push(u.retired.length + ' 道练过的题已下线,复习记录保留,不再安排复习');
+ return '' + icon('bell') + '内容更新' + (u.date ? ' ' + dateLabel(u.date) + ' ' : '') + ' ' +
+ items.map(function (t) { return '' + t + ' '; }).join('') + ' 知道了
';
+ }
+ // 距离上一次学习过了几天,今天学过或从没学过时为 0
+ function lastActive(today) {
+ var active = C.activeDays(state.log, state.done), days = Object.keys(active).filter(function (d) { return d < today; }).sort();
+ return !days.length || active[today] ? 0 : C.daysBetween(days[days.length - 1], today);
+ }
+ // 首页最上面只给一个下一步:先定级,再复习,再学今天的内容
+ function nextStep(plan, today, dueIds, todays) {
+ var placing = state.placement && C.nextPlacementQuestion(state.placement);
+ if (placing) {
+ var pr = C.placementProgress(state.placement);
+ return { icon: 'target', title: '继续定级', text: '还剩 ' + (pr.total - pr.done) + ' 个阶段,完成后计划会跳过已经掌握的内容。', href: '#/placement', label: '继续定级' };
+ }
+ // 每天的复习有份额(按每天的时间算):做完份额后先学当天的内容,到期的题积压时不会一直挡住新内容
+ var base = C.reviewLimit(state.profile.minutes), limit = base, reviewed = state.log[today] || 0;
+ // 积压超过两批时,今天的份额加倍(复习两批),先把积压降下来再学新内容,避免越积越多
+ if (dueIds.length > base * 2) limit = base * 2;
+ var next = todays.filter(function (item) { return !state.done[item.day]; })[0];
+ if (dueIds.length && (reviewed < limit || !next)) {
+ var gap = lastActive(today), back = gap >= 3 ? '欢迎回来,上次学习是 ' + gap + ' 天前。' : '';
+ var batch = Math.min(dueIds.length, base, reviewed < limit ? Math.max(5, limit - reviewed) : base);
+ var todayTotal = Math.max(batch, Math.min(dueIds.length, limit - reviewed)), rest = dueIds.length - todayTotal;
+ var plan = todayTotal > batch ? '积压较多,今天复习 ' + todayTotal + ' 道(分两批),' : '';
+ return { icon: 'review', title: '先复习 ' + batch + ' 道题', text: back + '约 ' + Math.max(1, Math.round(batch * 1.5)) + ' 分钟。' +
+ (rest > 0 ? (next ? plan + '另外 ' + rest + ' 道留到明天,复习完再学今天的内容。' : '共 ' + dueIds.length + ' 道到期,其余的可以接着复习或留到明天。') : plan ? plan.replace(/,$/, '。') : '趁快要忘的时候回忆一次,记得最牢。'),
+ href: '#/review', label: '开始复习',
+ // 已经复习完一批、积压还没清时,给出先学今天内容的选择,不让新内容一直被挡住
+ // 已经复习完一批、积压还没清时,给出先学今天内容的选择;时间紧的日子可以只复习 5 道,保住连续天数
+ alt: next && reviewed >= base ? { href: '#/day/' + next.day, label: '先学第 ' + day(next.day).n + ' 天' } :
+ batch > QUICK && !reviewed ? { href: '#/review/quick', label: '只有 5 分钟' } : null };
+ }
+ if (next) {
+ var d = day(next.day);
+ var backlog = dueIds.length ? '今天的复习份额做完了,还有 ' + dueIds.length + ' 道到期,学完有空可以接着复习,没空就留到明天。' : '';
+ return { icon: next.mode === 'check' ? 'quiz' : 'book', minutes: next.minutes, stage: stageOf(d).title, title: '第 ' + d.n + ' 天:' + d.title,
+ text: (next.mode === 'check' ? '定级显示已经掌握,只做自测,约 ' : '阅读、自测和动手,约 ') + next.minutes + ' 分钟。' + backlog, href: '#/day/' + next.day, label: next.mode === 'check' ? '开始自测' : '开始学习' };
+ }
+ return null;
+ }
+ function heroHtml(cls, eyebrow, title, text, actions, side) {
+ return '' + eyebrow + '
' +
+ '
' + title + ' ' + (text ? '
' + text + '
' : '') + '
' + actions + '
' +
+ (side ? '' + side + '
' : '') + ' ';
+ }
+ // 路线的各个阶段:完成的天数和当前所在的阶段
+ function stagePath(r, plan) {
+ var nextDay = plan && plan.days.length ? plan.days[0].items.filter(function (i) { return !state.done[i.day]; })[0] : null;
+ var current = nextDay ? day(nextDay.day).stage : -1;
+ // 首页只列到当前阶段之后的两个,其余的在计划页
+ var upto = Math.max(current, 0) + 2, rest = r.stages.length - 1 - upto;
+ return '' + r.stages.map(function (stage, i) {
+ if (i > upto && rest > 1) return '';
+ var n = stage.days.filter(function (id) { return state.done[id]; }).length, all = stage.days.length;
+ var status = n === all ? 'complete' : i === current ? 'current' : n ? 'started' : '';
+ return '' + (n === all ? icon('check') : i + 1) + ' ' +
+ '' + esc(stage.title) + (status === 'current' ? '进行中 ' : '') + ' ' +
+ ' ' +
+ '' + n + ' / ' + all + ' 天 ';
+ }).join('') + (rest > 1 ? '之后还有 ' + rest + ' 个阶段:' + r.stages.slice(upto + 1).map(function (st) { return esc(st.title); }).join('、') + ' ' : '') + ' ';
+ }
+ function viewHome() {
+ if (!state.profile) return viewSetup();
+ var r = route(state.profile.route), plan = currentPlan(), today = now(), dueIds = due();
+ var todays = plan.days.length && plan.days[0].date === today ? plan.days[0].items : [];
+ var finished = doneCount(r), total = totalDays(r), streakDays = C.streak(state.log, today, state.done);
+ var m = C.mastery(r, data, state.cards);
+ var solid = m.reduce(function (n, x) { return n + x.solid; }, 0), qTotal = m.reduce(function (n, x) { return n + x.total; }, 0);
+ var step = nextStep(plan, today, dueIds, todays);
+ var pct = total ? Math.round(finished / total * 100) : 0, progressRing = ring(finished, total, pct + '%', '已完成 ' + finished + ' / ' + total + ' 天');
+ var html = '' + greeting() + ' · ' + esc(r.title) + '
今天 ' + dateLabel(today, true) + ' ' + updatePanel(r);
+ var hero;
+ if (step) {
+ // 时长已经写在说明里,标签只写所在的阶段
+ var chips = step.stage ? '' + icon('layers', 'sm') + esc(step.stage) + ' ' : '';
+ hero = heroHtml('', icon(step.icon, 'sm') + '下一步', esc(step.title), esc(step.text) + (chips ? '
' + chips : ''),
+ '' + step.label + icon('arrow') + ' ' + (step.alt ? '' + esc(step.alt.label) + ' ' : ''), progressRing);
+ } else if (finished === total) {
+ hero = heroHtml('done', icon('trophy', 'sm') + '全部完成', esc(r.title) + '的 ' + total + ' 天都学完了', '接下来靠复习把记住的内容保持住,也可以做一轮模拟面试检验一下。',
+ '模拟面试' + icon('arrow') + ' 查看进度 ', progressRing);
+ } else if (state.profile.deadline && state.profile.deadline <= today) {
+ hero = heroHtml('', icon('mic', 'sm') + '已到面试日期', '祝面试顺利', '可以做一轮模拟面试热身;面试推迟了的话,在设置中修改日期,计划会重新排。',
+ '模拟面试' + icon('arrow') + ' 修改日期 ', progressRing);
+ } else {
+ var tomorrow = plan.days.filter(function (entry) { return entry.date > today; })[0];
+ hero = heroHtml('done', icon('check', 'sm') + '今天的任务完成了', streakDays > 1 ? '已经连续学习 ' + streakDays + ' 天' : '明天继续',
+ tomorrow ? '明天:' + tomorrow.items.map(function (i) { var d = day(i.day); return '第 ' + d.n + ' 天 ' + esc(d.title); }).join('、') + '。想多学一点可以现在开始。' : '',
+ tomorrow ? '提前学明天的内容 模拟面试 ' : '', progressRing);
+ }
+ var mainCol = hero;
+ // 今天只有一项、而且就是上面的下一步时不再重复列出
+ if (todays.length > 1 || (todays.length === 1 && !(step && step.href === '#/day/' + todays[0].day))) {
+ var left = todays.filter(function (i) { return !state.done[i.day]; }).length;
+ mainCol += '
今天的计划 ' + (left ? '还剩 ' + left + ' 项' : '都完成了') + '
' +
+ '' + todays.map(function (item) { return planItemHtml(item, day(item.day)); }).join('') + '
';
+ }
+ mainCol += '' + stagePath(r, plan) + ' ';
+ var last = plan.days[plan.days.length - 1];
+ var side = '' +
+ '
' + icon('flame') + ' ' + streakDays + ' 天连续学习 ' + weekStrip(today) + streakNote(today) + '
' +
+ '
' + icon('plan') + ' ' + finished + ' / ' + total + ' 天已完成
' +
+ '
' + icon('target') + ' ' + solid + ' / ' + qTotal + ' 道题已掌握
';
+ if (last && finished < total) {
+ side += '' + icon('clock', 'sm') + '按现在的计划,' + (plan.dropped.length ? '面试前能学完 ' + (total - plan.dropped.length) + ' / ' + total + ' 天,' : '') + '预计 ' + dateLabel(last.date) + '(' + relative(last.date) + ')学完。没按计划完成时,剩下的内容自动顺延。
';
+ }
+ side += '' +
+ '' + icon('mic') + '模拟面试按热度抽 8 道题 ' + icon('chev', 'task-chev') + ' ' +
+ '' + icon('review') + '复习' + (dueIds.length ? dueIds.length + ' 道到期' : '今天没有到期的题') + ' ' + icon('chev', 'task-chev') + ' ' +
+ '' + icon('bank') + '题库' + Object.keys(data.questions).length + ' 道题,可搜索 ' + icon('chev', 'task-chev') + ' ' +
+ '' + icon('sliders') + '调整设置路线、每天的时间、面试日期 ' + icon('chev', 'task-chev') + ' ';
+ return html + '';
+ }
+
+ // ---------- 页面:计划 ----------
+ function viewPlan() {
+ if (!state.profile) return viewSetup();
+ var r = route(state.profile.route), plan = currentPlan(), today = now();
+ var minutes = MINUTES.filter(function (m) { return m[0] === state.profile.minutes; })[0];
+ var last = plan.days[plan.days.length - 1];
+ var html = '' + icon('plan', 'sm') + esc(r.title) + '
学习计划 ' +
+ '' + icon('clock', 'sm') + '每天 ' + (minutes ? minutes[1] : state.profile.minutes + ' 分钟') + ' ' +
+ (state.profile.deadline ? '' + icon('mic', 'sm') + '面试 ' + dateLabel(state.profile.deadline) + ' ' : '') +
+ '' + icon('sliders', 'sm') + '调整
' +
+ '' + (plan.counts.learn + plan.counts.check) + ' 天 待学' + (plan.counts.check ? ',其中 ' + plan.counts.check + ' 天只做自测' : '') + '
' +
+ '
' + plan.days.length + ' 天 排完需要
' +
+ '
' + (last ? shortDate(last.date) : '—') + ' 预计学完' + (last ? '(' + relative(last.date) + ')' : '') + '
' +
+ '没按计划完成时,剩下的内容会从今天起顺延。
' +
+ (plan.days.length ? '' + icon('download') + '加到日历 导出 .ics 文件,在手机或电脑的日历中打开,每个计划日一个全天提醒
' : '');
+ if (plan.dropped.length) {
+ html += '到面试日期为止时间不够,以下 ' + plan.dropped.length + ' 天没有排进计划(先去掉只需自测的天,再去掉面试中问得最少的内容),有空时可以直接打开:
' +
+ plan.dropped.map(function (id) { var d = day(id); return '第 ' + d.n + ' 天:' + esc(d.title) + ' '; }).join('、') + '
';
+ }
+ var lastStage = null;
+ html += '' + plan.days.map(function (entry) {
+ var first = day(entry.items[0].day), label = '';
+ if (first.stage !== lastStage) { lastStage = first.stage; label = '
' + icon('layers', 'sm') + esc(stageOf(first).title) + '
'; }
+ var p = entry.date.split('-').map(Number), wd = WEEK[new Date(p[0], p[1] - 1, p[2]).getDay()];
+ return label + '
' +
+ (entry.date === today ? '今天' : shortDate(entry.date)) + ' ' + (entry.date === today ? shortDate(entry.date) : '周' + wd) + '
' +
+ entry.items.map(function (item) { return planItemHtml(item, day(item.day)); }).join('') + '
';
+ }).join('') + '
';
+ var done = Object.keys(state.done).filter(function (id) { return day(id) && day(id).route === r.id; })
+ .sort(function (a, b) { return day(a).n - day(b).n; });
+ if (done.length) {
+ html += '' + icon('check', 'sm') + '已完成 ' + done.length + ' 天 ' +
+ done.map(function (id) { var d = day(id); return '第 ' + d.n + ' 天:' + esc(d.title) + ' '; }).join('、') + '
';
+ }
+ return html;
+ }
+
+ // ---------- 页面:每天的内容 ----------
+ function viewDay(id) {
+ var d = day(id);
+ if (!d) {
+ var gone = data.retired && data.retired.days[id];
+ return gone ? '' + esc(gone.title) + ' 路线调整后,这一天的内容已经合并到别的天或下线。完成记录和复习记录都保留着。
'
+ : '没有找到这一天 返回计划
';
+ }
+ var r = route(d.route), mode = state.profile && state.profile.route === d.route ? modes()[id] : null;
+ var siblings = [].concat.apply([], r.stages.map(function (s) { return s.days; })), at = siblings.indexOf(id);
+ var reading = d.reading.map(function (l) {
+ return '' + icon('notes') +
+ '' + esc(l.note || '笔记') + ' ' + esc(l.text) + ' ' + (l.heat ? '' + icon('flame', 'sm') + '常问 ' : '') + icon('external', 'sm') + ' ';
+ }).join('');
+ var rated = d.questions.filter(function (q) { return state.cards[q]; }).length;
+ var steps = [['s-read', 'book', '阅读']].concat([['s-quiz', 'quiz', '自测']], d.practice ? [['s-practice', 'code', '动手']] : [], d.algos.length ? [['s-algo', 'algo', '算法']] : []);
+ function head(i, sid, name, extra) {
+ return '' + icon(steps[i][1]) + '
' + name + ' ' + (extra || '') + '';
+ }
+ var html = '' + icon('back', 'sm') + '计划 ' + esc(r.title) + ' ' + esc(stageOf(d).title) + ' ' +
+ '第 ' + d.n + ' / ' + siblings.length + ' 天
第 ' + d.n + ' 天:' + esc(d.title) + ' ' +
+ '' + (mode === 'check' ? '只做自测 ' : '') +
+ '' + icon('book', 'sm') + d.reading.length + ' 个小节 ' + icon('quiz', 'sm') + d.questions.length + ' 道自测 ' +
+ (d.practice ? '' + icon('code', 'sm') + '1 项动手 ' : '') + (d.algos.length ? '' + icon('algo', 'sm') + d.algos.length + ' 道算法 ' : '') + '
' +
+ (mode === 'check' ? '定级显示这部分已经掌握,直接做下面的自测;有答不上的再读原文。
' : '') +
+ (d.guide ? '' + esc(d.guide) + '
' : '');
+ var body = '' + head(0, 's-read', '阅读') + '' + esc(d.readingText) + '
' +
+ '' + head(1, 's-quiz', '自测', '' + rated + ' / ' + d.questions.length + ' ') +
+ '先自己回答,再看参考要点并自评;自评后加入复习。
' +
+ d.questions.map(function (qid) { return cardHtml(data.questions[qid]); }).join('') + ' ';
+ var i = 2;
+ if (d.practice) {
+ body += '' + head(i++, 's-practice', '动手') + '' + esc(d.practice) + '
' + (d.expect ? '预期结果 ' + esc(d.expect) + '
' : '') + ' ';
+ }
+ if (d.algos.length) {
+ body += '' + head(i++, 's-algo', '算法') + ' ';
+ }
+ body += '' +
+ '';
+ var toc = '';
+ return html + '';
+ }
+ function bindDay(id) {
+ var d = day(id);
+ function counts() {
+ var n = d.questions.filter(function (q) { return state.cards[q]; }).length, all = d.questions.length;
+ var a = document.getElementById('quiz-count'), b = document.getElementById('toc-quiz'), bar = document.getElementById('toc-bar'), st = document.getElementById('toc-state');
+ if (a) a.textContent = n + ' / ' + all;
+ if (b) b.textContent = n + '/' + all;
+ if (bar) bar.style.width = (n / all * 100) + '%';
+ if (st) st.textContent = state.done[id] ? '已完成' : n === all ? '自测都做完了,可以标记完成' : '自评完再标记完成';
+ }
+ onClick = function (event) {
+ if (handleCard(event)) {
+ var current = main.querySelector('[data-action="toggle-done"]');
+ if (current && !state.done[id]) doneButton(current, id);
+ counts();
+ return;
+ }
+ var button = event.target.closest('[data-action="toggle-done"]');
+ if (!button) return;
+ if (state.done[id]) delete state.done[id];
+ else state.done[id] = now();
+ save();
+ // 只更新按钮,不重绘页面,题卡上写了一半的回答和展开的要点都保留
+ doneButton(button, id, !!state.done[id]);
+ if (!state.done[id]) toast('已撤销完成标记');
+ counts();
+ };
+ var button = main.querySelector('[data-action="toggle-done"]');
+ if (button) doneButton(button, id);
+ }
+ function doneButton(button, id, justDone) {
+ var done = state.done[id], d = day(id);
+ button.className = 'btn' + (done ? ' quiet' : ' primary big');
+ button.setAttribute('aria-pressed', String(!!done));
+ button.innerHTML = done ? icon('check') + '已完成(' + dateLabel(done) + '),撤销' : icon('check') + '完成这一天';
+ var note = document.getElementById('done-note');
+ if (!note) return;
+ var unrated = d.questions.filter(function (q) { return !state.cards[q]; }).length;
+ if (!done) {
+ note.innerHTML = unrated ? '还有 ' + unrated + ' 道自测没有自评,自评后才会加入复习。
' : '';
+ return;
+ }
+ if (!justDone) { note.innerHTML = ''; return; }
+ // 刚完成时给出进度和下一步,学完一个阶段时单独说明
+ var r = route(d.route), stage = stageOf(d), plan = state.profile && state.profile.route === d.route ? currentPlan() : null;
+ var stageDone = stage.days.every(function (x) { return state.done[x]; });
+ var next = null, today = false;
+ (plan ? plan.days : []).some(function (entry) {
+ next = entry.items.filter(function (i) { return !state.done[i.day]; })[0];
+ today = entry.date === now();
+ return !!next;
+ });
+ note.innerHTML = '' + icon(stageDone ? 'trophy' : 'check', 'sm') + (stageDone ? '阶段完成' : '第 ' + d.n + ' 天完成') + '
' +
+ '
' + (stageDone ? esc(stage.title) + '的 ' + stage.days.length + ' 天都学完了' : '已完成 ' + doneCount(r) + ' / ' + totalDays(r) + ' 天') + ' ' +
+ '
' + (unrated ? '还有 ' + unrated + ' 道自测没有自评,自评后才会按间隔复习。' : '今天自评的题会在 1~3 天后出现在复习中。') +
+ (C.streak(state.log, now(), state.done) > 1 ? '已经连续学习 ' + C.streak(state.log, now(), state.done) + ' 天。' : '') + '
' +
+ milestoneLine() + '
' +
+ '' + ring(doneCount(r), totalDays(r), Math.round(doneCount(r) / totalDays(r) * 100) + '%', '路线进度') + '
';
+ }
+
+ // ---------- 页面:复习与模拟面试 ----------
+ // 两者共用一套逐题作答的流程。复习进入时取一次到期列表,按每天的时间分批,答“不会”的题明天再出现;
+ // 模拟面试按热度和不牢的程度抽 8 道,自评同样计入复习安排
+ var sessions = { review: null, mock: null, 'new': null };
+ var MOCK_SIZE = 8, QUICK = 5, quickNext = false;
+ function startSession(kind) {
+ var queue;
+ if (kind === 'review') queue = due().slice(0, quickNext ? QUICK : C.reviewLimit(state.profile.minutes));
+ else if (kind === 'new') queue = C.unpracticed(route(state.profile.route), data, state).slice(0, C.reviewLimit(state.profile.minutes));
+ else queue = C.mockSet(route(state.profile.route), data, state.cards, MOCK_SIZE);
+ sessions[kind] = { date: now(), queue: queue, index: 0, counts: [0, 0, 0], missed: [] };
+ return sessions[kind];
+ }
+ function sessionSummary(kind, session) {
+ var total = session.queue.length, c = session.counts;
+ var rest = kind === 'review' ? due().length : kind === 'new' ? C.unpracticed(route(state.profile.route), data, state).length : 0;
+ var missed = session.missed.map(function (id) {
+ var q = data.questions[id], l = q.links[0];
+ return '' + esc(q.text) + ' ' + (l ? ' 读原文' + icon('external', 'sm') + ' ' : '') + ' ';
+ }).join('');
+ var score = Math.round((c[2] + c[1] * 0.5) / total * 100);
+ var html = '' + ring(c[2] + c[1] * 0.5, total, kind === 'mock' ? String(score) : c[2] + '/' + total, kind === 'mock' ? '分' : '答“会”', 132) +
+ '
' + icon(kind === 'mock' ? 'mic' : 'review', 'sm') + { review: '复习', mock: '模拟面试', 'new': '练习新题' }[kind] + ' · ' + total + ' 道题
' +
+ '
' + { review: '这一轮复习完成了', mock: '模拟面试结束', 'new': '新题练完了' }[kind] + ' ' +
+ '
' + icon('check', 'sm') + '' + c[2] + ' 会 ' + icon('wave', 'sm') + '' + c[1] + ' 模糊 ' + icon('x', 'sm') + '' + c[0] + ' 不会
' +
+ (kind === 'mock' ? '
按“会”计 1 分、“模糊”计半分,这一轮得 ' + score + ' 分。' + (score >= 80 ? '高频题基本都能答上来。' : score >= 50 ? '还有一些题需要补,答不上的题明天会出现在复习中。' : '先把答不上的题对应的原文读一遍,明天在复习中再试。') + '
' :
+ '
' + (c[0] ? '答“不会”的题明天再出现,' : '') + '答“会”的题间隔会拉长。' + (kind === 'new' ? (rest ? '还有 ' + rest + ' 道新题。' : '自评过的题已经加入复习。') : rest ? '还有 ' + rest + ' 道到期的题。' : '今天到期的题都复习完了。') + '
');
+ if (missed) html += '
需要再看的题 ';
+ html += milestoneLine();
+ html += '
' +
+ (rest ? '
再' + (kind === 'new' ? '练' : '复习') + ' ' + Math.min(rest, C.reviewLimit(state.profile.minutes)) + ' 道 先到这里 ' :
+ kind === 'mock' ? '
再来一轮 返回今天 ' :
+ '
返回今天 ') + '
';
+ return html;
+ }
+ function viewSession(kind) {
+ if (!state.profile) return viewSetup();
+ var session = sessions[kind];
+ // 上一轮已经做完、又从别的页面进来时,开始新的一轮
+ if (!session || session.date !== now() || quickNext || (entering && session.index >= session.queue.length)) session = startSession(kind);
+ quickNext = false;
+ var total = session.queue.length;
+ if (!total && kind === 'new') {
+ return '' + icon('check') + ' 练习新题 学过的天里没有还没练过的题。
';
+ }
+ if (!total) {
+ var upcoming = Object.keys(state.cards).map(function (id) { return state.cards[id].due; }).filter(Boolean).sort();
+ return '' + icon('check') + ' 复习 今天没有到期的题。' +
+ (upcoming.length ? '下一批在 ' + dateLabel(upcoming[0]) + '(' + relative(upcoming[0]) + '),共 ' + upcoming.filter(function (d) { return d === upcoming[0]; }).length + ' 道。' :
+ '还没有加入复习的题:定级和每天的自测中自评过的题会自动加入。') + '
' +
+ '
复习的间隔是 1、3、7、15、30、60 天:答“会”就拉长一级,“模糊”保持并减半,“不会”从 1 天重新开始。
' +
+ '
';
+ }
+ if (session.index >= total) return sessionSummary(kind, session);
+ var q = data.questions[session.queue[session.index]];
+ var source = q.days.map(function (id) { var d = day(id); return route(d.route).title + ' 第 ' + d.n + ' 天'; })[0];
+ var segs = '';
+ for (var k = 0; k < total; k++) segs += ' ';
+ return '' + icon('x') + ' ' +
+ '
' + (session.index + 1) + ' / ' + total + ' ' +
+ '
' + segs + '
' +
+ '
' + icon('check', 'sm') + session.counts[2] + ' ' + icon('x', 'sm') + session.counts[0] + ' ' +
+ '
' + { review: '复习', mock: '模拟面试', 'new': '练习新题' }[kind] + ' ' +
+ (kind === 'new' && session.index === 0 ? '
这些题是笔记更新后加进你学过的天里的,自评后和其它题一样按间隔复习。
' : '') +
+ (kind === 'mock' && session.index === 0 ? '
像在面试中一样,先用一两分钟把答案说出来或写下来,再看参考要点自评。题目按面试中被问到的频率和你还不牢的程度抽取。
' : '') +
+ cardHtml(q, { source: source, hideDue: true }) +
+ '
快捷键:空格 看参考要点,1 不会,2 模糊,3 会
';
+ }
+ function bindSession(kind) {
+ onClick = function (event) {
+ if (event.target.closest('[data-action="more"]')) { startSession(kind); render(); main.focus({ preventScroll: true }); window.scrollTo(0, 0); return; }
+ handleCard(event, function (cardEl, card, rating) {
+ var session = sessions[kind];
+ session.counts[rating] += 1;
+ if (rating !== C.KNOWN) session.missed.push(cardEl.dataset.q);
+ session.index += 1;
+ setTimeout(function () { render(); }, 250);
+ });
+ };
+ }
+ document.addEventListener('keydown', function (event) {
+ if (['review/', 'mock/', 'new/'].indexOf(currentView) === -1 || event.metaKey || event.ctrlKey || event.altKey || event.isComposing) return;
+ if (event.target.closest && event.target.closest('textarea, input')) return;
+ var cardEl = main.querySelector('.card');
+ if (!cardEl) return;
+ var answer = cardEl.querySelector('.answer');
+ if ((event.key === ' ' || event.key === 'Enter') && answer.hidden && !cardEl.querySelector('.rated:not([hidden])')) {
+ event.preventDefault();
+ reveal(cardEl);
+ } else if (!answer.hidden && /^[123]$/.test(event.key)) {
+ event.preventDefault();
+ cardEl.querySelector('[data-rating="' + (Number(event.key) - 1) + '"]').click();
+ }
+ });
+
+ // ---------- 分享 ----------
+ function siteUrl() { return location.origin + location.pathname.replace(/index\.html$/, ''); }
+ function questionUrl(id) { return siteUrl() + '#/q/' + id; }
+ // 手机上用系统分享面板(可以直接发到微信等应用),电脑上复制链接更顺手
+ function canShareNative() { return !!navigator.share && window.matchMedia('(pointer: coarse)').matches; }
+ function shareLink(title, text, url) {
+ if (canShareNative()) {
+ navigator.share({ title: title, text: text, url: url }).catch(function (error) {
+ if (error && error.name !== 'AbortError') copyText(text + ' ' + url).then(function () { toast('已复制链接'); });
+ });
+ return;
+ }
+ copyText(text + '\n' + url).then(function () { toast('已复制链接,可以粘贴给朋友'); }, function () { toast('复制失败:' + url); });
+ }
+
+ // 分享图片:1080×1440 竖图,适合朋友圈和聊天;全部在本地用 canvas 绘制
+ var CARD = { w: 1080, h: 1440 };
+ function cssVar(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
+ function wrapText(g, text, maxWidth) {
+ var lines = [], line = '';
+ Array.from(text).forEach(function (ch) {
+ if (g.measureText(line + ch).width > maxWidth && line) { lines.push(line); line = ch; } else line += ch;
+ });
+ if (line) lines.push(line);
+ return lines;
+ }
+ // 版面自上而下:标题 120~210、完成天数 250~430、三个数字 470~560、各阶段 610 起,最多到底部文字区上方 40 像素;
+ // 阶段多时按可用高度缩小行高和字号,保证不会压到底部的说明和二维码
+ // 分享图的配色,与 app.css 中各方案的浅色变量一致
+ var SKIN_CARD = {
+ '': { theme: '#5b5bd6', soft: '#e3e3fb', ink: '#15151c', muted: '#5e5e6a', line: '#e4e4ea', bg: '#f6f6f8' },
+ paper: { theme: '#0f766e', soft: '#d6ebe6', ink: '#1f1b16', muted: '#6a6155', line: '#e4dccf', bg: '#f5f1ea' },
+ play: { theme: '#18874a', soft: '#d5f0de', ink: '#17222c', muted: '#5d6974', line: '#e1e8ee', bg: '#f7f9fb' }
+ };
+ function drawShareCard(canvas) {
+ var r = route(state.profile.route), today = now(), m = C.mastery(r, data, state.cards);
+ var finished = doneCount(r), total = totalDays(r), streakDays = C.streak(state.log, today, state.done);
+ var solid = m.reduce(function (n, x) { return n + x.solid; }, 0), seen = Object.keys(state.cards).length;
+ canvas.width = CARD.w; canvas.height = CARD.h;
+ var g = canvas.getContext('2d'), font = '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif';
+ // 固定使用当前配色方案的浅色,不随页面的深色模式变化
+ var pal = SKIN_CARD[skin()] || SKIN_CARD[''];
+ var theme = pal.theme, soft = pal.soft, ink = pal.ink, muted = pal.muted, line = pal.line, left = 120, right = CARD.w - 120;
+ g.textBaseline = 'alphabetic';
+ g.fillStyle = pal.bg; g.fillRect(0, 0, CARD.w, CARD.h);
+ g.fillStyle = '#fff'; roundRect(g, 60, 60, CARD.w - 120, CARD.h - 120, 36); g.fill();
+ g.fillStyle = theme; g.font = '600 34px ' + font; g.fillText('CS-Notes 学习', left, 150);
+ g.fillStyle = muted; g.font = '30px ' + font; g.fillText(dateLabel(today) + ' · ' + r.title, left, 198);
+ g.fillStyle = ink; g.font = '700 132px ' + font; g.fillText(String(finished), left, 360);
+ var w = g.measureText(String(finished)).width;
+ g.fillStyle = muted; g.font = '500 48px ' + font; g.fillText('/ ' + total + ' 天', left + w + 18, 360);
+ g.font = '30px ' + font; g.fillText('已完成的学习天数', left, 408);
+ g.fillStyle = line; roundRect(g, left, 432, right - left, 16, 8); g.fill();
+ g.fillStyle = theme; roundRect(g, left, 432, Math.max(16, (right - left) * finished / total), 16, 8); g.fill();
+ [[streakDays, '天连续学习'], [solid, '道题已掌握'], [seen, '道题练过']].forEach(function (item, i) {
+ var x = left + i * 290;
+ g.fillStyle = ink; g.font = '700 64px ' + font; g.fillText(String(item[0]), x, 540);
+ g.fillStyle = muted; g.font = '28px ' + font; g.fillText(item[1], x, 582);
+ });
+ var qrSize = 220, qrX = right - qrSize, footTop = CARD.h - 120 - qrSize - 30;
+ g.fillStyle = ink; g.font = '600 34px ' + font; g.fillText('各阶段掌握', left, 650);
+ var top = 676, bottom = footTop - 40;
+ if (m.length <= 6) {
+ // 阶段少:一行一个,名称在左、进度条在中间、数字在右
+ var rowH = Math.min(58, (bottom - top) / m.length), size = 28, barH = 20;
+ g.font = size + 'px ' + font;
+ var labelW = Math.min(360, Math.max.apply(null, m.map(function (st) { return g.measureText(st.title).width; })));
+ var barX = left + labelW + 28, barW = right - barX - 110;
+ m.forEach(function (st, i) {
+ var mid = top + i * rowH + rowH / 2;
+ g.textBaseline = 'middle';
+ g.fillStyle = ink; g.font = size + 'px ' + font; g.fillText(fitText(g, st.title, labelW), left, mid);
+ stageBar(g, barX, mid - barH / 2, barW, barH, st, soft, theme);
+ g.fillStyle = muted; g.font = '26px ' + font; g.fillText(st.solid + '/' + st.total, barX + barW + 18, mid);
+ });
+ } else {
+ // 阶段多:分两列,每格上面是名称和数字、下面是进度条,字号不小于 24 像素,手机上看分享图时仍然认得出
+ var colW = (right - left - 48) / 2, perCol = Math.ceil(m.length / 2), cellH = Math.min(64, (bottom - top) / perCol);
+ m.forEach(function (st, i) {
+ var x = left + Math.floor(i / perCol) * (colW + 48), y = top + (i % perCol) * cellH;
+ g.textBaseline = 'alphabetic';
+ g.fillStyle = muted; g.font = '22px ' + font;
+ var count = st.solid + '/' + st.total, countW = g.measureText(count).width;
+ g.fillText(count, x + colW - countW, y + 26);
+ g.fillStyle = ink; g.font = '24px ' + font; g.fillText(fitText(g, st.title, colW - countW - 16), x, y + 26);
+ stageBar(g, x, y + 36, colW, 10, st, soft, theme);
+ });
+ }
+ g.textBaseline = 'alphabetic';
+ // 底部:一句介绍、说明和二维码
+ g.strokeStyle = line; g.lineWidth = 2; g.beginPath(); g.moveTo(left, footTop); g.lineTo(right, footTop); g.stroke();
+ var qrY = footTop + 30;
+ g.fillStyle = ink; g.font = '600 36px ' + font;
+ g.fillText('先定级,再按天学,', left, qrY + 52);
+ g.fillText('用间隔复习记牢面试题', left, qrY + 102);
+ g.fillStyle = muted; g.font = '28px ' + font;
+ wrapText(g, '免费、无需注册,记录只保存在自己的浏览器中。扫码开始。', qrX - left - 40).forEach(function (t, i) { g.fillText(t, left, qrY + 156 + i * 40); });
+ drawQr(g, siteUrl(), qrX, qrY, qrSize);
+ g.strokeStyle = line; g.lineWidth = 2; roundRect(g, 60, 60, CARD.w - 120, CARD.h - 120, 36); g.stroke();
+ }
+ // 超出宽度时截断并加省略号
+ function fitText(g, text, width) {
+ if (g.measureText(text).width <= width) return text;
+ while (g.measureText(text + '…').width > width && text.length > 2) text = text.slice(0, -1);
+ return text + '…';
+ }
+ function stageBar(g, x, y, w, h, st, soft, theme) {
+ g.fillStyle = soft; roundRect(g, x, y, w, h, h / 2); g.fill();
+ if (st.solid) { g.fillStyle = theme; roundRect(g, x, y, Math.max(h, w * st.solid / st.total), h, h / 2); g.fill(); }
+ }
+ function roundRect(g, x, y, w, h, r) {
+ g.beginPath();
+ g.moveTo(x + r, y); g.arcTo(x + w, y, x + w, y + h, r); g.arcTo(x + w, y + h, x, y + h, r);
+ g.arcTo(x, y + h, x, y, r); g.arcTo(x, y, x + w, y, r); g.closePath();
+ }
+ function drawQr(g, text, x, y, size) {
+ var q = window.LearnQR.encode(text), cell = Math.floor(size / (q.size + 2)), offset = (size - cell * q.size) / 2;
+ g.fillStyle = '#fff'; g.fillRect(x, y, size, size);
+ g.fillStyle = '#1f2d33';
+ q.modules.forEach(function (row, yy) { row.forEach(function (dark, xx) { if (dark) g.fillRect(x + offset + xx * cell, y + offset + yy * cell, cell, cell); }); });
+ }
+ function shareCardBlob(callback) {
+ var canvas = document.createElement('canvas');
+ drawShareCard(canvas);
+ canvas.toBlob(callback, 'image/png');
+ }
+
+ // ---------- 页面:进度 ----------
+ function heatmap(today) {
+ // 最近 16 周,每列一周,从周一到周日;颜色按当天自评题数和完成天数分 4 级
+ var weeks = 16, counts = {}, cells = '';
+ Object.keys(state.log).forEach(function (d) { counts[d] = (counts[d] || 0) + state.log[d]; });
+ Object.keys(state.done).forEach(function (id) { var d = state.done[id]; counts[d] = (counts[d] || 0) + 5; });
+ var p = today.split('-').map(Number), weekday = (new Date(p[0], p[1] - 1, p[2]).getDay() + 6) % 7;
+ var first = C.addDays(today, -weekday - (weeks - 1) * 7), active = 0;
+ for (var i = 0; i < weeks * 7; i++) {
+ var d = C.addDays(first, i), n = counts[d] || 0, future = d > today;
+ if (n) active++;
+ var level = !n ? 0 : n < 5 ? 1 : n < 10 ? 2 : n < 20 ? 3 : 4;
+ cells += ' ';
+ }
+ return '' + cells + '
' +
+ '
最近 16 周有 ' + active + ' 天在学习 少 多
';
+ }
+ // 热力图旁边的摘要:本周、最长连续、累计自评
+ function weekSummary(today) {
+ var active = C.activeDays(state.log, state.done), week = 0, best = 0, run = 0;
+ for (var i = 0; i < 7; i++) if (active[C.addDays(today, -i)]) week++;
+ for (var k = 111; k >= 0; k--) { if (active[C.addDays(today, -k)]) { run++; best = Math.max(best, run); } else run = 0; }
+ var reviews = Object.keys(state.log).reduce(function (n, d) { return n + state.log[d]; }, 0);
+ return '
最近 7 天 ' + week + ' 天
最长连续 ' + best + ' 天 ' +
+ '
累计自评 ' + reviews + ' 次 ';
+ }
+ function viewProgress() {
+ if (!state.profile) return viewSetup();
+ var r = route(state.profile.route), today = now(), m = C.mastery(r, data, state.cards);
+ var solid = m.reduce(function (n, x) { return n + x.solid; }, 0), qTotal = m.reduce(function (n, x) { return n + x.total; }, 0);
+ var streakDays = C.streak(state.log, today, state.done);
+ var html = '' + icon('progress', 'sm') + esc(r.title) + ' · 从 ' + dateLabel(state.profile.start || today) + ' 开始
进度 ' +
+ '' + ring(doneCount(r), totalDays(r), Math.round(doneCount(r) / totalDays(r) * 100) + '%', '路线完成', 120) +
+ '
' + doneCount(r) + ' / ' + totalDays(r) + ' 天 ' + r.stages.filter(function (st) { return st.days.every(function (x) { return state.done[x]; }); }).length + ' / ' + r.stages.length + ' 个阶段学完
' +
+ '
' +
+ '
' + icon('flame') + ' ' + streakDays + ' 天连续学习
' +
+ '
' + icon('target') + ' ' + solid + ' / ' + qTotal + ' 道题已掌握
' +
+ '
' + icon('review') + ' ' + Object.keys(state.cards).length + ' 道题练过
' +
+ '
学习记录 最近 16 周 ' + heatmap(today) + weekSummary(today) + '
' +
+ '
各阶段掌握 连续答“会”、复习间隔到 7 天以上的题算掌握;练过但还没到的算复习中。
' +
+ m.map(function (st, i) {
+ var days = r.stages[i].days, doneDays = days.filter(function (d) { return state.done[d]; }).length;
+ return '
' + esc(st.title) + ' ' + doneDays + '/' + days.length + ' 天 · 掌握 ' + st.solid + '/' + st.total + ' 题
' +
+ '
';
+ }).join('') + '
掌握 复习中 没练过
';
+ // 答错过的题:遗忘次数多的在前
+ var shaky = Object.keys(state.cards).filter(function (id) { var c = state.cards[id]; return c.lapses > 0 || (c.stage === 0 && c.reps > 0); })
+ .sort(function (a, b) { return state.cards[b].lapses - state.cards[a].lapses || (data.questions[b].heat - data.questions[a].heat); }).slice(0, 10);
+ if (shaky.length) {
+ html += '' + shaky.map(function (id) {
+ var q = data.questions[id], c = state.cards[id];
+ return '' + esc(q.text) + ' ' + (c.lapses ? '忘过 ' + c.lapses + ' 次' : '上次答“不会”') + ' ';
+ }).join('') + ' ';
+ }
+ html += '' +
+ '
' + icon('share') + '分享进度 生成一张进度图,可以发到朋友圈或学习群;图中的二维码指向本站首页,不包含你的学习记录。
' +
+ (canShareNative() && navigator.canShare ? '' + icon('share') + '分享图片 ' : '') +
+ '' + icon('download') + '保存图片 ' +
+ '' + icon('copy') + '复制邀请链接
' +
+ '';
+ // 练过的题多了、又很久没备份时,在页面底部轻提示一次导出
+ var exported = lastExport(), n = Object.keys(state.cards).length;
+ if (n >= 20 && (!exported || C.daysBetween(exported, now()) >= 14)) {
+ html += '' + icon('lock', 'sm') + n + ' 道题的复习记录只在这个浏览器中,' + (exported ? '上次导出是 ' + ago(exported) : '还没有导出过') + ',换设备或清理浏览器前记得导出 。
';
+ }
+ return html;
+ }
+ function bindProgress() {
+ var preview = document.getElementById('share-preview'), url = null;
+ shareCardBlob(function (blob) { if (blob && preview) { url = URL.createObjectURL(blob); preview.src = url; } });
+ onClick = function (event) {
+ var button = event.target.closest('[data-action]');
+ if (!button) return;
+ var action = button.dataset.action, name = 'cs-notes-learn-' + now() + '.png';
+ if (action === 'save-image') {
+ shareCardBlob(function (blob) { downloadBlob(blob, name); toast('图片已保存'); });
+ } else if (action === 'share-image') {
+ shareCardBlob(function (blob) {
+ var file = new File([blob], name, { type: 'image/png' });
+ if (navigator.canShare && navigator.canShare({ files: [file] })) {
+ navigator.share({ files: [file], title: 'CS-Notes 学习进度' }).catch(function () { /* 取消分享时不提示 */ });
+ } else { downloadBlob(blob, name); toast('当前浏览器不能直接分享图片,已保存到本地'); }
+ });
+ } else if (action === 'share-site') {
+ shareLink('CS-Notes 学习', '我在用 CS-Notes 学习准备面试:先定级,再按天学,用间隔复习记牢面试题。免费、无需注册。', siteUrl());
+ }
+ };
+ }
+ function downloadBlob(blob, name) {
+ var link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = name;
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ setTimeout(function () { URL.revokeObjectURL(link.href); }, 1000);
+ }
+
+ // ---------- 页面:题库 ----------
+ // 全部自测题按路线和阶段排列,可以搜索和筛选;每道题链接到单独的页面
+ var bankQuery = { text: '', filter: 'all', route: null };
+ var FILTERS = [['all', '全部'], ['hot', '面试常问'], ['new', '没练过'], ['shaky', '常忘'], ['solid', '已掌握']];
+ function bankMatch(q) {
+ var c = state.cards[q.id], f = bankQuery.filter;
+ if (f === 'hot' && !q.heat) return false;
+ if (f === 'new' && c) return false;
+ if (f === 'shaky' && !(c && (c.lapses > 0 || (c.stage === 0 && c.reps > 0)))) return false;
+ if (f === 'solid' && !(c && c.stage >= 2)) return false;
+ if (bankQuery.text) {
+ var hay = (q.text + ' ' + q.points).toLowerCase();
+ return bankQuery.text.toLowerCase().split(/\s+/).filter(Boolean).every(function (t) { return hay.indexOf(t) !== -1; });
+ }
+ return true;
+ }
+ function bankList() {
+ var r = route(bankQuery.route), seen = {}, shown = 0, html = '';
+ r.stages.forEach(function (stage) {
+ var rows = '';
+ stage.days.forEach(function (id) {
+ day(id).questions.forEach(function (qid) {
+ if (seen[qid]) return;
+ seen[qid] = true;
+ var q = data.questions[qid], c = state.cards[qid];
+ if (!bankMatch(q)) return;
+ shown++;
+ var status = !c ? '' : c.stage >= 2 ? '已掌握 ' : (c.lapses > 0 || c.stage === 0) ? '常忘 ' : '复习中 ';
+ rows += '' + esc(q.text) + ' ' + (q.heat ? '' + icon('flame', 'sm') + (q.heat > 1 ? '最常问' : '常问') + ' ' : '') + status + icon('chev', 'task-chev') + ' ';
+ });
+ });
+ if (rows) html += '' + esc(stage.title) + ' ';
+ });
+ return { html: html || '' + icon('search') + ' 没有符合条件的题,换个关键词或筛选试试。
', count: shown };
+ }
+ function viewBank() {
+ if (!bankQuery.route) bankQuery.route = state.profile ? state.profile.route : data.routes[0].id;
+ var list = bankList();
+ return '' + icon('bank', 'sm') + Object.keys(data.questions).length + ' 道面试题
题库 ' +
+ '' + data.routes.map(function (r) {
+ return '' + esc(r.title) + ' ';
+ }).join('') + '
' +
+ '' +
+ '' + list.count + ' 道题
' + list.html + '
';
+ }
+ function bindBank() {
+ var input = document.getElementById('bank-search'), timer;
+ function refresh() {
+ var list = bankList();
+ document.getElementById('bank-list').innerHTML = list.html;
+ document.getElementById('bank-count').textContent = list.count + ' 道题';
+ }
+ input.addEventListener('input', function () {
+ clearTimeout(timer);
+ timer = setTimeout(function () { bankQuery.text = input.value.trim(); refresh(); }, 120);
+ });
+ onClick = function (event) {
+ var chip = event.target.closest('.chip');
+ if (!chip) return;
+ if (chip.dataset.route) bankQuery.route = chip.dataset.route;
+ if (chip.dataset.filter) bankQuery.filter = chip.dataset.filter;
+ chip.parentNode.querySelectorAll('.chip').forEach(function (c) { c.setAttribute('aria-pressed', String(c === chip)); });
+ refresh();
+ };
+ }
+
+ // ---------- 页面:单道题(分享出去的链接) ----------
+ function viewQuestion(id) {
+ var q = data.questions[id];
+ if (!q) {
+ var gone = data.retired && data.retired.questions[id];
+ return gone ? '这道题已下线 ' + esc(gone.text) + '
笔记更新后这道题不再出现在学习路线中,复习记录保留,不会再安排复习。
'
+ : '没有找到这道题 链接可能不完整。去题库看看
';
+ }
+ var d = day(q.days[0]), r = route(d.route);
+ var html = '' + icon('back', 'sm') + '题库 ' + esc(r.title) + ' 第 ' + d.n + ' 天:' + esc(d.title) + ' ' +
+ '' + esc(q.text) + ' ' + cardHtml(q, { source: '', hideDue: false });
+ if (!state.profile) {
+ html += heroHtml('', icon('sparkle', 'sm') + 'CS-Notes 学习', '这样的面试题,按计划一道道掌握',
+ '先用 ' + Object.keys(data.questions).length + ' 道真实面试题自评定级,按你的水平和面试日期排出每天的计划,再用间隔复习记牢。免费、无需注册,记录只保存在你的浏览器中。',
+ '开始定级' + icon('arrow') + ' 看这道题所在的一天 ');
+ } else {
+ html += '';
+ }
+ return html;
+ }
+
+ // ---------- 页面:数据 ----------
+ // ---------- 学习记录:导出、导入、数据说明 ----------
+ var EXPORTED = 'cs-learn-exported', pendingImport = null, persisted = null;
+ function lastExport() { try { return localStorage.getItem(EXPORTED); } catch (e) { return null; } }
+ function markExported() { try { localStorage.setItem(EXPORTED, now()); } catch (e) { /* 忽略 */ } }
+ // 导出的文件就是本地记录本身,另加导出日期和来源,导入时忽略这两项
+ function exportText(pretty) {
+ return JSON.stringify(Object.assign({ app: 'cs-notes-learn', exportedAt: now() }, state), null, pretty ? 2 : 0);
+ }
+ // 请浏览器把本站数据当作需要长期保存的数据(不保证同意);只在开始学习后请求一次
+ function askPersist() {
+ if (persisted !== null || !navigator.storage || !navigator.storage.persist || !state.profile) return;
+ persisted = false;
+ navigator.storage.persisted().then(function (yes) { return yes || navigator.storage.persist(); }).then(function (yes) { persisted = !!yes; }, function () {});
+ }
+ function exportNote() {
+ var d = lastExport(), n = Object.keys(state.cards).length;
+ if (d) return '上次导出:' + dateLabel(d) + '(' + ago(d) + ')。';
+ return n ? '还没有导出过。' : '';
+ }
+ function viewData() {
+ var cards = Object.keys(state.cards).length, days = Object.keys(state.done).length, reviews = Object.keys(state.log).reduce(function (n, d) { return n + state.log[d]; }, 0);
+ var html = '' + icon('lock', 'sm') + '只保存在这个浏览器中
学习记录 ' +
+ '学习记录只保存在当前浏览器中,不上传。清除浏览器数据、换浏览器或换设备时记录不会跟着走,可以导出成文件或复制成文字,在另一处导入。数据与隐私说明
' +
+ (storageOk ? '' : '当前浏览器不允许保存记录 (可能是隐私模式),关闭页面后记录会丢失,请及时导出。
') +
+ '' + cards + ' 道题在复习中
' + days + ' 天已完成
' + reviews + ' 次自评
' +
+ (Object.keys(state.archive.cards).length || Object.keys(state.archive.done).length ? '另有 ' + Object.keys(state.archive.cards).length + ' 道题、' + Object.keys(state.archive.done).length +
+ ' 天的记录属于已经下线的内容,保留在记录中、随导出一起保存,内容重新上线时自动恢复。
' : '');
+ if (pendingImport) return html + importPreview();
+ return html +
+ '导出 ' + exportNote() + '文件和文字的内容一样,都可以在另一处导入。
' +
+ '' + icon('download') + '导出成文件 ' +
+ '' + icon('copy') + '复制成文字
' +
+ '在手机和电脑之间转移时,复制成文字后发给自己(例如文件传输助手),在另一台设备上粘贴导入。
' +
+ '' +
+ '' + skinPicker() + '
' +
+ '清空学习记录
' +
+ '关于题目和参考要点 题目来自 CS-Notes 学习路线中的自测题,是面试中的常见问法;参考要点由作者编写并对照原文核对。热度标记来自笔记中的高频小节标记。“复制给 AI 追问”只把题目、你的回答和参考要点复制到剪贴板,由你粘贴到自己使用的 AI 应用中,本站不调用任何 AI 服务。
';
+ }
+ function importPreview() {
+ var inc = C.stateSummary(pendingImport.state, data), cur = C.stateSummary(state, data);
+ var line = function (x) { return (x.route ? esc(x.route) + ',' : '没有选路线,') + x.cards + ' 道题、' + x.done + ' 天' + (x.last ? ',最后学习 ' + dateLabel(x.last) : '') + (x.archived ? ',另有 ' + x.archived + ' 条下线内容的记录' : ''); };
+ var empty = !state.profile && !cur.cards && !cur.done;
+ return '导入前确认 ' +
+ '
要导入的记录' + (pendingImport.exportedAt ? '(' + dateLabel(pendingImport.exportedAt) + ' 导出)' : '') + ' ' + line(inc) + ' ' +
+ (empty ? '' : '
这个浏览器中现有的记录 ' + line(cur) + ' ') + ' ' +
+ (empty ? '' : '合并:两边的记录都保留,同一道题取最近一次自评的那份,路线和每天的时间用这个浏览器的设置。替换:只留下导入的记录,现有的会被覆盖。
') +
+ '' + (empty ? '导入 ' :
+ '合并 替换 ') +
+ '取消
';
+ }
+ function readImport(text) {
+ var raw;
+ try { raw = JSON.parse(String(text).trim()); } catch (e) { throw new Error('内容不是本站导出的记录(无法解析)'); }
+ var imported = C.sanitize(raw, data);
+ var any = function (o) { return Object.keys(o).length > 0; };
+ if (!imported.profile && !imported.archive.profile && !any(imported.cards) && !any(imported.done) && !any(imported.archive.cards)) throw new Error('里面没有可用的学习记录');
+ pendingImport = { state: imported, exportedAt: C.isDate(raw.exportedAt) ? raw.exportedAt : null };
+ render();
+ // 焦点移到确认框的标题上,读屏软件会读出要导入的内容
+ var title = document.getElementById('import-title');
+ if (title) { title.setAttribute('tabindex', '-1'); title.focus({ preventScroll: true }); title.closest('section').scrollIntoView({ block: 'center' }); }
+ }
+ function importFile(file) {
+ if (!file) return;
+ if (file.size > 5 * 1024 * 1024) { toast('文件过大,不是本站导出的记录'); return; }
+ file.text().then(readImport).catch(function (error) { toast('导入失败:' + error.message); });
+ }
+ function bindData() {
+ onClick = function (event) {
+ var button = event.target.closest('[data-action]');
+ if (!button) return;
+ var action = button.dataset.action;
+ if (action === 'export') {
+ downloadBlob(new Blob([exportText(true)], { type: 'application/json' }), 'cs-notes-learn-' + now() + '.json');
+ markExported(); toast('已导出'); render();
+ } else if (action === 'copy-export') {
+ copyText(exportText(false)).then(function () { markExported(); toast('已复制,可以粘贴到另一台设备上导入'); render(); }, function () { toast('复制失败,请改用导出成文件'); });
+ } else if (action === 'import') {
+ document.getElementById('import-file').click();
+ } else if (action === 'paste-open') {
+ var box = document.getElementById('paste-box'); box.hidden = false; document.getElementById('paste-text').focus();
+ } else if (action === 'paste-import') {
+ try { readImport(document.getElementById('paste-text').value); } catch (error) { toast('导入失败:' + error.message); }
+ } else if (action === 'import-merge' || action === 'import-replace') {
+ state = action === 'import-merge' ? C.mergeState(state, pendingImport.state) : pendingImport.state;
+ pendingImport = null;
+ sessions = { review: null, mock: null, 'new': null };
+ save();
+ render();
+ toast((action === 'import-merge' ? '已合并:' : '已导入:') + Object.keys(state.cards).length + ' 道题、' + Object.keys(state.done).length + ' 天');
+ } else if (action === 'import-cancel') {
+ pendingImport = null; render();
+ } else if (action === 'reset') {
+ if (!window.confirm('清空后无法恢复,建议先导出。确定清空全部学习记录吗?')) return;
+ state = C.emptyState();
+ sessions = { review: null, mock: null, 'new': null };
+ save();
+ location.hash = '#/';
+ toast('已清空');
+ }
+ };
+ var input = document.getElementById('import-file');
+ if (input) input.addEventListener('change', function () { importFile(input.files[0]); input.value = ''; });
+ var zone = document.getElementById('drop-zone');
+ if (zone) {
+ ['dragenter', 'dragover'].forEach(function (type) { zone.addEventListener(type, function (event) { event.preventDefault(); zone.classList.add('dragging'); }); });
+ ['dragleave', 'drop'].forEach(function (type) { zone.addEventListener(type, function () { zone.classList.remove('dragging'); }); });
+ zone.addEventListener('drop', function (event) { event.preventDefault(); importFile(event.dataTransfer.files[0]); });
+ }
+ }
+
+ // 数据与隐私:放在页脚和学习记录页中,不在主流程里出现
+ function viewPrivacy() {
+ var keep = persisted ? '浏览器已同意长期保存本站的数据。' : '';
+ return '' + icon('lock', 'sm') + '最后更新:2026 年 10 月 31 日
数据与隐私 ' +
+ '' +
+ '
CS-Notes 学习是一个纯静态的网站:没有账号系统,没有后端服务器,也没有数据库。下面说明使用过程中会产生哪些数据、放在哪里。
' +
+ '
不收集个人信息 ' +
+ '不需要注册或登录,不要求填写姓名、手机号、邮箱等任何个人信息; ' +
+ '不使用 Cookie,不加载统计、广告或其它第三方脚本,页面只请求本站自己的文件; ' +
+ '不调用任何 AI 服务。“复制给 AI 追问”只是把内容放进剪贴板,是否粘贴、粘贴到哪里由你决定。 ' +
+ '
学习记录保存在你的浏览器中 ' +
+ '路线设置、定级结果、完成的天、每道题的复习安排和每天的学习量,保存在当前浏览器的 localStorage 中(cs-learn-v1),不上传、不同步; ' +
+ '外观选择和上次导出的日期也只保存在本机(cs-learn-skin、cs-learn-exported); ' +
+ '题卡中输入的回答只留在当前页面,离开或刷新后就没有了; ' +
+ '为了离线使用,浏览器会缓存本站的页面、脚本和题目文件,其中不含学习记录; ' +
+ '分享图和二维码在浏览器中本地生成,只有你保存或分享时才会离开这台设备。 ' +
+ '
访问日志 和所有网站一样,提供网站托管和加速的服务商在传送页面时,会按其规则记录访问日志(例如 IP 地址、访问时间、浏览器类型),用于运行和安全防护。本站不另外收集这些信息,也不把它们和学习记录关联。
' +
+ '
保存、转移和删除 ' +
+ '记录只在这一个浏览器中。清除浏览器数据、使用隐私模式,或在 Safari 中较长时间没有访问本站时,浏览器可能删除这些记录;建议定期在学习记录 页导出,添加到主屏幕后使用也更不容易被清理。' + keep + ' ' +
+ '导出的文件或文字由你自己保管,导入时只在浏览器中读取,不经过服务器; ' +
+ '在学习记录 页可以随时清空,也可以在浏览器设置中清除本站数据。 ' +
+ '
外部链接和反馈 页面中指向 GitHub 等其它网站的链接,打开后适用那些网站自己的规则。“要点有误”和“反馈建议”会打开 GitHub 的新建议题页并预填题目内容,只有你自己提交时才会发出,不包含学习记录。
' +
+ '
联系 对这些说明有疑问,可以在 GitHub Issues 中提出。说明有变化时会更新本页的日期。
' +
+ '
';
+ }
+
+ // ---------- 路由 ----------
+ var currentView = null, entering = false, presetRoute = null;
+ var TITLES = { home: '今天', setup: '设置', placement: '定级', plan: '学习计划', day: '', q: '', review: '复习', mock: '模拟面试', 'new': '练习新题', bank: '题库', progress: '进度', data: '学习记录', privacy: '数据与隐私' };
+ // 页面出错时不留白屏:说明情况,给出刷新和导出记录的办法(导出不依赖出错的页面)
+ function render() {
+ try { renderPage(); } catch (error) {
+ if (window.console) console.error(error);
+ main.innerHTML = '页面出错了 这个页面没有正常显示,学习记录没有受影响。可以先刷新;如果一直出错,请先导出记录,再到 GitHub 反馈。
' +
+ '
';
+ }
+ }
+ main.addEventListener('click', function (event) {
+ var rescue = event.target.closest && event.target.closest('[data-rescue]');
+ if (!rescue) return;
+ if (rescue.dataset.rescue === 'reload') location.reload();
+ else downloadBlob(new Blob([localStorage.getItem(KEY) || '{}'], { type: 'application/json' }), 'cs-notes-learn-' + now() + '.json');
+ });
+ function renderPage() {
+ var hash = decodeURIComponent(location.hash.replace(/^#\/?/, ''));
+ var parts = hash.split('/'), name = parts[0] || 'home', html;
+ // 从笔记网站的学习路线进来:还没有设置时预先选中这条路线,已经在学时回到今天
+ if (name === 'start') {
+ if (!state.profile && route(parts[1])) presetRoute = parts[1];
+ location.replace('#/' + (state.profile ? '' : 'setup'));
+ return;
+ }
+ entering = currentView !== (parts[0] || 'home') + '/' + parts.slice(1).join('/');
+ onClick = null;
+ // 还没有设置时,除了单独打开某一天和数据页,都显示设置
+ if (!state.profile && ['day', 'data', 'q', 'bank'].indexOf(name) === -1) name = 'setup';
+ if (['home', 'setup', 'placement', 'plan', 'review', 'mock', 'new', 'progress', 'data', 'privacy', 'day', 'q', 'bank'].indexOf(name) === -1) name = 'home';
+ if (name === 'day') html = viewDay(parts[1]);
+ else if (name === 'setup') html = viewSetup();
+ else if (name === 'placement') html = viewPlacement();
+ else if (name === 'plan') html = viewPlan();
+ else if (name === 'review' || name === 'mock' || name === 'new') { if (name === 'review' && parts[1] === 'quick' && entering) quickNext = true; html = viewSession(name); }
+ else if (name === 'data') html = viewData();
+ else if (name === 'privacy') html = viewPrivacy();
+ else if (name === 'progress') html = viewProgress();
+ else if (name === 'q') html = viewQuestion(parts[1]);
+ else if (name === 'bank') html = viewBank();
+ else html = viewHome();
+ var view = name + '/' + parts.slice(1).join('/'), changed = currentView !== view;
+ currentView = view;
+ main.dataset.view = name;
+ main.innerHTML = html;
+ if (name === 'setup' && document.getElementById('setup')) bindSetup();
+ else if (name === 'placement') bindPlacement();
+ else if (name === 'day') bindDay(parts[1]);
+ else if (name === 'review' || name === 'mock' || name === 'new') bindSession(name);
+ else if (name === 'data') bindData();
+ else if (name === 'progress' && state.profile) bindProgress();
+ else if (name === 'bank') bindBank();
+ else if (name === 'plan' && state.profile) onClick = function (event) {
+ if (!event.target.closest('[data-action="calendar"]')) return;
+ var stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
+ downloadBlob(new Blob([C.planCalendar(currentPlan(), data, siteUrl(), stamp)], { type: 'text/calendar' }), 'cs-notes-plan.ics');
+ toast('已导出日历文件,计划调整后可以重新导出');
+ };
+ else onClick = function (event) {
+ var dismiss = event.target.closest('[data-action="dismiss-update"]');
+ if (dismiss) {
+ state.seenUpdate = Math.max(state.seenUpdate || 0, Number(dismiss.dataset.seq) || 0);
+ save();
+ dismiss.closest('.update').remove();
+ return;
+ }
+ handleCard(event);
+ };
+ var heading = main.querySelector('h1');
+ document.title = ((name === 'day' || name === 'q') && heading ? heading.textContent : TITLES[name] || '') + ' · CS-Notes 学习';
+ document.querySelectorAll('[data-nav]').forEach(function (link) {
+ var owner = { day: 'plan', mock: 'review', 'new': 'review', data: 'progress', privacy: 'progress', q: 'bank' }[name] || name;
+ if (link.dataset.nav === owner) link.setAttribute('aria-current', 'page');
+ else link.removeAttribute('aria-current');
+ });
+ updateBadge();
+ if (changed) {
+ window.scrollTo(0, 0);
+ main.focus({ preventScroll: true });
+ }
+ }
+ // 回答框随内容变高,空着时只占两行
+ main.addEventListener('input', function (event) {
+ var t = event.target;
+ if (!t.matches || !t.matches('textarea[data-answer]')) return;
+ t.style.height = 'auto';
+ t.style.height = Math.min(t.scrollHeight + 2, 420) + 'px';
+ });
+ main.addEventListener('change', function (event) {
+ if (event.target.name === 'skin') { setSkin(event.target.value); toast('已切换为“' + SKINS.filter(function (x) { return x[0] === event.target.value; })[0][1] + '”'); }
+ });
+ main.addEventListener('click', function (event) {
+ // 页内跳转:hash 已经用于路由,用按钮滚动到对应的元素
+ var jump = event.target.closest('[data-jump]');
+ if (jump) {
+ var target = document.getElementById(jump.dataset.jump);
+ if (target) {
+ target.scrollIntoView({ behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', block: 'start' });
+ var focusable = target.querySelector('input, button');
+ if (focusable && jump.dataset.jump === 'setup') setTimeout(function () { focusable.focus({ preventScroll: true }); }, 400);
+ }
+ return;
+ }
+ if (onClick) onClick(event);
+ });
+ window.addEventListener('hashchange', render);
+ // 其它标签页修改了记录时同步过来,避免两个标签页互相覆盖
+ window.addEventListener('storage', function (event) {
+ if (event.key !== KEY) return;
+ state = load();
+ sessions = { review: null, mock: null, 'new': null };
+ render();
+ });
+
+ // 离线缓存只在 HTTPS 或本机地址下可用
+ if ('serviceWorker' in navigator && (location.protocol === 'https:' || /^(127\.0\.0\.1|localhost)$/.test(location.hostname))) {
+ // 部署新版本后:打开着的页面在切回前台时检查更新,新版本接管后提示刷新;页面本身先走网络,刷新即可拿到新内容
+ var hadController = !!navigator.serviceWorker.controller;
+ // 新版本接管时,从网络取一次最新的入口页(加查询参数,不读离线缓存):里面引用的脚本和数据版本号和本页不同(本页是旧版本)才提示刷新;
+ // 刚刷新过的页面已经是新版本,不再提示;取不到(离线)时也不提示,反正刷新不了
+ var pageFiles = [document.currentScript.getAttribute('src'), document.currentScript.dataset.src];
+ navigator.serviceWorker.addEventListener('controllerchange', function () {
+ if (!hadController) { hadController = true; return; }
+ fetch('./?check=' + Date.now(), { cache: 'no-store', credentials: 'omit' }).then(function (response) { return response.text(); }).then(function (html) {
+ if (pageFiles.some(function (f) { return html.indexOf('"' + f + '"') === -1; })) showUpdateBar();
+ }).catch(function () { /* 离线时不提示 */ });
+ });
+ window.addEventListener('load', function () {
+ navigator.serviceWorker.register('sw.js').then(function (registration) {
+ document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible') registration.update().catch(function () {}); });
+ }).catch(function () { /* 不支持时照常使用 */ });
+ });
+ }
+
+ function showUpdateBar(message) {
+ if (document.getElementById('update-bar')) return;
+ var bar = document.createElement('div');
+ bar.id = 'update-bar';
+ bar.className = 'update-bar';
+ bar.setAttribute('role', 'status');
+ bar.innerHTML = '' + (message || '学习站已更新,刷新后使用新的内容。你的记录不受影响。') + ' 刷新 ';
+ bar.querySelector('button').addEventListener('click', function () { location.reload(); });
+ document.body.appendChild(bar);
+ }
+
+ setSkin(skin());
+ document.querySelectorAll('[data-nav]').forEach(function (link) { link.insertAdjacentHTML('afterbegin', icon(link.dataset.nav)); });
+ document.querySelector('.notes-link').insertAdjacentHTML('afterbegin', icon('notes'));
+ var source = document.currentScript.dataset.src;
+ fetch(source, { credentials: 'omit' }).then(function (response) {
+ if (!response.ok) throw new Error(response.status);
+ return response.json();
+ }).then(function (loaded) {
+ if (loaded.version !== 1) throw new Error('数据版本不兼容');
+ data = loaded;
+ state = load();
+ render();
+ }).catch(function () {
+ main.innerHTML = '载入失败 题目数据没有载入,请检查网络后重试。学习记录保存在浏览器中,不受影响。
重试
';
+ });
+})();
diff --git a/learn/src/core.js b/learn/src/core.js
new file mode 100644
index 000000000..5574c1989
--- /dev/null
+++ b/learn/src/core.js
@@ -0,0 +1,583 @@
+// 学习站的核心逻辑:日期、复习调度、定级、学习计划、提示词和本地数据。
+// 不访问页面和存储,浏览器和 Node 测试共用同一份代码。
+(function (root, factory) {
+ if (typeof module === 'object' && module.exports) module.exports = factory();
+ else root.LearnCore = factory();
+})(typeof self !== 'undefined' ? self : this, function () {
+ 'use strict';
+
+ // ---------- 日期:一律用本地日期的 YYYY-MM-DD,避免时区让“今天”跨天 ----------
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
+ function today(now) {
+ var d = now ? new Date(now) : new Date();
+ return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
+ }
+ function parseDate(s) {
+ var p = String(s).split('-').map(Number);
+ return new Date(p[0], p[1] - 1, p[2]);
+ }
+ function addDays(s, n) {
+ var d = parseDate(s);
+ d.setDate(d.getDate() + n);
+ return today(d);
+ }
+ function daysBetween(a, b) {
+ return Math.round((parseDate(b) - parseDate(a)) / 86400000);
+ }
+ function isDate(s) {
+ return typeof s === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(s) && today(parseDate(s)) === s;
+ }
+
+ // ---------- 复习调度 ----------
+ // 固定间隔:答“会”时间隔逐级拉长,“模糊”保持当前级别并减半,“不会”回到第一级。
+ // 不用 SM-2、FSRS:题卡只有几百张,自评只有三档,固定间隔可解释,读者能预期下次什么时候出现。
+ var INTERVALS = [1, 3, 7, 15, 30, 60];
+ var FORGOT = 0, FUZZY = 1, KNOWN = 2;
+
+ function newCard() { return { stage: -1, due: null, reps: 0, lapses: 0, last: null }; }
+
+ function schedule(card, rating, on) {
+ var next = Object.assign(newCard(), card);
+ var interval;
+ if (rating === KNOWN) {
+ next.stage = Math.min(Math.max(next.stage, 0) + 1, INTERVALS.length - 1);
+ interval = INTERVALS[next.stage];
+ } else if (rating === FUZZY) {
+ next.stage = Math.max(next.stage, 0);
+ interval = Math.max(1, Math.floor(INTERVALS[next.stage] / 2));
+ } else if (rating === FORGOT) {
+ if (next.stage > 0) next.lapses += 1;
+ next.stage = 0;
+ interval = INTERVALS[0];
+ } else {
+ throw new Error('未知的自评:' + rating);
+ }
+ next.reps += 1;
+ next.last = on;
+ next.due = addDays(on, interval);
+ return next;
+ }
+
+ function isDue(card, on) { return !!card && !!card.due && card.due <= on; }
+
+ // 今天要复习的题:先到期最久的,同样到期的高频题优先
+ function dueList(cards, questions, on) {
+ return Object.keys(cards).filter(function (id) { return questions[id] && isDue(cards[id], on); })
+ .sort(function (a, b) {
+ return cards[a].due.localeCompare(cards[b].due) || (questions[b].heat - questions[a].heat) || a.localeCompare(b);
+ });
+ }
+
+ // 有学习活动的日期:当天自评过题,或完成了计划中的一天
+ function activeDays(log, done) {
+ var days = {};
+ Object.keys(log || {}).forEach(function (d) { if (log[d] > 0) days[d] = true; });
+ Object.keys(done || {}).forEach(function (id) { days[done[id]] = true; });
+ return days;
+ }
+ // 连续学习天数:从今天(今天还没学则从昨天)往前数
+ // 连续学习:每连续学满 7 天得到 1 次“保护”(最多存 2 次),某天没学时自动用掉 1 次,连续天数不断(没学的那天不计数)。
+ // 偶尔有事中断一天不至于前功尽弃;连着几天不学仍会归零。今天还没学时不算中断
+ var FREEZE_EVERY = 7, FREEZE_MAX = 2;
+ function streakInfo(log, on, done) {
+ var active = activeDays(log, done), days = Object.keys(active).filter(function (d) { return d <= on; }).sort();
+ var out = { days: 0, freezes: 0, used: [] };
+ if (!days.length) return out;
+ var run = 0, earned = 0, freezes = 0, used = [];
+ for (var d = days[0]; d <= on; d = addDays(d, 1)) {
+ if (active[d]) {
+ run += 1; earned += 1;
+ if (earned === FREEZE_EVERY) { freezes = Math.min(FREEZE_MAX, freezes + 1); earned = 0; }
+ } else if (d === on) {
+ break;
+ } else if (freezes > 0 && run > 0) {
+ freezes -= 1; used.push(d);
+ } else {
+ run = 0; earned = 0; used = [];
+ }
+ }
+ out.days = run; out.freezes = run ? freezes : 0; out.used = run ? used : [];
+ return out;
+ }
+ function streak(log, on, done) { return streakInfo(log, on, done).days; }
+ // 值得庆祝的连续天数
+ var MILESTONES = [3, 7, 14, 21, 30, 50, 75, 100, 150, 200, 365];
+ function isMilestone(n) { return MILESTONES.indexOf(n) !== -1 || (n > 365 && n % 100 === 0); }
+ // 一次复习的题数上限,约占每天时间的三成(每题约 1.5 分钟);超出的留到下一次,避免中断几天后一次面对几十道题
+ function reviewLimit(minutes) {
+ return Math.max(15, Math.round((minutes || 90) * 0.3 / 1.5));
+ }
+ // 某条路线各阶段的掌握情况:连续答“会”到间隔 7 天以上算掌握,自评过但还没到算复习中
+ function mastery(route, data, cards) {
+ return route.stages.map(function (stage) {
+ var ids = [];
+ stage.days.forEach(function (d) { data.days[d].questions.forEach(function (q) { if (ids.indexOf(q) === -1) ids.push(q); }); });
+ var solid = 0, learning = 0;
+ ids.forEach(function (q) {
+ var c = cards[q];
+ if (c && c.stage >= 2) solid += 1;
+ else if (c && c.stage >= 0) learning += 1;
+ });
+ return { title: stage.title, total: ids.length, solid: solid, learning: learning };
+ });
+ }
+
+ // ---------- 定级 ----------
+ // 每个阶段先问两道题(高频优先、来自不同的天);两道结论一致就判定,不一致再问第三道。
+ // 读者可以整段跳过没学过的阶段。结果按阶段给出“掌握、部分、薄弱、未学”。
+ function stageCandidates(route, stageIndex, data) {
+ var stage = route.stages[stageIndex], seen = {}, list = [];
+ stage.days.forEach(function (dayId, order) {
+ data.days[dayId].questions.forEach(function (qid, i) {
+ if (seen[qid]) return;
+ seen[qid] = true;
+ list.push({ id: qid, day: dayId, heat: data.questions[qid].heat, order: order * 10 + i });
+ });
+ });
+ // 高频在前;同热度按路线顺序,保证能覆盖阶段中不同的天
+ list.sort(function (a, b) { return b.heat - a.heat || a.order - b.order; });
+ var picked = [], days = {};
+ list.forEach(function (c) { if (!days[c.day]) { days[c.day] = true; picked.push(c.id); } });
+ list.forEach(function (c) { if (picked.indexOf(c.id) === -1) picked.push(c.id); });
+ return picked;
+ }
+
+ // 每个阶段记下标题:路线增删阶段后按标题找回定级结果
+ function createPlacement(route, data) {
+ return {
+ route: route.id,
+ stages: route.stages.map(function (s, i) { return { title: s.title, pool: stageCandidates(route, i, data), asked: [], ratings: {}, skipped: false }; })
+ };
+ }
+
+ function stageResult(stage) {
+ if (stage.skipped) return 'unknown';
+ var values = stage.asked.map(function (id) { return stage.ratings[id]; }).filter(function (v) { return v !== undefined; });
+ if (!values.length) return null;
+ var known = values.filter(function (v) { return v === KNOWN; }).length;
+ var forgot = values.filter(function (v) { return v === FORGOT; }).length;
+ if (values.length >= 2 && known === values.length) return 'mastered';
+ if (values.length >= 2 && forgot === values.length) return 'weak';
+ if (values.length >= 3 || values.length >= stage.pool.length) {
+ // “模糊”算半个“会”:都答模糊时是部分掌握,答“会”的天在计划中只做自测,其余的天照常学
+ var fuzzy = values.length - known - forgot;
+ return (known + fuzzy / 2) * 2 >= values.length ? 'partial' : 'weak';
+ }
+ return null;
+ }
+
+ // 下一道题:{stage, id};全部问完返回 null
+ function nextPlacementQuestion(placement) {
+ for (var i = 0; i < placement.stages.length; i++) {
+ var stage = placement.stages[i];
+ if (stageResult(stage)) continue;
+ var id = stage.pool.filter(function (q) { return stage.asked.indexOf(q) === -1; })[0];
+ if (id) return { stage: i, id: id };
+ }
+ return null;
+ }
+
+ function answerPlacement(placement, stageIndex, id, rating) {
+ var stage = placement.stages[stageIndex];
+ if (stage.asked.indexOf(id) === -1) stage.asked.push(id);
+ stage.ratings[id] = rating;
+ return placement;
+ }
+
+ function skipStage(placement, stageIndex) {
+ placement.stages[stageIndex].skipped = true;
+ return placement;
+ }
+
+ function placementProgress(placement) {
+ var done = placement.stages.filter(function (s) { return stageResult(s); }).length;
+ return { done: done, total: placement.stages.length };
+ }
+
+ // ---------- 学习计划 ----------
+ // 每天的设计用时(学习路线说明中写的范围取中间值),已掌握的天只做当天的自测
+ var DAY_MINUTES = { java: 75, llm: 75, sprint: 105 };
+ var REVIEW_MINUTES = 15;
+ var REVIEW_RESERVE = 15;
+
+ // 每天的处理方式:learn 按路线学习,check 只做自测确认(定级显示已掌握)
+ function dayModes(route, data, placement) {
+ var modes = {};
+ route.stages.forEach(function (stage, i) {
+ var st = placement ? placement.stages[i] : null;
+ var result = st ? stageResult(st) : null;
+ stage.days.forEach(function (dayId) {
+ var mode = 'learn';
+ if (result === 'mastered') mode = 'check';
+ else if (result === 'partial') {
+ // 这一天被问到的题都答“会”,才认为这一天已掌握
+ var asked = data.days[dayId].questions.filter(function (q) { return st.ratings[q] !== undefined; });
+ if (asked.length && asked.every(function (q) { return st.ratings[q] === KNOWN; })) mode = 'check';
+ }
+ modes[dayId] = mode;
+ });
+ });
+ return modes;
+ }
+
+ function dayHeat(dayId, data) {
+ return data.days[dayId].questions.reduce(function (sum, q) { return sum + data.questions[q].heat; }, 0);
+ }
+
+ // 生成计划:保持路线顺序(前面的内容是后面的基础),按每天的可用时间装箱;
+ // 有面试日期且时间不够时,先去掉“只做自测”的天,再去掉热度最低的学习日
+ function buildPlan(options, data) {
+ var route = data.routes.filter(function (r) { return r.id === options.route; })[0];
+ if (!route) throw new Error('未知的路线:' + options.route);
+ var budget = Math.max(30, options.minutes || 90);
+ // 每天先留出复习的时间,剩下的用来学新内容
+ var room = Math.max(30, budget - REVIEW_RESERVE);
+ var modes = dayModes(route, data, options.placement);
+ var items = [], doneToday = [];
+ route.stages.forEach(function (stage) {
+ stage.days.forEach(function (dayId) {
+ var doneOn = options.done && options.done[dayId];
+ // 今天完成的天仍算在今天,占用今天的时间;否则完成一天后,明天的内容会被顺延到今天
+ if (doneOn && doneOn === options.start) {
+ var m = modes[dayId];
+ doneToday.push({ day: dayId, mode: m, minutes: m === 'learn' ? DAY_MINUTES[route.id] || 75 : REVIEW_MINUTES, heat: dayHeat(dayId, data), done: true });
+ return;
+ }
+ if (doneOn) return;
+ var mode = modes[dayId];
+ items.push({ day: dayId, mode: mode, minutes: mode === 'learn' ? DAY_MINUTES[route.id] || 75 : REVIEW_MINUTES, heat: dayHeat(dayId, data) });
+ });
+ });
+ var start = options.start;
+ var available = options.deadline && isDate(options.deadline) ? Math.max(0, daysBetween(start, options.deadline)) : null;
+ var dropped = [];
+ var usedToday = doneToday.reduce(function (n, i) { return n + i.minutes; }, 0);
+ function pack(list) {
+ var days = doneToday.length ? [doneToday.slice()] : [], used = usedToday;
+ list.forEach(function (item) {
+ // 一项超过当天剩余时间时放到下一天;一天至少放一项,即使它本身超过预算
+ if (!days.length || (used > 0 && used + item.minutes > room * 1.15)) { days.push([]); used = 0; }
+ days[days.length - 1].push(item);
+ used += item.minutes;
+ });
+ return days;
+ }
+ var calendar = pack(items);
+ if (available !== null) {
+ // 每天之外还要留出复习的时间,计划只排到面试前一天
+ var order = items.slice().sort(function (a, b) {
+ return (a.mode === 'check' ? 0 : 1) - (b.mode === 'check' ? 0 : 1) || a.heat - b.heat || items.indexOf(b) - items.indexOf(a);
+ });
+ while (calendar.length > available && order.length) {
+ var cut = order.shift();
+ dropped.push(cut);
+ items = items.filter(function (i) { return i !== cut; });
+ calendar = pack(items);
+ }
+ }
+ return {
+ route: route.id, budget: budget, start: start, deadline: options.deadline || null,
+ days: calendar.map(function (list, i) { return { date: addDays(start, i), items: list }; }),
+ dropped: dropped.map(function (d) { return d.day; }),
+ counts: {
+ learn: items.filter(function (i) { return i.mode === 'learn'; }).length,
+ check: items.filter(function (i) { return i.mode === 'check'; }).length
+ }
+ };
+ }
+
+ // ---------- 复制给 AI 的提示词 ----------
+ function aiPrompt(question, answer) {
+ var refs = question.links.map(function (l) { return '- ' + l.text + ':' + l.href; }).join('\n');
+ var mine = answer && answer.trim() ? answer.trim() : '(还没作答。请先给一个提示,不要直接给出完整答案。)';
+ return [
+ '我在准备技术面试,下面是一道题和我的回答。',
+ '请先指出我的回答中正确、错误和遗漏的地方,再像面试官一样追问 2~3 个相关问题,每次只问一个,等我回答后再继续。',
+ '参考要点来自 CS-Notes,已经人工核对;如果你的看法与参考要点不一致,请说明依据。',
+ '',
+ '题目:' + question.text,
+ '',
+ '我的回答:' + mine,
+ '',
+ '参考要点:' + question.points,
+ '',
+ '原文:',
+ refs
+ ].join('\n');
+ }
+
+ // ---------- 模拟面试 ----------
+ // 从路线中抽题:高频题优先,自评过但还不牢的题加分,random 用于打乱同分的题
+ function mockSet(route, data, cards, n, random) {
+ random = random || Math.random;
+ var ids = [];
+ route.stages.forEach(function (stage) {
+ stage.days.forEach(function (d) { data.days[d].questions.forEach(function (q) { if (ids.indexOf(q) === -1) ids.push(q); }); });
+ });
+ return ids.map(function (q) {
+ var c = cards[q], shaky = c && c.stage >= 0 && c.stage < 2;
+ return { id: q, score: data.questions[q].heat * 2 + (shaky ? 3 : 0) + random() * 2 };
+ }).sort(function (a, b) { return b.score - a.score; }).slice(0, n).map(function (x) { return x.id; });
+ }
+
+ // ---------- 日历 ----------
+ // 把计划导出为 iCalendar(RFC 5545):每个计划日一个全天事件
+ function icsText(value) {
+ return String(value).replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\r?\n/g, '\\n');
+ }
+ // 每行不超过 75 个字节,续行以空格开头;按 UTF-8 计算,不拆开一个字符
+ function icsFold(line) {
+ var out = [], current = '', bytes = 0;
+ Array.from(line).forEach(function (ch) {
+ var cp = ch.codePointAt(0), size = cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
+ if (bytes + size > (out.length ? 74 : 75)) { out.push(current); current = ''; bytes = 0; }
+ current += ch;
+ bytes += size;
+ });
+ out.push(current);
+ return out.join('\r\n ');
+ }
+ function planCalendar(plan, data, siteUrl, stamp) {
+ var lines = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//CS-Notes//Learn//ZH', 'CALSCALE:GREGORIAN', 'X-WR-CALNAME:CS-Notes 学习计划'];
+ plan.days.forEach(function (entry) {
+ var titles = entry.items.map(function (i) { var d = data.days[i.day]; return '第 ' + d.n + ' 天:' + d.title; });
+ var date = entry.date.replace(/-/g, '');
+ lines.push('BEGIN:VEVENT', 'UID:' + plan.route + '-' + date + '@cs-notes-learn', 'DTSTAMP:' + stamp,
+ 'DTSTART;VALUE=DATE:' + date, 'DTEND;VALUE=DATE:' + addDays(entry.date, 1).replace(/-/g, ''),
+ 'SUMMARY:' + icsText('CS-Notes ' + titles.join('、')),
+ 'DESCRIPTION:' + icsText(entry.items.map(function (i) { return siteUrl + '#/day/' + i.day; }).join('\n')),
+ 'URL:' + siteUrl + '#/day/' + entry.items[0].day, 'TRANSP:TRANSPARENT', 'END:VEVENT');
+ });
+ lines.push('END:VCALENDAR');
+ return lines.map(icsFold).join('\r\n') + '\r\n';
+ }
+
+ // ---------- 本地数据 ----------
+ // 版本 2 起增加 archive(下线内容的记录)和 seenUpdate(读者看过的最后一条更新记录)
+ var STORE_VERSION = 2;
+ var ARCHIVE_LIMIT = 2000;
+ function emptyState() {
+ return { version: STORE_VERSION, profile: null, placement: null, cards: {}, done: {}, log: {}, archive: { cards: {}, done: {}, profile: null }, seenUpdate: 0 };
+ }
+ function cleanCard(c) {
+ if (!c || typeof c !== 'object') return null;
+ var stage = Number(c.stage);
+ if (!(stage >= -1 && stage < INTERVALS.length && stage === Math.floor(stage))) return null;
+ return {
+ stage: stage, due: isDate(c.due) ? c.due : null,
+ reps: Math.max(0, Math.floor(Number(c.reps) || 0)), lapses: Math.max(0, Math.floor(Number(c.lapses) || 0)),
+ last: isDate(c.last) ? c.last : null
+ };
+ }
+ function cleanProfile(p) {
+ if (!p || typeof p !== 'object' || typeof p.route !== 'string' || !/^[a-z][a-z0-9-]{0,30}$/.test(p.route)) return null;
+ var minutes = Number(p.minutes);
+ return {
+ route: p.route,
+ minutes: [60, 90, 120, 180].indexOf(minutes) !== -1 ? minutes : 90,
+ deadline: isDate(p.deadline) ? p.deadline : null,
+ start: isDate(p.start) ? p.start : null
+ };
+ }
+ // 编号只允许小写字母和数字;查找时只看对象自身的属性,避免 constructor 这类名字碰到原型上的属性
+ var QUESTION_ID = /^[0-9a-z]{1,32}$/, DAY_ID = /^[a-z][a-z0-9]*-\d{2,}$/;
+ function own(map, key) { return !!map && Object.prototype.hasOwnProperty.call(map, key); }
+ function limit(map) {
+ var keys = Object.keys(map);
+ if (keys.length <= ARCHIVE_LIMIT) return map;
+ var out = {};
+ keys.slice(keys.length - ARCHIVE_LIMIT).forEach(function (k) { out[k] = map[k]; });
+ return out;
+ }
+
+ // 读入记录:来自本机的旧版本或用户选择的文件,逐项检查,只保留认识的字段。
+ // 题目或天在当前内容中找不到(已下线,或来自更新的版本)时放进 archive,不丢弃;它们重新上线后自动恢复。
+ function sanitize(input, data) {
+ if (!input || typeof input !== 'object' || typeof input.version !== 'number' || input.version < 1) throw new Error('不是本站导出的学习记录');
+ if (input.version > STORE_VERSION) throw new Error('这份记录来自更新版本的学习站,请刷新页面后再试');
+ var questions = data.questions, days = data.days || {}, routes = data.routes;
+ var out = emptyState(), routeIds = routes.map(function (r) { return r.id; });
+ var archive = input.archive && typeof input.archive === 'object' ? input.archive : {};
+ function eachCard(map, fn) {
+ if (!map || typeof map !== 'object') return;
+ Object.keys(map).forEach(function (id) { if (QUESTION_ID.test(id)) { var c = cleanCard(map[id]); if (c) fn(id, c); } });
+ }
+ // 先放归档中的,再放当前的:同一道题两边都有时以当前的为准
+ [archive.cards, input.cards].forEach(function (map) {
+ eachCard(map, function (id, c) {
+ if (own(questions, id)) { out.cards[id] = c; delete out.archive.cards[id]; } else out.archive.cards[id] = c;
+ });
+ });
+ [archive.done, input.done].forEach(function (map) {
+ if (!map || typeof map !== 'object') return;
+ Object.keys(map).forEach(function (id) {
+ if (!DAY_ID.test(id) || !isDate(map[id])) return;
+ if (own(days, id)) { out.done[id] = map[id]; delete out.archive.done[id]; } else out.archive.done[id] = map[id];
+ });
+ });
+ out.archive.cards = limit(out.archive.cards);
+ out.archive.done = limit(out.archive.done);
+ var log = input.log && typeof input.log === 'object' ? input.log : {};
+ Object.keys(log).forEach(function (day) {
+ var n = Math.floor(Number(log[day]));
+ if (isDate(day) && n > 0) out.log[day] = Math.min(n, 10000);
+ });
+ // 每天一条,最多保留 10 年
+ var keys = Object.keys(out.log).sort();
+ if (keys.length > 3660) keys.slice(0, keys.length - 3660).forEach(function (d) { delete out.log[d]; });
+ // 路线下线时把设置放进归档,读者换路线后不再需要;路线重新上线且读者没有换路线时恢复
+ var profile = cleanProfile(input.profile), archived = cleanProfile(archive.profile);
+ if (profile && routeIds.indexOf(profile.route) !== -1) out.profile = profile;
+ else if (profile) out.archive.profile = profile;
+ else if (archived && routeIds.indexOf(archived.route) !== -1) out.profile = archived;
+ else if (archived) out.archive.profile = archived;
+ if (out.profile && out.archive.profile && out.archive.profile.route === out.profile.route) out.archive.profile = null;
+ var placement = input.placement || (out.profile && archive.placement);
+ if (placement && out.profile && placement.route === out.profile.route && Array.isArray(placement.stages)) {
+ out.placement = remapPlacement(placement, routes.filter(function (r) { return r.id === out.profile.route; })[0], data);
+ } else if (input.placement && out.archive.profile && input.placement.route === out.archive.profile.route && Array.isArray(input.placement.stages)) {
+ // 路线下线时定级结果也先存着,只留格式正确的字段
+ out.archive.placement = {
+ route: input.placement.route,
+ stages: input.placement.stages.slice(0, 50).filter(function (st) { return st && typeof st === 'object'; }).map(function (st) {
+ var asked = (Array.isArray(st.asked) ? st.asked : []).filter(function (q) { return QUESTION_ID.test(q); }).slice(0, 50), ratings = {};
+ asked.forEach(function (q) { var v = st.ratings && st.ratings[q]; if (v === 0 || v === 1 || v === 2) ratings[q] = v; });
+ return { title: typeof st.title === 'string' ? st.title.slice(0, 100) : undefined, asked: asked, ratings: ratings, skipped: st.skipped === true };
+ })
+ };
+ }
+ var seen = Math.floor(Number(input.seenUpdate));
+ out.seenUpdate = seen > 0 ? seen : 0;
+ return out;
+ }
+ // ---------- 回答对照 ----------
+ // 从参考要点里取出术语(英文名词、类名、方法名和带引号的说法),去掉题目中已经出现的和单位;
+ // 读者写了回答时,对照哪些提到了、哪些没提到,只作提示,不打分
+ var TERM_STOP = ['a', 'an', 'the', 'is', 'of', 'to', 'in', 'and', 'or', 'vs', 'on', 'for', 'it', 'be', 'by', 'as', 'at', 'id', 'kb', 'mb', 'gb', 'tb', 'ms', 'ns', 'us', 'qps', 'tps'];
+ function termKey(t) { return t.toLowerCase().replace(/\(\)$/, ''); }
+ function keyTerms(question, points) {
+ var seen = {}, out = [], asked = String(question || '').toLowerCase();
+ function add(t) {
+ var k = termKey(t);
+ if (k.length < 2 || TERM_STOP.indexOf(k) !== -1 || seen[k] || asked.indexOf(k) !== -1) return;
+ seen[k] = true; out.push(t);
+ }
+ var text = String(points || ''), m;
+ var en = /[A-Za-z][A-Za-z0-9+#_\-]*(?:\.[A-Za-z][A-Za-z0-9_]*)*(?:\(\))?/g, quoted = /“([^”]{2,10})”/g;
+ while ((m = en.exec(text))) add(m[0]);
+ while ((m = quoted.exec(text))) add(m[1]);
+ return out.slice(0, 10);
+ }
+ function compareAnswer(answer, terms) {
+ var a = String(answer || '').toLowerCase().replace(/\s+/g, ' ');
+ var hit = [], miss = [];
+ terms.forEach(function (t) { (a.indexOf(termKey(t)) !== -1 ? hit : miss).push(t); });
+ return { hit: hit, miss: miss };
+ }
+
+ // 合并两份已经清理过的记录(换设备、两台设备都学过时用):
+ // 同一道题取最近一次自评的那份,同样新时取复习次数多的;完成的天取较早的日期;每天的学习量取较大的;
+ // 路线设置、定级以当前浏览器为准,当前没有时用导入的
+ function mergeState(current, incoming) {
+ var out = JSON.parse(JSON.stringify(current));
+ function newer(a, b) {
+ if (!a) return b;
+ if (!b) return a;
+ var la = a.last || '', lb = b.last || '';
+ if (la !== lb) return la > lb ? a : b;
+ return (b.reps || 0) > (a.reps || 0) ? b : a;
+ }
+ [['cards', out.cards, incoming.cards], ['archive', out.archive.cards, incoming.archive.cards]].forEach(function (x) {
+ Object.keys(x[2]).forEach(function (id) { x[1][id] = newer(x[1][id], x[2][id]); });
+ });
+ [[out.done, incoming.done], [out.archive.done, incoming.archive.done]].forEach(function (x) {
+ Object.keys(x[1]).forEach(function (id) { if (!x[0][id] || x[1][id] < x[0][id]) x[0][id] = x[1][id]; });
+ });
+ // 当前的和归档的只留一份:题目或天在当前记录中有时,以当前为准
+ Object.keys(out.cards).forEach(function (id) { delete out.archive.cards[id]; });
+ Object.keys(out.done).forEach(function (id) { delete out.archive.done[id]; });
+ Object.keys(incoming.log).forEach(function (d) { out.log[d] = Math.max(out.log[d] || 0, incoming.log[d]); });
+ if (!out.profile) { out.profile = incoming.profile; out.placement = incoming.placement; }
+ else if (!out.placement && incoming.placement && incoming.profile && incoming.profile.route === out.profile.route) out.placement = incoming.placement;
+ if (!out.archive.profile) out.archive.profile = incoming.archive.profile;
+ out.seenUpdate = Math.max(out.seenUpdate || 0, incoming.seenUpdate || 0);
+ return out;
+ }
+ // 一份记录的概况,导入前给读者看
+ function stateSummary(st, data) {
+ var route = st.profile && data.routes.filter(function (r) { return r.id === st.profile.route; })[0];
+ var days = Object.keys(st.log).sort();
+ return { route: route ? route.title : null, cards: Object.keys(st.cards).length, done: Object.keys(st.done).length,
+ archived: Object.keys(st.archive.cards).length + Object.keys(st.archive.done).length, last: days.length ? days[days.length - 1] : null };
+ }
+ // 定级结果按阶段标题对应到当前路线;旧版本的记录没有标题,阶段数相同时按顺序对应。
+ // 每个阶段的候选题按当前内容重新排,已经问过的题保留自评,下线的题去掉
+ function remapPlacement(input, route, data) {
+ var old = input.stages.filter(function (s) { return s && typeof s === 'object'; });
+ var byIndex = old.length === route.stages.length && old.every(function (s) { return typeof s.title !== 'string'; });
+ return {
+ route: route.id,
+ stages: route.stages.map(function (stage, i) {
+ var match = byIndex ? old[i] : old.filter(function (s) { return s.title === stage.title; })[0];
+ var fresh = stageCandidates(route, i, data);
+ if (!match) return { title: stage.title, pool: fresh, asked: [], ratings: {}, skipped: false };
+ var asked = (Array.isArray(match.asked) ? match.asked : []).filter(function (q, k, list) { return fresh.indexOf(q) !== -1 && list.indexOf(q) === k; });
+ var ratings = {};
+ asked.forEach(function (q) { var v = match.ratings && match.ratings[q]; if (v === 0 || v === 1 || v === 2) ratings[q] = v; });
+ asked = asked.filter(function (q) { return ratings[q] !== undefined; });
+ return { title: stage.title, pool: asked.concat(fresh.filter(function (q) { return asked.indexOf(q) === -1; })), asked: asked, ratings: ratings, skipped: match.skipped === true };
+ })
+ };
+ }
+
+ // ---------- 内容更新 ----------
+ // 读者上次看过之后的更新,按读者自己的记录归纳:学过的天里新增的题、当前路线新增的天、练过的题中修改和下线的
+ function updateSummary(state, data) {
+ var entries = (data.updates || []).filter(function (u) { return u.seq > (state.seenUpdate || 0); });
+ if (!entries.length) return null;
+ var routeId = state.profile && state.profile.route;
+ var fresh = {}, newDays = {}, edited = {}, retired = {}, changedDays = {};
+ entries.forEach(function (u) {
+ (u.added.questions || []).forEach(function (q) {
+ var qd = data.questions[q];
+ if (qd && !state.cards[q] && qd.days.some(function (d) { return state.done[d]; })) fresh[q] = true;
+ });
+ (u.added.days || []).forEach(function (d) { if (data.days[d] && data.days[d].route === routeId && !state.done[d]) newDays[d] = true; });
+ (u.edited.questions || []).forEach(function (q) { if (state.cards[q] && data.questions[q]) edited[q] = true; });
+ (u.retired.questions || []).forEach(function (q) { if (state.archive.cards[q]) retired[q] = true; });
+ (u.edited.days || []).forEach(function (d) { if (state.done[d] && data.days[d]) changedDays[d] = true; });
+ });
+ // 页面只带最近的若干条更新;读者落下的比这还多时,最早的几次看不到明细,只提示有过调整
+ var oldest = (data.updates || [])[0];
+ var summary = {
+ latest: entries[entries.length - 1].seq, date: entries[entries.length - 1].date,
+ fresh: Object.keys(fresh), newDays: Object.keys(newDays), edited: Object.keys(edited), retired: Object.keys(retired),
+ changedDays: Object.keys(changedDays), missedOlder: !!(oldest && state.seenUpdate > 0 && state.seenUpdate < oldest.seq - 1)
+ };
+ summary.relevant = summary.fresh.length + summary.newDays.length + summary.edited.length + summary.retired.length + summary.changedDays.length > 0 || summary.missedOlder;
+ return summary;
+ }
+ // 学过的天里还没练过的题,按路线顺序;内容更新后新增的题从这里练
+ function unpracticed(route, data, state) {
+ var out = [];
+ route.stages.forEach(function (stage) {
+ stage.days.forEach(function (d) {
+ if (!state.done[d]) return;
+ data.days[d].questions.forEach(function (q) { if (!state.cards[q] && out.indexOf(q) === -1) out.push(q); });
+ });
+ });
+ return out;
+ }
+
+ return {
+ today: today, addDays: addDays, daysBetween: daysBetween, isDate: isDate,
+ INTERVALS: INTERVALS, FORGOT: FORGOT, FUZZY: FUZZY, KNOWN: KNOWN,
+ newCard: newCard, schedule: schedule, isDue: isDue, dueList: dueList, streak: streak, streakInfo: streakInfo, isMilestone: isMilestone, FREEZE_EVERY: FREEZE_EVERY, activeDays: activeDays,
+ reviewLimit: reviewLimit, mastery: mastery,
+ createPlacement: createPlacement, nextPlacementQuestion: nextPlacementQuestion, answerPlacement: answerPlacement,
+ skipStage: skipStage, stageResult: stageResult, placementProgress: placementProgress,
+ dayModes: dayModes, buildPlan: buildPlan, DAY_MINUTES: DAY_MINUTES, REVIEW_MINUTES: REVIEW_MINUTES,
+ aiPrompt: aiPrompt, mockSet: mockSet, planCalendar: planCalendar, icsFold: icsFold,
+ updateSummary: updateSummary, unpracticed: unpracticed, mergeState: mergeState, stateSummary: stateSummary, keyTerms: keyTerms, compareAnswer: compareAnswer, remapPlacement: remapPlacement, emptyState: emptyState, sanitize: sanitize, STORE_VERSION: STORE_VERSION
+ };
+});
diff --git a/learn/src/icons/favicon-32.png b/learn/src/icons/favicon-32.png
new file mode 100644
index 0000000000000000000000000000000000000000..25356e4c6b32f9378ca09f9e4925709240b70c2b
GIT binary patch
literal 1113
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
literal 0
HcmV?d00001
diff --git a/learn/src/icons/icon-192.png b/learn/src/icons/icon-192.png
new file mode 100644
index 0000000000000000000000000000000000000000..ce827004b21163c6b95a82473bb0aee43087beb7
GIT binary patch
literal 6508
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
P0zn$8y7wxT?IQmVjkP@)
literal 0
HcmV?d00001
diff --git a/learn/src/icons/maskable-512.png b/learn/src/icons/maskable-512.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5f2677144b28a2c1152f809103b5d403bde8837
GIT binary patch
literal 8182
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
+
+
+
+
+
+
+
+ CS-Notes 学习
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 跳到正文
+
+ 正在载入…
+
+
+ 请启用 JavaScript 使用学习站,或直接阅读 CS-Notes 。
+
+
+
+
+
diff --git a/learn/src/manifest.webmanifest b/learn/src/manifest.webmanifest
new file mode 100644
index 000000000..ed109473f
--- /dev/null
+++ b/learn/src/manifest.webmanifest
@@ -0,0 +1,16 @@
+{
+ "name": "CS-Notes 学习",
+ "short_name": "CS-Notes 学习",
+ "description": "先测水平,再按天学,用间隔复习记牢面试题。",
+ "lang": "zh-CN",
+ "start_url": "./",
+ "scope": "./",
+ "display": "standalone",
+ "background_color": "#f6f6f8",
+ "theme_color": "#5b5bd6",
+ "icons": [
+ { "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
+ { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
+ { "src": "icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
+ ]
+}
diff --git a/learn/src/og.png b/learn/src/og.png
new file mode 100644
index 0000000000000000000000000000000000000000..76863f42b58cf8243b04bbedce712c0f67ba163f
GIT binary patch
literal 73063
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