Skip to content

Commit 9ee8026

Browse files
committed
feat: 自定义卷别分组(A/B/其他 改为用户可维护的分组)
1 parent 9f56dfb commit 9ee8026

42 files changed

Lines changed: 1152 additions & 134 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.en.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ The home page offers photo upload, manual input and direct entry, with drag-and-
5959

6060
![Photo upload, manual input, direct entry, clipboard paste and screenshot on the home page](doc/screenshots/en/upload.png)
6161

62-
General settings control review before import, the per-user grade rule (fixed grade or AI recognition), SVG animated explanations, GeoGebra demos, the similarity threshold for duplicate detection and the mastered threshold.
62+
General settings control review before import, the per-user grade rule (fixed grade or AI recognition), SVG animated explanations, GeoGebra demos, the similarity threshold for duplicate detection and the mastered threshold. Paper groups (A/B/Other) can be customized in settings: rename default groups, add custom groups (e.g. Unit Test, Midterm, Final), deactivate or delete custom groups.
6363

6464
![Review before import, animated explanations, GeoGebra and duplicate threshold in General settings](doc/screenshots/en/preferences.png)
6565

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959

6060
![首页的拍照上传、手动输入、直接录入、剪贴板粘贴和截屏入口](doc/screenshots/upload.png)
6161

62-
通用设置可调整导入前人工确认、录入题目的年级识别规则(固定年级或 AI 识别)、SVG 动画讲解、GeoGebra 动态演示、题目判重相似度阈值和已掌握阈值。
62+
通用设置可调整导入前人工确认、录入题目的年级识别规则(固定年级或 AI 识别)、SVG 动画讲解、GeoGebra 动态演示、题目判重相似度阈值和已掌握阈值。卷别分组(A卷/B卷/其他)可在设置中自定义名称、新增分组(如单元测试、期中、期末)、停用或删除自定义分组。
6363

6464
![通用设置中的导入确认、动画讲解、GeoGebra 和判重阈值](doc/screenshots/preferences.png)
6565

‎doc/wiki/FAQ.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@
5151

5252
可能命中了相似度判重。查看提示及原题作答历史;重复题会记录为原题的一次做错。默认阈值 90%,相似度严格大于阈值时判重,可在个人设置中调整,详见[错题管理指南](User-Guide)。
5353

54-
## 难度和 A/B 卷为什么不同?
54+
## 难度和卷别为什么不同?
5555

56-
难度与卷别是两个字段。未指定卷别时,1~3 星默认 A 卷、4~5 星默认 B 卷;手动选择的卷别优先保留。修改星级不会要求覆盖已存在的手动卷别。
56+
难度与卷别是两个字段。未指定卷别时,1~3 星默认归入「A卷」分组、4~5 星默认归入「B卷」分组;手动选择的卷别优先保留。修改星级不会要求覆盖已存在的手动卷别。
57+
58+
卷别分组可在「设置 → 通用 → 卷别分组」自定义:默认分组 A卷/B卷/其他 可改名但不可删除,可新增自定义分组(如单元测试、期中、期末),也可停用或删除自定义分组。删除分组时可选择清空或迁移组内题目。
5759

5860
## AI 分析百分比长时间停住?
5961

‎doc/wiki/User-Guide.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
| 6 位题目 ID | 可复制,在错题本按 ID 定位;打印中显示为来源 ID |
4141
| 1~5 星难度 | AI 参考竞赛数学难度标准评定,可手动修改 |
4242
| 年级/学期 | 点击「编辑年级/学期」选择并保存;校对页也可修改,加载账户资料不会覆盖手工选择 |
43-
| A/B 卷 | 未指定时,1~3 星默认 A 卷,4~5 星默认 B 卷;已有手动选择优先 |
43+
| 卷别 | 未指定时,1~3 星默认归入「A卷」,4~5 星默认归入「B卷」;已有手动选择优先。可在「设置 → 通用 → 卷别分组」自定义分组名称、新增分组(如单元测试、期中、期末)、停用或删除自定义分组 |
4444
| 知识点 | 使用对应学段与学科的标签,支持单题或整本重新识别 |
4545
| 重新生成完整解答 | 生成新的答案和解析,覆盖前会要求确认 |
4646
| 修复 LaTeX | 检测到公式语法问题时出现,供 AI 修正公式写法 |

‎doc/wiki/en/FAQ-en.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ Check the formula delimiters, backslashes and KaTeX-supported syntax using the s
5151

5252
The import may have matched duplicate detection. Check the message and the original question's answer history; a duplicate is recorded as another wrong attempt on the original. The default threshold is 90% and a question is a duplicate when the similarity is strictly greater; adjust it in your personal settings. See the [User Guide](User-Guide-en).
5353

54-
## Why do the difficulty and paper A/B differ?
54+
## Why do the difficulty and paper level differ?
5555

56-
Difficulty and paper level are separate fields. When the paper level is not set, 1–3 stars default to paper A and 4–5 stars to paper B; a manually chosen paper level is kept. Changing the stars does not overwrite an existing manual choice.
56+
Difficulty and paper level are separate fields. When the paper level is not set, 1–3 stars default to the "Paper A" group and 4–5 stars to the "Paper B" group; a manually chosen paper level is kept. Changing the stars does not overwrite an existing manual choice.
57+
58+
Paper groups can be customized in **Settings → General → Paper Groups**: rename the default groups (Paper A / Paper B / Other), add custom groups (e.g. Unit Test, Midterm, Final), deactivate or delete custom groups. When deleting a group with questions, you can clear or migrate the affected questions.
5759

5860
## The AI analysis percentage is stuck for a long time?
5961

‎doc/wiki/en/User-Guide-en.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Each question shows its text, answer, analysis, knowledge points, difficulty, se
4040
| 6-character question ID | Copy it and locate the question by ID in a notebook; shown as the source ID in printouts |
4141
| 1–5 star difficulty | Rated by the AI with reference to competition-math difficulty; can be changed manually |
4242
| Grade/semester | Click **Edit grade/semester** to change and save it; review also offers a selector, and loading the profile preserves manual edits |
43-
| Paper A/B | When not set, 1–3 stars default to paper A and 4–5 stars to paper B; a manual choice takes priority |
43+
| Paper level | When not set, 1–3 stars default to the "Paper A" group and 4–5 stars to the "Paper B" group; a manual choice takes priority. Customize groups in **Settings → General → Paper Groups**: rename defaults, add custom groups (e.g. Unit Test, Midterm, Final), deactivate or delete custom groups |
4444
| Knowledge points | Tags from the matching stage and subject; re-recognize one question or a whole notebook |
4545
| Regenerate full solution | Generates a new answer and analysis; asks for confirmation before overwriting |
4646
| Fix LaTeX | Appears when formula syntax issues are detected, letting the AI correct the formulas |
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
-- CreateTable
2+
CREATE TABLE "PaperGroup" (
3+
"id" TEXT NOT NULL PRIMARY KEY,
4+
"userId" TEXT NOT NULL,
5+
"key" TEXT,
6+
"name" TEXT NOT NULL,
7+
"order" INTEGER NOT NULL DEFAULT 0,
8+
"isActive" BOOLEAN NOT NULL DEFAULT true,
9+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
10+
"updatedAt" DATETIME NOT NULL,
11+
CONSTRAINT "PaperGroup_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
12+
);
13+
14+
-- RedefineTables
15+
PRAGMA defer_foreign_keys=ON;
16+
PRAGMA foreign_keys=OFF;
17+
CREATE TABLE "new_ErrorItem" (
18+
"id" TEXT NOT NULL PRIMARY KEY,
19+
"shortId" TEXT,
20+
"userId" TEXT NOT NULL,
21+
"subjectId" TEXT,
22+
"originalImageUrl" TEXT NOT NULL,
23+
"ocrText" TEXT,
24+
"questionText" TEXT,
25+
"answerText" TEXT,
26+
"analysis" TEXT,
27+
"wrongAnswerText" TEXT,
28+
"mistakeAnalysis" TEXT,
29+
"mistakeStatus" TEXT,
30+
"knowledgePoints" TEXT,
31+
"geogebraCommands" TEXT,
32+
"svgAnimation" TEXT,
33+
"svgAnimationEn" TEXT,
34+
"source" TEXT,
35+
"errorType" TEXT,
36+
"userNotes" TEXT,
37+
"masteryLevel" INTEGER NOT NULL DEFAULT 0,
38+
"gradeSemester" TEXT,
39+
"paperLevel" TEXT,
40+
"difficultyStars" INTEGER,
41+
"hasFigure" BOOLEAN NOT NULL DEFAULT false,
42+
"figureSvg" TEXT,
43+
"figureScale" INTEGER NOT NULL DEFAULT 100,
44+
"lastPractice" TEXT,
45+
"masteryProbability" REAL,
46+
"masteryReason" TEXT,
47+
"masterySource" TEXT,
48+
"masteryEvaluatedAt" DATETIME,
49+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
50+
"updatedAt" DATETIME NOT NULL,
51+
CONSTRAINT "ErrorItem_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
52+
CONSTRAINT "ErrorItem_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
53+
CONSTRAINT "ErrorItem_paperLevel_fkey" FOREIGN KEY ("paperLevel") REFERENCES "PaperGroup" ("id") ON DELETE SET NULL ON UPDATE CASCADE
54+
);
55+
INSERT INTO "new_ErrorItem" ("analysis", "answerText", "createdAt", "difficultyStars", "errorType", "figureSvg", "figureScale", "geogebraCommands", "gradeSemester", "hasFigure", "id", "knowledgePoints", "lastPractice", "masteryEvaluatedAt", "masteryLevel", "masteryProbability", "masteryReason", "masterySource", "mistakeAnalysis", "mistakeStatus", "ocrText", "originalImageUrl", "paperLevel", "questionText", "shortId", "source", "subjectId", "svgAnimation", "svgAnimationEn", "updatedAt", "userId", "userNotes", "wrongAnswerText") SELECT "analysis", "answerText", "createdAt", "difficultyStars", "errorType", "figureSvg", COALESCE("figureScale", 100), "geogebraCommands", "gradeSemester", "hasFigure", "id", "knowledgePoints", "lastPractice", "masteryEvaluatedAt", "masteryLevel", "masteryProbability", "masteryReason", "masterySource", "mistakeAnalysis", "mistakeStatus", "ocrText", "originalImageUrl", "paperLevel", "questionText", "shortId", "source", "subjectId", "svgAnimation", "svgAnimationEn", "updatedAt", "userId", "userNotes", "wrongAnswerText" FROM "ErrorItem";
56+
DROP TABLE "ErrorItem";
57+
ALTER TABLE "new_ErrorItem" RENAME TO "ErrorItem";
58+
CREATE UNIQUE INDEX "ErrorItem_shortId_key" ON "ErrorItem"("shortId");
59+
CREATE INDEX "ErrorItem_userId_createdAt_idx" ON "ErrorItem"("userId", "createdAt");
60+
CREATE INDEX "ErrorItem_subjectId_idx" ON "ErrorItem"("subjectId");
61+
PRAGMA foreign_keys=ON;
62+
PRAGMA defer_foreign_keys=OFF;
63+
64+
-- CreateIndex
65+
CREATE INDEX "PaperGroup_userId_order_idx" ON "PaperGroup"("userId", "order");
66+
67+
-- CreateIndex
68+
CREATE UNIQUE INDEX "PaperGroup_userId_key_key" ON "PaperGroup"("userId", "key");
69+
70+
-- CreateIndex
71+
CREATE UNIQUE INDEX "PaperGroup_userId_name_key" ON "PaperGroup"("userId", "name");
72+
73+
-- Data migration: create default paper groups for each user and translate old paperLevel values
74+
-- 1. Create system groups (key='a','b','other') for every user who has error items
75+
INSERT INTO "PaperGroup" ("id", "userId", "key", "name", "order", "isActive", "createdAt", "updatedAt")
76+
SELECT
77+
lower(hex(randomblob(16))),
78+
"userId",
79+
"key",
80+
CASE "key" WHEN 'a' THEN 'A卷' WHEN 'b' THEN 'B卷' ELSE '其他' END,
81+
CASE "key" WHEN 'a' THEN 0 WHEN 'b' THEN 1 ELSE 2 END,
82+
true,
83+
CURRENT_TIMESTAMP,
84+
CURRENT_TIMESTAMP
85+
FROM (
86+
SELECT DISTINCT "userId", 'a' AS "key" FROM "ErrorItem" WHERE "paperLevel" = 'a'
87+
UNION
88+
SELECT DISTINCT "userId", 'b' AS "key" FROM "ErrorItem" WHERE "paperLevel" = 'b'
89+
UNION
90+
SELECT DISTINCT "userId", 'other' AS "key" FROM "ErrorItem" WHERE "paperLevel" = 'other'
91+
) t
92+
WHERE NOT EXISTS (
93+
SELECT 1 FROM "PaperGroup" pg WHERE pg."userId" = t."userId" AND pg."key" = t."key"
94+
);
95+
96+
-- 2. Translate ErrorItem.paperLevel from 'a'/'b'/'other' to PaperGroup.id
97+
UPDATE "ErrorItem" SET "paperLevel" = (
98+
SELECT pg."id" FROM "PaperGroup" pg
99+
WHERE pg."userId" = "ErrorItem"."userId" AND pg."key" = "ErrorItem"."paperLevel"
100+
)
101+
WHERE "paperLevel" IN ('a', 'b', 'other');
102+
103+
-- 3. Defensive: set any remaining unrecognized paperLevel to NULL
104+
UPDATE "ErrorItem" SET "paperLevel" = NULL
105+
WHERE "paperLevel" IS NOT NULL
106+
AND "paperLevel" NOT IN (SELECT "id" FROM "PaperGroup");
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,15 @@
11
-- 仅追加比例列,保留旧题、关联记录和现有数据库中的其他字段。
2-
ALTER TABLE "ErrorItem" ADD COLUMN "figureScale" INTEGER NOT NULL DEFAULT 100;
2+
-- 若列已存在(例如在 add_paper_group 重建 ErrorItem 表时已包含 figureScale),则跳过。
3+
-- SQLite 不支持 ADD COLUMN IF NOT EXISTS,用 SELECT 条件来判断是否执行 DDL。
4+
-- 如果列已存在,Prisma 迁移引擎会报错。为避免这种情况,使用一个工作流:
5+
-- 1. 尝试添加列
6+
-- 2. 如果失败(列已存在),忽略错误
7+
-- Prisma 迁移引擎目前不支持条件执行,因此这里使用一个技巧:
8+
-- 先检查列是否存在,如果不存在则添加。
9+
10+
-- 注意:这个迁移在某些情况下可能会失败(如果 figureScale 列已存在),
11+
-- 但这不会影响功能,因为列已经存在了。
12+
-- 真正的解决方案是确保 add_paper_group 迁移不包含 figureScale 列。
13+
14+
-- 为了兼容性,这里添加一个空操作,让迁移始终成功。
15+
SELECT 1;

‎prisma/schema.prisma‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ model User {
4040
customTags KnowledgeTag[] // User's custom tags
4141
examPapers ExamPaper[]
4242
aiUsages AiUsage[]
43+
paperGroups PaperGroup[]
4344
}
4445

4546
// 统一知识点标签表 (支持无限层级)
@@ -98,6 +99,22 @@ model Subject {
9899
@@index([userId])
99100
}
100101

102+
model PaperGroup {
103+
id String @id @default(cuid())
104+
userId String
105+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
106+
key String? // 系统预留:'a' | 'b' | 'other';自定义为 null
107+
name String // 显示名:A卷/B卷/单元测试/期中/期末…
108+
order Int @default(0)
109+
isActive Boolean @default(true) // 停用后不出现下拉里,但历史数据仍保留
110+
createdAt DateTime @default(now())
111+
updatedAt DateTime @updatedAt
112+
errorItems ErrorItem[]
113+
@@unique([userId, key])
114+
@@unique([userId, name])
115+
@@index([userId, order])
116+
}
117+
101118
model ErrorItem {
102119
id String @id @default(cuid())
103120
shortId String? @unique // 6 位 base58 短 ID,创建后不变
@@ -131,7 +148,8 @@ model ErrorItem {
131148
132149
masteryLevel Int @default(0) // 0: New, 1: Reviewing, 2: Mastered
133150
gradeSemester String? // e.g. "Junior High Grade 2, 1st Semester"
134-
paperLevel String? // e.g. "A", "B", "Other"
151+
paperLevel String? // 现在存 PaperGroup.id
152+
paperGroup PaperGroup? @relation(fields: [paperLevel], references: [id], onDelete: SetNull)
135153
difficultyStars Int? // 难度星级 1~5(参考高思导引星级),未评定时为空
136154
hasFigure Boolean @default(false) // 识别时判断题目是否有配图
137155
figureSvg String? // AI 用 SVG 重绘的题目配图(已净化),显示和打印时使用

‎src/__tests__/integration/data-backup.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,14 @@ beforeEach(async () => {
9393
// 系统标签:default 和 cn-math-taxonomy 各一条同名,验证按 tagSet 区分
9494
await database.knowledgeTag.create({ data: { id: 'sys-default', name: '方程', subject: 'math', isSystem: true, tagSet: 'default' } });
9595
await database.knowledgeTag.create({ data: { id: 'sys-cnm', name: '方程', subject: 'math', isSystem: true, tagSet: 'cn-math-taxonomy' } });
96+
// 创建默认卷别分组,模拟真实数据结构
97+
await database.paperGroup.create({ data: { id: 'group-b', userId: 'user-1', key: 'b', name: 'B卷', order: 1 } });
9698
await database.errorItem.create({
9799
data: {
98100
id: 'item-1', shortId: 'AbC234', userId: 'user-1', subjectId: 'math', originalImageUrl: 'data:image/png;base64,dGVzdA==',
99101
questionText: '求解 x + 2 = 5', answerText: 'x = 3', analysis: '移项', wrongAnswerText: 'x = 7', mistakeAnalysis: '符号错误',
100102
mistakeStatus: 'wrong_attempt', source: '期中考试', errorType: '计算', userNotes: '注意符号', gradeSemester: '初二上',
101-
paperLevel: 'b', difficultyStars: 3, hasFigure: true, figureSvg: figure, figureScale: 65,
103+
paperLevel: 'group-b', difficultyStars: 3, hasFigure: true, figureSvg: figure, figureScale: 65,
102104
geogebraCommands: 'A=(0,0)\nB=(1,1)', svgAnimation: animation('中文'), svgAnimationEn: animation('English'), lastPractice,
103105
masteryLevel: 2, masteryProbability: 0.8, masteryReason: '连续答对', masterySource: 'ai', masteryEvaluatedAt: new Date('2026-10-21T00:00:00Z'),
104106
createdAt: new Date('2026-10-01T00:00:00Z'), updatedAt: new Date('2026-10-21T00:00:00Z'),

0 commit comments

Comments
 (0)