diff --git a/doc/01_行业和领域调研分析报告230340180孙世杰.docx b/doc/01_行业和领域调研分析报告230340180孙世杰.docx new file mode 100644 index 0000000..13f8aeb Binary files /dev/null and b/doc/01_行业和领域调研分析报告230340180孙世杰.docx differ diff --git a/doc/02_软件系统的需求构思及描述230340180孙世杰.docx b/doc/02_软件系统的需求构思及描述230340180孙世杰.docx new file mode 100644 index 0000000..90cc9b0 Binary files /dev/null and b/doc/02_软件系统的需求构思及描述230340180孙世杰.docx differ diff --git a/doc/03_软件需求规格说明书230340180孙世杰.docx b/doc/03_软件需求规格说明书230340180孙世杰.docx new file mode 100644 index 0000000..7bcf867 Binary files /dev/null and b/doc/03_软件需求规格说明书230340180孙世杰.docx differ diff --git a/doc/04_软件设计规格说明书孙世杰230340180.docx b/doc/04_软件设计规格说明书孙世杰230340180.docx new file mode 100644 index 0000000..8fe36b8 Binary files /dev/null and b/doc/04_软件设计规格说明书孙世杰230340180.docx differ diff --git a/doc/05_软件工程课程设计汇报_孙世杰.pptx b/doc/05_软件工程课程设计汇报_孙世杰.pptx new file mode 100644 index 0000000..d7cd023 Binary files /dev/null and b/doc/05_软件工程课程设计汇报_孙世杰.pptx differ diff --git a/doc/06_软件开发项目的个人自评报告.xlsx b/doc/06_软件开发项目的个人自评报告.xlsx new file mode 100644 index 0000000..e6943d9 Binary files /dev/null and b/doc/06_软件开发项目的个人自评报告.xlsx differ diff --git a/doc/07_软件开发项目的团队自评报告.xlsx b/doc/07_软件开发项目的团队自评报告.xlsx new file mode 100644 index 0000000..dce0ab5 Binary files /dev/null and b/doc/07_软件开发项目的团队自评报告.xlsx differ diff --git a/doc/08_230340175王龙瑞杰-实践总结报告.docx b/doc/08_230340175王龙瑞杰-实践总结报告.docx new file mode 100644 index 0000000..2858118 Binary files /dev/null and b/doc/08_230340175王龙瑞杰-实践总结报告.docx differ diff --git a/doc/08_230340176亢宇翔-实践总结报告.docx b/doc/08_230340176亢宇翔-实践总结报告.docx new file mode 100644 index 0000000..12fbbfe Binary files /dev/null and b/doc/08_230340176亢宇翔-实践总结报告.docx differ diff --git a/doc/08_230340180孙世杰-实践总结报告.docx b/doc/08_230340180孙世杰-实践总结报告.docx new file mode 100644 index 0000000..49a1b0c Binary files /dev/null and b/doc/08_230340180孙世杰-实践总结报告.docx differ diff --git a/doc/08_230340202艾力库提-实践总结报告.docx b/doc/08_230340202艾力库提-实践总结报告.docx new file mode 100644 index 0000000..8597b6c Binary files /dev/null and b/doc/08_230340202艾力库提-实践总结报告.docx differ diff --git a/doc/09_演示运行视频.mp4 b/doc/09_演示运行视频.mp4 new file mode 100644 index 0000000..edd28f9 Binary files /dev/null and b/doc/09_演示运行视频.mp4 differ diff --git a/doc/10_项目宣传海报.jpg b/doc/10_项目宣传海报.jpg new file mode 100644 index 0000000..d3ba139 Binary files /dev/null and b/doc/10_项目宣传海报.jpg differ diff --git a/model/uml文档.docx b/model/uml文档.docx new file mode 100644 index 0000000..2b6f4aa Binary files /dev/null and b/model/uml文档.docx differ diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..0df32f6 --- /dev/null +++ b/src/README.md @@ -0,0 +1,25 @@ +# 课程笔记共享 + +#### 介绍 +这是软件工程课程设计项目,提供课程笔记的上传、OCR文字识别、自动分类与智能推荐功能。推荐遵循“同专业同年级 > 同专业 > 同年级 > 基础学科(如高数)”的原则;高等数学仅在无其他可推荐内容时作为兜底出现。 + +#### 软件架构 +- 客户端:Android(Java、AppCompat、Material、Gradle) +- 分层结构: + - 界面层(UI):登录注册、首页推荐、搜索、笔记详情、个人中心 + - 服务层(Service):OCR识别、分类推荐、用户与积分 + - 数据访问层(Repository):SQLite数据读写与查询 +- 数据库:SQLite,核心数据表包含 `T_User`、`T_Major`、`T_Subject`、`T_Chapter`、`T_Note`、`T_Comment`、`T_Major_Subject` 等,支持专业-学科-年级的关联与笔记基础操作。 + +#### 安装教程 +1. 安装 Android Studio(含 Android SDK)与 JDK 11+。 +2. 克隆项目源代码:`git clone ` 并打开项目根目录。 +3. 构建与运行: + - 命令行执行:`./gradlew.bat :app:assembleDebug` + - 或在 Android Studio 中直接点击 Run 运行到模拟器/真机。 + +#### 使用说明 +1. 注册或登录后,在“个人中心”完善专业与年级信息,以便获得更精准的推荐。 +2. 上传笔记(支持图片/PDF):系统进行OCR识别与自动分类,若不准确可在界面中手动调整章节。 +3. 首页“推荐”区域点击“刷新推荐”,按“同专业同年级 > 同专业 > 同年级”优先显示;无非基础内容时展示“高数”作为兜底。 +4. 在笔记详情页进行浏览、点赞、评论与下载;积分机制与下载规则可在后续版本中完善。 \ No newline at end of file diff --git a/src/androidTest/java/net/micode/myapplication/ExampleInstrumentedTest.java b/src/androidTest/java/net/micode/myapplication/ExampleInstrumentedTest.java new file mode 100644 index 0000000..4c0d6db --- /dev/null +++ b/src/androidTest/java/net/micode/myapplication/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package net.micode.myapplication; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("net.micode.myapplication", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 0000000..53b6fc3 --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/MyNoteApp.java b/src/main/java/com/example/myapplication/MyNoteApp.java new file mode 100644 index 0000000..bee4e19 --- /dev/null +++ b/src/main/java/com/example/myapplication/MyNoteApp.java @@ -0,0 +1,408 @@ +package com.example.myapplication; + +import android.app.Application; +import com.example.myapplication.repository.DatabaseManager; + +public class MyNoteApp extends Application { + @Override + public void onCreate() { + super.onCreate(); + try { + DatabaseManager.init(this); + } catch (Exception e) { + android.util.Log.e("MyNoteApp", "Database init failed", e); + } + + try { + android.database.sqlite.SQLiteDatabase db = com.example.myapplication.repository.DatabaseManager.getDb(); + if (db != null) { + db.beginTransaction(); + try { + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (1, '编译原理', 'CS-301', '编译原理是计算机科学中的重要基础课程,主要介绍编译器的设计与实现技术。')"); + + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (1, 1, '第一章 编译器概述', 'CS-301-01', '介绍编译器的基本概念、结构和工作原理', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (2, 1, '第二章 词法分析', 'CS-301-02', '讲解词法分析的基本原理和实现方法', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (3, 1, '第三章 语法分析', 'CS-301-03', '深入探讨语法分析的理论和实践', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (4, 1, '第四章 语法制导翻译', 'CS-301-04', '介绍语法制导翻译的基本概念和方法', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (5, 1, '第五章 类型检查', 'CS-301-05', '讲解类型检查的原理和实现', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (6, 1, '第六章 运行时环境', 'CS-301-06', '分析程序运行时的存储管理机制', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (7, 1, '第七章 中间代码生成', 'CS-301-07', '介绍中间代码的生成方法和技术', 7)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (8, 1, '第八章 代码生成', 'CS-301-08', '讲解目标代码生成的基本原理和算法', 8)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (9, 1, '第九章 代码优化', 'CS-301-09', '深入探讨代码优化的各种技术', 9)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (10, 1, '第十章 数据流分析', 'CS-301-10', '介绍数据流分析的理论基础和算法', 10)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (11, 1, '第十一章 循环优化', 'CS-301-11', '专门讨论循环结构的优化技术', 11)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (12, 1, '第十二章 过程间分析', 'CS-301-12', '探讨跨越过程边界的程序分析技术', 12)"); + + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 1, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (2, '数据结构', 'CS-303', '数据结构是计算机科学的核心课程,研究数据的组织、存储和管理方法。')"); + + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (13, 2, '第一章 绪论', 'CS-303-01', '介绍数据结构的基本概念和算法分析方法', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (14, 2, '第二章 线性表', 'CS-303-02', '讲解线性表的顺序存储和链式存储实现', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (15, 2, '第三章 栈和队列', 'CS-303-03', '分析栈和队列的特性及其应用', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (16, 2, '第四章 串', 'CS-303-04', '研究字符串的存储结构和模式匹配算法', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (17, 2, '第五章 树与二叉树', 'CS-303-05', '介绍树的基本概念和二叉树的性质与操作', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (18, 2, '第六章 树与二叉树的应用', 'CS-303-06', '探讨树结构在实际问题中的应用', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (19, 2, '第七章 图', 'CS-303-07', '讲解图的基本概念和存储表示', 7)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (20, 2, '第八章 图的应用', 'CS-303-08', '分析图在路径查找和网络优化中的应用', 8)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (21, 2, '第九章 查找', 'CS-303-09', '研究各种查找算法的实现和性能', 9)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (22, 2, '第十章 排序', 'CS-303-10', '讲解各种排序算法的原理和实现', 10)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (23, 2, '第十一章 高级数据结构', 'CS-303-11', '介绍高级数据结构及其应用场景', 11)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (24, 2, '第十二章 算法设计与分析', 'CS-303-12', '探讨算法设计策略和复杂度分析', 12)"); + + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 2, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (3, '计算机网络', 'CS-304', '计算机网络是研究计算机之间如何相互连接和通信的学科。')"); + + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (25, 3, '第一章 计算机网络概述', 'CS-304-01', '介绍计算机网络的基本概念和发展历程', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (26, 3, '第二章 网络体系结构', 'CS-304-02', '讲解网络分层体系结构和各种模型', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (27, 3, '第三章 物理层', 'CS-304-03', '分析物理层的数据传输原理和技术', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (28, 3, '第四章 数据链路层', 'CS-304-04', '研究数据链路层的帧传输和差错控制', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (29, 3, '第五章 局域网', 'CS-304-05', '介绍局域网技术和以太网协议', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (30, 3, '第六章 网络层', 'CS-304-06', '深入分析网络层的路由和寻址机制', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (31, 3, '第七章 传输层', 'CS-304-07', '研究传输层的端到端通信服务', 7)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (32, 3, '第八章 应用层', 'CS-304-08', '介绍常见的网络应用协议和服务', 8)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (33, 3, '第九章 网络安全', 'CS-304-09', '分析网络安全威胁和防护措施', 9)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (34, 3, '第十章 网络管理', 'CS-304-10', '讨论网络管理的方法和技术', 10)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (35, 3, '第十一章 无线网络', 'CS-304-11', '介绍无线通信技术和移动网络', 11)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (36, 3, '第十二章 网络新技术', 'CS-304-12', '探讨新兴网络技术的发展趋势和应用', 12)"); + + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 3, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (4, '操作系统', 'CS-302', '操作系统是计算机系统的核心软件,负责管理硬件资源和提供用户接口。')"); + + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (37, 4, '第一章 操作系统概述', 'CS-302-01', '介绍操作系统的基本概念、功能和分类', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (38, 4, '第二章 进程管理', 'CS-302-02', '讲解进程的基本概念和管理机制', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (39, 4, '第三章 线程管理', 'CS-302-03', '深入探讨线程的概念和管理方法', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (40, 4, '第四章 进程同步', 'CS-302-04', '分析进程间的同步与互斥问题', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (41, 4, '第五章 内存管理', 'CS-302-05', '讲解内存管理的基本原理和技术', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (42, 4, '第六章 文件系统', 'CS-302-06', '介绍文件系统的组织和管理', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (43, 4, '第七章 I/O系统', 'CS-302-07', '分析输入输出系统的原理和管理', 7)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (44, 4, '第八章 虚拟内存', 'CS-302-08', '深入探讨虚拟内存的实现机制', 8)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (45, 4, '第九章 系统保护与安全', 'CS-302-09', '讨论操作系统的安全保护和访问控制', 9)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (46, 4, '第十章 分布式系统', 'CS-302-10', '介绍分布式操作系统的基本概念和机制', 10)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (47, 4, '第十一章 实时系统', 'CS-302-11', '分析实时操作系统的特点和要求', 11)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (48, 4, '第十二章 Linux内核分析', 'CS-302-12', '深入分析Linux操作系统的内核实现', 12)"); + + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 4, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (5, '机器人学导论(第4版)', 'RBT-INTRO-4e', '机器人学导论课程,涵盖机器人运动学、动力学、控制等核心内容')"); + + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (49, 5, '第1章 概述', 'RBT-1', '机器人学基本概念和机械臂几何结构介绍', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (50, 5, '第2章 空间描述和变换', 'RBT-2', '位置、姿态、坐标系变换和欧拉角等数学基础', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (51, 5, '第3章 操作臂运动学', 'RBT-3', '连杆、关节描述和D-H参数法建立运动学方程', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (52, 5, '第4章 操作臂逆运动学', 'RBT-4', '逆运动学的可解性分析和解法研究', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (53, 5, '第5章 速度和静力', 'RBT-5', '线速度、角速度表示和雅可比矩阵分析', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (54, 5, '第6章 操作臂动力学', 'RBT-6', '机器人动力学建模和牛顿-欧拉、拉格朗日方法', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (55, 5, '第7章 轨迹生成', 'RBT-7', '关节空间和笛卡尔空间的轨迹规划方法', 7)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (56, 5, '第8章 操作臂的机械设计', 'RBT-8', '机器人机械结构设计原理', 8)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (57, 5, '第9章 操作臂的线性控制', 'RBT-9', '机器人线性控制系统设计和PID控制', 9)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (58, 5, '第10章 操作臂的非线性控制', 'RBT-10', '机器人非线性控制方法', 10)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (59, 5, '第11章 操作臂的力控制', 'RBT-11', '机器人柔顺控制和力/位混合控制', 11)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (60, 5, '第12章 机器人编程语言与系统', 'RBT-12', '机器人编程语言和系统介绍', 12)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (61, 5, '第13章 离线编程系统', 'RBT-13', '机器人离线仿真和编程技术', 13)"); + + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 5, '大三')"); + + // 高等数学 + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (6, '高等数学', 'MATH-101', '涵盖微积分、线性代数、概率论与数理统计等基础数学内容')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (62, 6, '微积分基础', 'MATH-101-01', '极限、导数与积分的基本概念与运算', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (63, 6, '多元微积分', 'MATH-101-02', '多元函数微分与重积分、方向导数与梯度', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (64, 6, '级数', 'MATH-101-03', '数项级数与函数项级数、收敛判别法', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (65, 6, '向量与空间解析几何', 'MATH-101-04', '向量运算、空间直线与平面、二次曲面', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (66, 6, '线性代数基础', 'MATH-101-05', '线性方程组与向量空间、线性变换', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (67, 6, '行列式与矩阵', 'MATH-101-06', '行列式性质、矩阵运算与初等变换', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (68, 6, '特征值与特征向量', 'MATH-101-07', '特征值问题、相似对角化与主成分', 7)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (69, 6, '概率论', 'MATH-101-08', '概率模型、条件概率与全概率公式', 8)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (70, 6, '随机变量与分布', 'MATH-101-09', '常见分布、期望方差与矩母函数', 9)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (71, 6, '数理统计', 'MATH-101-10', '估计、区间估计与检验的基本方法', 10)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (72, 6, '假设检验与方差分析', 'MATH-101-11', '假设检验框架、t检验与ANOVA', 11)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (73, 6, '数值方法初步', 'MATH-101-12', '插值、数值积分与迭代解方程', 12)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 6, '大一')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10044, 6, '大一')"); + + // 大学英语 + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (7, '大学英语', 'ENG-101', '大学英语综合教程,涵盖词汇、语法、阅读、写作、听说等能力提升')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (74, 7, '词汇与短语', 'ENG-101-01', '核心词汇与常用短语积累', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (75, 7, '语法与句法', 'ENG-101-02', '时态、语态、从句与复杂句式', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (76, 7, '阅读理解', 'ENG-101-03', '篇章结构与主旨细节理解', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (77, 7, '写作训练', 'ENG-101-04', '段落与文章写作、论证与修辞', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (78, 7, '听力训练', 'ENG-101-05', '学术与生活场景听力理解', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (79, 7, '口语交流', 'ENG-101-06', '发音与口语表达、讨论与演讲', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (80, 7, '翻译与改写', 'ENG-101-07', '汉英/英汉翻译技巧与改写', 7)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (81, 7, '英美文化', 'ENG-101-08', '跨文化理解与背景知识', 8)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (82, 7, '学术英语', 'ENG-101-09', '学术阅读与写作、术语与体裁', 9)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (83, 7, '综合复习', 'ENG-101-10', '综合能力提升与期末复习', 10)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 7, '大一')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10044, 7, '大一')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (100, '毛泽东思想和中国特色社会主义理论体系概论', 'POL-001', '系统阐释中国化马克思主义的理论与实践')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (84, 100, '第一章 绪论', 'POL-001-01', '课程导论与学习要求', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (85, 100, '第二章 新民主主义革命理论', 'POL-001-02', '中国革命道路与理论', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (86, 100, '第三章 社会主义改造理论', 'POL-001-03', '社会主义改造的理论与实践', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (87, 100, '第四章 社会主义建设探索', 'POL-001-04', '社会主义建设道路探索', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (88, 100, '第五章 改革开放与中国特色社会主义', 'POL-001-05', '改革开放历史进程与理论成果', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (89, 100, '第六章 总体布局与战略布局', 'POL-001-06', '五位一体与四个全面', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (90, 100, '第七章 党的领导与全面从严治党', 'POL-001-07', '党的建设与治理能力现代化', 7)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (91, 100, '第八章 国家治理现代化', 'POL-001-08', '国家治理体系与治理能力现代化', 8)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (92, 100, '第九章 民生与社会建设', 'POL-001-09', '民生改善与社会建设', 9)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (93, 100, '第十章 青年责任与时代使命', 'POL-001-10', '新时代青年的使命担当', 10)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10044, 100, '大一')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (101, '思想道德与法治', 'POL-002', '提升思想道德修养与法治素养')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (94, 101, '第一章 道德与人生', 'POL-002-01', '道德的本质与价值', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (95, 101, '第二章 社会主义核心价值观', 'POL-002-02', '核心价值观的培育与践行', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (96, 101, '第三章 法治与法治思维', 'POL-002-03', '法治理念与法治思维', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (97, 101, '第四章 宪法与宪法权利', 'POL-002-04', '宪法基本原则与权利保障', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (98, 101, '第五章 民法典与民事权利', 'POL-002-05', '民法典体系与权利义务', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (99, 101, '第六章 刑法概述', 'POL-002-06', '刑法基本原理与犯罪构成', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (100, 101, '第七章 行政法与行政救济', 'POL-002-07', '行政行为与救济途径', 7)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (101, 101, '第八章 涉网法治与合规', 'POL-002-08', '网络安全法治与合规要求', 8)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (102, 101, '第九章 大学生法治素养', 'POL-002-09', '校园场景中的法治意识', 9)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (103, 101, '第十章 职业伦理与社会责任', 'POL-002-10', '职业道德与社会责任', 10)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10044, 101, '大一')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (102, '马克思主义基本原理', 'POL-003', '马克思主义哲学、政治经济学与科学社会主义基本原理')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (104, 102, '第一章 哲学基本问题', 'POL-003-01', '物质与意识的关系', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (105, 102, '第二章 辩证唯物主义', 'POL-003-02', '世界的物质统一性与发展规律', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (106, 102, '第三章 历史唯物主义', 'POL-003-03', '社会历史发展的普遍规律', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (107, 102, '第四章 政治经济学原理', 'POL-003-04', '剩余价值与资本运行', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (108, 102, '第五章 科学社会主义', 'POL-003-05', '社会主义产生、发展与实践', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (109, 102, '第六章 马克思主义中国化', 'POL-003-06', '中国化时代化的理论成果', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (110, 102, '第七章 价值观与人生观', 'POL-003-07', '价值选择与人生道路', 7)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (111, 102, '第八章 社会发展与现代化', 'POL-003-08', '社会现代化与中国式现代化', 8)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (112, 102, '第九章 实践与认识', 'POL-003-09', '实践与认识的辩证统一', 9)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (113, 102, '第十章 理论与方法论', 'POL-003-10', '基本立场、观点与方法', 10)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10044, 102, '大一')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (103, '中国近现代史纲要', 'POL-004', '中国近现代史进程与基本规律')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (114, 103, '第一章 近代中国的沦陷与抗争', 'POL-004-01', '近代中国国情与反侵略斗争', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (115, 103, '第二章 辛亥革命与共和探索', 'POL-004-02', '辛亥革命与民国初期探索', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (116, 103, '第三章 中国共产党成立与新民主主义革命', 'POL-004-03', '新民主主义革命的开展', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (117, 103, '第四章 抗日战争与民族解放', 'POL-004-04', '全民族抗战与胜利', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (118, 103, '第五章 解放战争与新中国成立', 'POL-004-05', '全国解放与新中国成立', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (119, 103, '第六章 社会主义改造与建设', 'POL-004-06', '社会主义改造与早期建设', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (120, 103, '第七章 改革开放与现代化建设', 'POL-004-07', '改革开放与现代化进程', 7)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (121, 103, '第八章 中国特色社会主义新时代', 'POL-004-08', '新时代的历史方位与任务', 8)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (122, 103, '第九章 民族复兴与中国梦', 'POL-004-09', '民族复兴的历史逻辑', 9)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (123, 103, '第十章 历史人物与史料研读', 'POL-004-10', '重要历史人物与史料研读', 10)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10044, 103, '大一')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (104, '大学体育', 'PE-101', '增强体质与掌握基本运动技能')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (124, 104, '第一章 体育与健康概论', 'PE-101-01', '体育与健康的关系', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (125, 104, '第二章 田径基础', 'PE-101-02', '跑、跳、投的基础技能', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (126, 104, '第三章 球类运动基础', 'PE-101-03', '篮球、足球、排球基础', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (127, 104, '第四章 健身与体能训练', 'PE-101-04', '力量与心肺耐力训练', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (128, 104, '第五章 安全与运动损伤防护', 'PE-101-05', '运动损伤预防与处理', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (129, 104, '第六章 体质评价与管理', 'PE-101-06', '体测与健康管理', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10044, 104, '大一')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (105, '军事理论', 'MIL-101', '国防与安全、军事制度与科技')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (130, 105, '第一章 国防概述', 'MIL-101-01', '国家安全与国防建设', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (131, 105, '第二章 现代战争形态', 'MIL-101-02', '信息化战争与联合作战', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (132, 105, '第三章 我国军事制度', 'MIL-101-03', '兵役制度与国防动员', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (133, 105, '第四章 军事科技与装备', 'MIL-101-04', '军事科技发展与武器装备', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (134, 105, '第五章 国家安全与总体安全观', 'MIL-101-05', '总体国家安全观', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10044, 105, '大一')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (106, '微积分', 'MATH-201', '函数、极限、导数与积分的系统学习')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (135, 106, '第一章 函数与极限', 'MATH-201-01', '函数与极限的基础', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (136, 106, '第二章 导数与微分', 'MATH-201-02', '导数与微分运算', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (137, 106, '第三章 不定积分', 'MATH-201-03', '积分基本方法', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (138, 106, '第四章 定积分与应用', 'MATH-201-04', '定积分性质与应用', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (139, 106, '第五章 多元函数微分', 'MATH-201-05', '偏导与梯度', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (140, 106, '第六章 重积分与曲线曲面积分', 'MATH-201-06', '重积分与曲线曲面积分', 6)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 106, '大一')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (107, '线性代数', 'MATH-202', '矩阵、向量空间与线性变换')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (141, 107, '第一章 行列式与矩阵', 'MATH-202-01', '行列式与矩阵运算', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (142, 107, '第二章 向量与向量空间', 'MATH-202-02', '向量空间与线性相关', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (143, 107, '第三章 线性方程组', 'MATH-202-03', '线性方程组解法', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (144, 107, '第四章 特征值与对角化', 'MATH-202-04', '特征值与相似对角化', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (145, 107, '第五章 正交与最小二乘', 'MATH-202-05', '内积空间与最小二乘', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 107, '大一')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (108, '概率论与数理统计', 'MATH-203', '随机变量与统计推断')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (146, 108, '第一章 概率与条件概率', 'MATH-203-01', '概率基础与条件概率', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (147, 108, '第二章 随机变量与分布', 'MATH-203-02', '常见分布与性质', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (148, 108, '第三章 数字特征与联合分布', 'MATH-203-03', '期望方差协方差', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (149, 108, '第四章 大数定律与中心极限定理', 'MATH-203-04', '极限定理', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (150, 108, '第五章 参数估计与假设检验', 'MATH-203-05', '点估计与检验', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 108, '大一')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (109, '程序设计基础', 'CS-101', '编程基础语法与问题求解')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (151, 109, '第一章 计算机与程序', 'CS-101-01', '编程概念与开发环境', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (152, 109, '第二章 基本数据类型与运算', 'CS-101-02', '数据类型与运算', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (153, 109, '第三章 选择与循环', 'CS-101-03', '控制结构', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (154, 109, '第四章 函数与递归', 'CS-101-04', '函数封装与递归', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (155, 109, '第五章 数组与字符串', 'CS-101-05', '顺序数据结构', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 109, '大一')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (110, '面向对象程序设计', 'CS-102', '类、对象与抽象机制')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (156, 110, '第一章 面向对象概述', 'CS-102-01', 'OO思想与基本概念', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (157, 110, '第二章 类与对象', 'CS-102-02', '封装与构造', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (158, 110, '第三章 继承与多态', 'CS-102-03', '继承层次与动态绑定', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (159, 110, '第四章 接口与泛型', 'CS-102-04', '接口抽象与泛型编程', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (160, 110, '第五章 异常与IO', 'CS-102-05', '异常处理与文件IO', 5)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 110, '大二')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (111, '数字逻辑电路', 'EE-201', '逻辑代数与组合时序电路')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (161, 111, '第一章 逻辑代数基础', 'EE-201-01', '布尔代数与逻辑化简', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (162, 111, '第二章 组合逻辑电路', 'EE-201-02', '组合电路设计', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (163, 111, '第三章 时序逻辑电路', 'EE-201-03', '触发器与时序分析', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (164, 111, '第四章 可编程逻辑器件', 'EE-201-04', 'PAL、FPGA基础', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 111, '大二')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (112, '离散数学', 'CS-201', '集合、图与形式逻辑')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (165, 112, '第一章 集合与关系', 'CS-201-01', '集合、关系与函数', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (166, 112, '第二章 逻辑与证明', 'CS-201-02', '命题逻辑与证明方法', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (167, 112, '第三章 图论基础', 'CS-201-03', '图与树结构', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (168, 112, '第四章 组合数学', 'CS-201-04', '计数原理与递推', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 112, '大二')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (113, '计算机组成原理', 'CS-202', '指令系统、存储层次与CPU')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (169, 113, '第一章 计算机系统概述', 'CS-202-01', '计算机系统结构', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (170, 113, '第二章 数据表示与运算', 'CS-202-02', '编码与算术运算', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (171, 113, '第三章 存储系统', 'CS-202-03', '主存与Cache', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (172, 113, '第四章 指令系统', 'CS-202-04', '指令格式与寻址', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 113, '大二')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (114, '汇编语言', 'CS-203', '汇编语法与底层编程')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (173, 114, '第一章 机器与汇编', 'CS-203-01', '机器指令与汇编基础', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (174, 114, '第二章 寄存器与指令', 'CS-203-02', '寄存器与常用指令', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (175, 114, '第三章 子程序与调用', 'CS-203-03', '过程调用与栈', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 114, '大二')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (115, '数据库', 'CS-305', '关系模型、SQL与事务')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (176, 115, '第一章 关系数据库概述', 'CS-305-01', '数据模型与规范化', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (177, 115, '第二章 SQL语言', 'CS-305-02', '查询、更新与视图', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (178, 115, '第三章 事务与并发控制', 'CS-305-03', '事务隔离与锁', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (179, 115, '第四章 索引与优化', 'CS-305-04', '索引结构与优化', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 115, '大二')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (116, '机器学习', 'AI-301', '监督学习、无监督与优化')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (180, 116, '第一章 机器学习概述', 'AI-301-01', '基本概念与术语', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (181, 116, '第二章 线性模型', 'AI-301-02', '回归与分类', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (182, 116, '第三章 树模型与集成', 'AI-301-03', '决策树与Boosting', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (183, 116, '第四章 聚类与降维', 'AI-301-04', 'KMeans与PCA', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 116, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (117, '深度学习', 'AI-302', '神经网络、卷积与序列模型')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (184, 117, '第一章 神经网络基础', 'AI-302-01', '前馈网络与反向传播', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (185, 117, '第二章 卷积神经网络', 'AI-302-02', 'CNN结构与应用', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (186, 117, '第三章 循环与序列模型', 'AI-302-03', 'RNN、LSTM、Transformer', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (187, 117, '第四章 训练与正则化', 'AI-302-04', '优化与正则化策略', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 117, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (118, '自然语言处理', 'AI-303', '文本表示、序列建模与生成')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (188, 118, '第一章 文本预处理与表示', 'AI-303-01', '分词、词向量与编码', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (189, 118, '第二章 语言模型', 'AI-303-02', '统计与神经语言模型', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (190, 118, '第三章 序列标注', 'AI-303-03', '标注与序列任务', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (191, 118, '第四章 文本生成与理解', 'AI-303-04', '生成模型与理解', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 118, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (119, '计算机视觉', 'AI-304', '图像处理、特征与检测识别')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (192, 119, '第一章 图像基础', 'AI-304-01', '图像采集与颜色空间', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (193, 119, '第二章 图像滤波与增强', 'AI-304-02', '空间与频域处理', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (194, 119, '第三章 特征提取', 'AI-304-03', '边缘、角点与局部特征', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (195, 119, '第四章 目标检测与识别', 'AI-304-04', '检测识别方法', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 119, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (120, '网络安全', 'SECU-301', '攻击与防御、漏洞与加固')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (196, 120, '第一章 安全概述', 'SECU-301-01', '威胁模型与安全目标', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (197, 120, '第二章 密码学基础', 'SECU-301-02', '对称、非对称与哈希', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (198, 120, '第三章 漏洞与利用', 'SECU-301-03', '常见漏洞类型与利用', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (199, 120, '第四章 网络攻防', 'SECU-301-04', '渗透与防御技术', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 120, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (121, '密码学', 'CRYPTO-301', '现代密码体制与协议')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (200, 121, '第一章 古典密码与现代密码', 'CRYPTO-301-01', '密码发展与概述', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (201, 121, '第二章 对称加密', 'CRYPTO-301-02', '分组与流密码', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (202, 121, '第三章 公钥密码', 'CRYPTO-301-03', 'RSA、ECC等', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (203, 121, '第四章 消息认证与签名', 'CRYPTO-301-04', 'MAC与签名', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 121, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (122, '软件工程', 'SE-301', '需求、设计、实现与维护')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (204, 122, '第一章 软件工程概述', 'SE-301-01', '软件过程与方法', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (205, 122, '第二章 需求工程', 'SE-301-02', '需求获取与分析', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (206, 122, '第三章 架构设计', 'SE-301-03', '架构风格与模式', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (207, 122, '第四章 测试与质量保证', 'SE-301-04', '测试策略与质量', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 122, '大二')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (123, '计算机图形学', 'CG-301', '几何、渲染与图形系统')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (208, 123, '第一章 图形学概述', 'CG-301-01', '渲染管线与基础', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (209, 123, '第二章 几何与变换', 'CG-301-02', '矩阵变换与投影', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (210, 123, '第三章 光照与着色', 'CG-301-03', '光照模型与着色', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (211, 123, '第四章 曲线与曲面', 'CG-301-04', 'Bezier与B样条', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 123, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (124, '数字图像处理', 'DIP-301', '图像采集、增强与分析')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (212, 124, '第一章 图像采集与表示', 'DIP-301-01', '采集与表示基础', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (213, 124, '第二章 图像增强', 'DIP-301-02', '直方图与滤波增强', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (214, 124, '第三章 图像分割', 'DIP-301-03', '阈值、边缘与区域方法', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (215, 124, '第四章 图像特征与理解', 'DIP-301-04', '特征提取与理解', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 124, '大三')"); + + db.execSQL("INSERT OR IGNORE INTO T_Subject (id, name, code, description) VALUES (125, '嵌入式系统', 'EE-301', '嵌入式硬件、RTOS与外设')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (216, 125, '第一章 嵌入式概述', 'EE-301-01', '嵌入式系统组成', 1)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (217, 125, '第二章 处理器与存储', 'EE-301-02', '处理器架构与存储', 2)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (218, 125, '第三章 RTOS基础', 'EE-301-03', '任务、调度与同步', 3)"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id, subject_id, name, code, description, order_num) VALUES (219, 125, '第四章 外设与驱动', 'EE-301-04', 'GPIO、串口与驱动', 4)"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id, subject_id, grade) VALUES (10001, 125, '大三')"); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + } catch (Exception e) { + android.util.Log.e("MyNoteApp", "Seed data insert failed", e); + } + + // 统一重复课程:将旧科目(2:数据结构,3:计算机网络,4:操作系统,6:高等数学)映射到新科目(20002,20004,20003,20013),并删除旧记录 + try { + android.database.sqlite.SQLiteDatabase db = com.example.myapplication.repository.DatabaseManager.getDb(); + if (db != null) { + db.beginTransaction(); + try { + db.execSQL("UPDATE T_Major_Subject SET subject_id=20002 WHERE subject_id=2"); + db.execSQL("UPDATE T_Major_Subject SET subject_id=20004 WHERE subject_id=3"); + db.execSQL("UPDATE T_Major_Subject SET subject_id=20003 WHERE subject_id=4"); + db.execSQL("UPDATE T_Major_Subject SET subject_id=20013 WHERE subject_id=6"); + + db.execSQL("DELETE FROM T_Chapter WHERE subject_id IN (2,3,4,6)"); + db.execSQL("DELETE FROM T_Subject WHERE id IN (2,3,4,6)"); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + } catch (Exception e) { + android.util.Log.e("MyNoteApp", "Dedup subjects failed", e); + } + // 移除清理白名单,允许数据库长期存储更多课程与章节 + + try { + android.database.sqlite.SQLiteDatabase db = com.example.myapplication.repository.DatabaseManager.getDb(); + if (db != null) { + db.beginTransaction(); + try { + db.execSQL("UPDATE T_Chapter SET keywords='栈,队列,入栈,出栈,链表', aliases='stack,queue' WHERE id=15"); + db.execSQL("UPDATE T_Chapter SET keywords='树,二叉树,遍历,BST', aliases='tree,binary tree' WHERE id=17"); + db.execSQL("UPDATE T_Chapter SET keywords='内存,分页,分段,虚拟内存', aliases='memory,vm' WHERE id=41"); + db.execSQL("UPDATE T_Chapter SET keywords='IO,设备,驱动,缓冲', aliases='io,i/o' WHERE id=43"); + db.execSQL("UPDATE T_Chapter SET keywords='TCP,UDP,端口,连接', aliases='transport layer' WHERE id=31"); + db.execSQL("UPDATE T_Chapter SET keywords='HTTP,DNS,SMTP,FTP', aliases='application layer' WHERE id=32"); + db.execSQL("UPDATE T_Chapter SET keywords='D-H参数,关节,连杆,运动学', aliases='kinematics' WHERE id=51"); + db.execSQL("UPDATE T_Chapter SET keywords='动力学,拉格朗日,牛顿-欧拉', aliases='dynamics' WHERE id=54"); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + } catch (Exception e) { + android.util.Log.e("MyNoteApp", "Seed keywords update failed", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/Utils.java b/src/main/java/com/example/myapplication/Utils.java new file mode 100644 index 0000000..fa2af64 --- /dev/null +++ b/src/main/java/com/example/myapplication/Utils.java @@ -0,0 +1,16 @@ +package com.example.myapplication; + +import android.content.Context; +import android.content.SharedPreferences; +import android.widget.Toast; + +public class Utils { + public static void toast(Context ctx, String msg) { + Toast.makeText(ctx, msg, Toast.LENGTH_SHORT).show(); + } + + public static String getUser(Context ctx) { + SharedPreferences prefs = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + return prefs.getString("currentUser", "张三"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/domain/Chapter.java b/src/main/java/com/example/myapplication/domain/Chapter.java new file mode 100644 index 0000000..85935b1 --- /dev/null +++ b/src/main/java/com/example/myapplication/domain/Chapter.java @@ -0,0 +1,31 @@ +package com.example.myapplication.domain; + +public class Chapter { + private long id; + private String name; + private int orderIndex; + private int courseId; + private String keywords; + private String aliases; + + public Chapter() {} + + public Chapter(long id, String name, int orderIndex) { + this.id = id; + this.name = name; + this.orderIndex = orderIndex; + } + + public long getId() { return id; } + public void setId(long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getOrderIndex() { return orderIndex; } + public void setOrderIndex(int orderIndex) { this.orderIndex = orderIndex; } + public int getCourseId() { return courseId; } + public void setCourseId(int courseId) { this.courseId = courseId; } + public String getKeywords() { return keywords; } + public void setKeywords(String keywords) { this.keywords = keywords; } + public String getAliases() { return aliases; } + public void setAliases(String aliases) { this.aliases = aliases; } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/domain/Comment.java b/src/main/java/com/example/myapplication/domain/Comment.java new file mode 100644 index 0000000..f057b45 --- /dev/null +++ b/src/main/java/com/example/myapplication/domain/Comment.java @@ -0,0 +1,30 @@ +package com.example.myapplication.domain; + +public class Comment { + private long id; + private String uploaderId; + private long noteId; + private String content; + private String createdAt; // ISO string + + public Comment() {} + + public Comment(long id, String uploaderId, long noteId, String content, String createdAt) { + this.id = id; + this.uploaderId = uploaderId; + this.noteId = noteId; + this.content = content; + this.createdAt = createdAt; + } + + public long getId() { return id; } + public void setId(long id) { this.id = id; } + public String getUploaderId() { return uploaderId; } + public void setUploaderId(String uploaderId) { this.uploaderId = uploaderId; } + public long getNoteId() { return noteId; } + public void setNoteId(long noteId) { this.noteId = noteId; } + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + public String getCreatedAt() { return createdAt; } + public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/domain/Course.java b/src/main/java/com/example/myapplication/domain/Course.java new file mode 100644 index 0000000..f94c2f8 --- /dev/null +++ b/src/main/java/com/example/myapplication/domain/Course.java @@ -0,0 +1,26 @@ +package com.example.myapplication.domain; + +public class Course { + private long id; + private String description; + private String name; + private String code; + + public Course() {} + + public Course(long id, String description, String name, String code) { + this.id = id; + this.description = description; + this.name = name; + this.code = code; + } + + public long getId() { return id; } + public void setId(long id) { this.id = id; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getCode() { return code; } + public void setCode(String code) { this.code = code; } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/domain/Like.java b/src/main/java/com/example/myapplication/domain/Like.java new file mode 100644 index 0000000..3a4ec61 --- /dev/null +++ b/src/main/java/com/example/myapplication/domain/Like.java @@ -0,0 +1,22 @@ +package com.example.myapplication.domain; + +public class Like { + private String userId; + private long noteId; + private String createdAt; + + public Like() {} + + public Like(String userId, long noteId, String createdAt) { + this.userId = userId; + this.noteId = noteId; + this.createdAt = createdAt; + } + + public String getUserId() { return userId; } + public void setUserId(String userId) { this.userId = userId; } + public long getNoteId() { return noteId; } + public void setNoteId(long noteId) { this.noteId = noteId; } + public String getCreatedAt() { return createdAt; } + public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/domain/Major.java b/src/main/java/com/example/myapplication/domain/Major.java new file mode 100644 index 0000000..b4050ab --- /dev/null +++ b/src/main/java/com/example/myapplication/domain/Major.java @@ -0,0 +1,26 @@ +package com.example.myapplication.domain; + +public class Major { + private long id; + private String name; + private String code; + private String description; + + public Major() {} + + public Major(long id, String name, String code, String description) { + this.id = id; + this.name = name; + this.code = code; + this.description = description; + } + + public long getId() { return id; } + public void setId(long id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getCode() { return code; } + public void setCode(String code) { this.code = code; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/domain/Note.java b/src/main/java/com/example/myapplication/domain/Note.java new file mode 100644 index 0000000..826db2e --- /dev/null +++ b/src/main/java/com/example/myapplication/domain/Note.java @@ -0,0 +1,76 @@ +package com.example.myapplication.domain; + +public class Note { + private long id; + private String uploaderId; + private long courseId; + private String content; + private int downloadCount; + private int likeCount; + private String created; + private String title; + private String filePath; + private Integer publisherGrade; // 发布者年级 + private String publisherMajor; // 发布者专业 + + // 额外字段用于 NoteUploadActivity + private String description; // 描述 + private String ocrText; // OCR识别文本 + private long uploadTime; // 上传时间 + private long updateTime; // 更新时间 + private long chapterId; // 章节ID + + public Note() {} + + public Note(long id, String uploaderId, long courseId, String content, + int downloadCount, int likeCount, String created, String title, String filePath, + Integer publisherGrade, String publisherMajor) { + this.id = id; + this.uploaderId = uploaderId; + this.courseId = courseId; + this.content = content; + this.downloadCount = downloadCount; + this.likeCount = likeCount; + this.created = created; + this.title = title; + this.filePath = filePath; + this.publisherGrade = publisherGrade; + this.publisherMajor = publisherMajor; + } + + public int getLikeCount() { return likeCount; } + public void setLikeCount(int likeCount) { this.likeCount = likeCount; } + + public long getId() { return id; } + public void setId(long id) { this.id = id; } + public String getUploaderId() { return uploaderId; } + public void setUploaderId(String uploaderId) { this.uploaderId = uploaderId; } + public long getCourseId() { return courseId; } + public void setCourseId(long courseId) { this.courseId = courseId; } + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + public int getDownloadCount() { return downloadCount; } + public void setDownloadCount(int downloadCount) { this.downloadCount = downloadCount; } + public String getCreated() { return created; } + public void setCreated(String created) { this.created = created; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getFilePath() { return filePath; } + public void setFilePath(String filePath) { this.filePath = filePath; } + public Integer getPublisherGrade() { return publisherGrade; } + public void setPublisherGrade(Integer publisherGrade) { this.publisherGrade = publisherGrade; } + public String getPublisherMajor() { return publisherMajor; } + public void setPublisherMajor(String publisherMajor) { this.publisherMajor = publisherMajor; } + + // 新增方法用于 NoteUploadActivity + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public String getOcrText() { return ocrText; } + public void setOcrText(String ocrText) { this.ocrText = ocrText; } + public long getUploadTime() { return uploadTime; } + public void setUploadTime(long uploadTime) { this.uploadTime = uploadTime; } + public long getUpdateTime() { return updateTime; } + public void setUpdateTime(long updateTime) { this.updateTime = updateTime; } + public long getChapterId() { return chapterId; } + public void setChapterId(long chapterId) { this.chapterId = chapterId; } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/domain/User.java b/src/main/java/com/example/myapplication/domain/User.java new file mode 100644 index 0000000..8e27556 --- /dev/null +++ b/src/main/java/com/example/myapplication/domain/User.java @@ -0,0 +1,66 @@ +package com.example.myapplication.domain; + +public class User { + private String account; + private String name; + private String password; + private int type; + private String mobile; + private int points; + private Integer grade; // 年级(大几) + private Long majorId; // 专业ID + + public User() {} + + public User(String account, String name, String password, int type, String mobile) { + this.account = account; + this.name = name; + this.password = password; + this.type = type; + this.mobile = mobile; + this.points = 0; + this.grade = null; + this.majorId = null; + } + + // 带积分的构造 + public User(String account, String name, String password, int type, String mobile, int points) { + this.account = account; + this.name = name; + this.password = password; + this.type = type; + this.mobile = mobile; + this.points = points; + this.grade = null; + this.majorId = null; + } + + // 带年级与专业的构造 + public User(String account, String name, String password, int type, String mobile, int points, Integer grade, Long majorId) { + this.account = account; + this.name = name; + this.password = password; + this.type = type; + this.mobile = mobile; + this.points = points; + this.grade = grade; + this.majorId = majorId; + } + + public String getAccount() { return account; } + public void setAccount(String account) { this.account = account; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + public int getType() { return type; } + public void setType(int type) { this.type = type; } + public String getMobile() { return mobile; } + public void setMobile(String mobile) { this.mobile = mobile; } + public int getPoints() { return points; } + public void setPoints(int points) { this.points = points; } + public Integer getGrade() { return grade; } + public void setGrade(Integer grade) { this.grade = grade; } + public Long getMajorId() { return majorId; } + public void setMajorId(Long majorId) { this.majorId = majorId; } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/repository/CommentRepository.java b/src/main/java/com/example/myapplication/repository/CommentRepository.java new file mode 100644 index 0000000..230f67a --- /dev/null +++ b/src/main/java/com/example/myapplication/repository/CommentRepository.java @@ -0,0 +1,66 @@ +package com.example.myapplication.repository; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.example.myapplication.domain.Comment; + +import java.util.ArrayList; +import java.util.List; + +public class CommentRepository { + private final Context context; + private DatabaseHelper helper; + private SQLiteDatabase db; + + public CommentRepository(Context context) { this.context = context.getApplicationContext(); } + + public void openDatabase() { + if (helper == null) helper = new DatabaseHelper(context); + if (db == null || !db.isOpen()) db = helper.getWritableDatabase(); + } + + public void closeDatabase() { if (db != null && db.isOpen()) db.close(); db = null; } + + public void close() { closeDatabase(); } + + private Comment map(Cursor c) { + Comment cm = new Comment(); + cm.setId(c.getLong(c.getColumnIndexOrThrow("id"))); + cm.setUploaderId(c.getString(c.getColumnIndexOrThrow("user_id"))); + cm.setNoteId(c.getLong(c.getColumnIndexOrThrow("note_id"))); + cm.setContent(c.getString(c.getColumnIndexOrThrow("content"))); + cm.setCreatedAt(c.getString(c.getColumnIndexOrThrow("created_at"))); + return cm; + } + + public List listByNoteId(long noteId) { + openDatabase(); + List list = new ArrayList<>(); + try (Cursor c = db.query("T_Comment", null, "note_id=?", new String[]{String.valueOf(noteId)}, null, null, "created_at DESC")) { + while (c != null && c.moveToNext()) list.add(map(c)); + } + closeDatabase(); + return list; + } + + public long add(String userId, long noteId, String content) { + openDatabase(); + ContentValues cv = new ContentValues(); + cv.put("user_id", userId); + cv.put("note_id", noteId); + cv.put("content", content); + long r = db.insert("T_Comment", null, cv); + closeDatabase(); + return r; + } + + public boolean delete(long id, String userId) { + openDatabase(); + int rows = db.delete("T_Comment", "id=? AND user_id=?", new String[]{String.valueOf(id), userId}); + closeDatabase(); + return rows > 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/repository/CourseRepository.java b/src/main/java/com/example/myapplication/repository/CourseRepository.java new file mode 100644 index 0000000..7786cbc --- /dev/null +++ b/src/main/java/com/example/myapplication/repository/CourseRepository.java @@ -0,0 +1,211 @@ +package com.example.myapplication.repository; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.example.myapplication.domain.Chapter; +import com.example.myapplication.domain.Course; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CourseRepository { + private final Context context; + private DatabaseHelper helper; + private SQLiteDatabase db; + + public CourseRepository(Context context) { this.context = context.getApplicationContext(); } + + public void openDatabase() { + if (helper == null) helper = new DatabaseHelper(context); + if (db == null || !db.isOpen()) db = helper.getWritableDatabase(); + } + + public void closeDatabase() { if (db != null && db.isOpen()) db.close(); db = null; } + + public void close() { closeDatabase(); } + + public List getAllCourses() { + openDatabase(); + List list = new ArrayList<>(); + try (Cursor c = db.query("T_Subject", null, null, null, null, null, "name ASC")) { + while (c != null && c.moveToNext()) list.add(map(c)); + } + closeDatabase(); + return list; + } + + public List getCoursesByMajorAndGrade(Long majorId, String gradeText) { + openDatabase(); + List list = new ArrayList<>(); + if (majorId == null || gradeText == null || gradeText.isEmpty()) { + closeDatabase(); + return list; + } + String sql = "SELECT s.* FROM T_Subject s INNER JOIN T_Major_Subject ms ON ms.subject_id = s.id WHERE ms.major_id = ? AND ms.grade = ? ORDER BY s.name ASC"; + try (Cursor c = db.rawQuery(sql, new String[]{String.valueOf(majorId), gradeText})) { + while (c != null && c.moveToNext()) list.add(map(c)); + } + closeDatabase(); + return list; + } + + public List getCoursesByMajor(Long majorId) { + openDatabase(); + List list = new ArrayList<>(); + if (majorId == null) { closeDatabase(); return list; } + String sql = "SELECT DISTINCT s.* FROM T_Subject s INNER JOIN T_Major_Subject ms ON ms.subject_id = s.id WHERE ms.major_id = ? ORDER BY s.name ASC"; + try (Cursor c = db.rawQuery(sql, new String[]{String.valueOf(majorId)})) { + while (c != null && c.moveToNext()) list.add(map(c)); + } + closeDatabase(); + return list; + } + + public Course getCourseById(long courseId) { + openDatabase(); + Course course = null; + try (Cursor c = db.query("T_Subject", null, "id=?", new String[]{String.valueOf(courseId)}, null, null, null)) { + if (c != null && c.moveToFirst()) course = map(c); + } + closeDatabase(); + return course; + } + + public List findSimilarCourses(String keywords) { + openDatabase(); + List list = new ArrayList<>(); + String like = "%" + (keywords == null ? "" : keywords) + "%"; + try (Cursor c = db.query("T_Subject", null, "name LIKE ? OR description LIKE ? OR code LIKE ?", + new String[]{like, like, like}, null, null, "name ASC")) { + while (c != null && c.moveToNext()) list.add(map(c)); + } + closeDatabase(); + return list; + } + + public Map> getCourseCatalog() { + Map> map = new HashMap<>(); + List courses = getAllCourses(); + for (Course c : courses) { + List chapters = getChaptersBySubjectId(c.getId()); + map.put(c, chapters); + } + return map; + } + + // Optional: helper to insert/update courses + public long upsertCourse(String name, String code, String description) { + openDatabase(); + ContentValues cv = new ContentValues(); + cv.put("name", name); + cv.put("code", code); + cv.put("description", description); + long id = db.insert("T_Subject", null, cv); + closeDatabase(); + return id; + } + + // 新增方法:插入课程及其章节 + public void insertCourseWithChapters(Course course, List chapters) { + openDatabase(); + try { + db.beginTransaction(); + + // 插入课程 + ContentValues courseValues = new ContentValues(); + courseValues.put("id", course.getId()); + courseValues.put("name", course.getName()); + courseValues.put("code", course.getCode()); + courseValues.put("description", course.getDescription()); + db.insert("T_Subject", null, courseValues); + + // 插入章节 + for (Chapter chapter : chapters) { + ContentValues chapterValues = new ContentValues(); + chapterValues.put("id", chapter.getId()); + chapterValues.put("name", chapter.getName()); + chapterValues.put("subject_id", chapter.getCourseId()); + chapterValues.put("order_num", chapter.getOrderIndex()); + if (chapter.getKeywords() != null) chapterValues.put("keywords", chapter.getKeywords()); + if (chapter.getAliases() != null) chapterValues.put("aliases", chapter.getAliases()); + db.insert("T_Chapter", null, chapterValues); + } + + db.setTransactionSuccessful(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + db.endTransaction(); + closeDatabase(); + } + } + + private Course map(Cursor c) { + return new Course( + c.getLong(c.getColumnIndexOrThrow("id")), + c.getString(c.getColumnIndexOrThrow("description")), + c.getString(c.getColumnIndexOrThrow("name")), + c.getString(c.getColumnIndexOrThrow("code")) + ); + } + + public List getChaptersBySubjectId(long subjectId) { + openDatabase(); + List list = new ArrayList<>(); + try (Cursor c = db.query("T_Chapter", null, "subject_id=?", new String[]{String.valueOf(subjectId)}, null, null, "order_num ASC")) { + while (c != null && c.moveToNext()) { + Chapter ch = new Chapter(); + ch.setId(c.getLong(c.getColumnIndexOrThrow("id"))); + ch.setName(c.getString(c.getColumnIndexOrThrow("name"))); + int oi = 0; try { oi = c.getInt(c.getColumnIndexOrThrow("order_num")); } catch (Exception ignored) {} + ch.setOrderIndex(oi); + ch.setCourseId((int) subjectId); + try { + int kwi = c.getColumnIndex("keywords"); + if (kwi >= 0 && !c.isNull(kwi)) ch.setKeywords(c.getString(kwi)); + } catch (Exception ignored) {} + try { + int ali = c.getColumnIndex("aliases"); + if (ali >= 0 && !c.isNull(ali)) ch.setAliases(c.getString(ali)); + } catch (Exception ignored) {} + list.add(ch); + } + } + closeDatabase(); + return list; + } + + public List findChaptersByNameLike(String q, int limit) { + openDatabase(); + List list = new ArrayList<>(); + String like = "%" + (q == null ? "" : q) + "%"; + String lim = String.valueOf(Math.max(1, limit)); + try (Cursor c = db.query("T_Chapter", null, "name LIKE ? OR code LIKE ?", new String[]{like, like}, null, null, "order_num ASC", lim)) { + while (c != null && c.moveToNext()) { + Chapter ch = new Chapter(); + ch.setId(c.getLong(c.getColumnIndexOrThrow("id"))); + ch.setName(c.getString(c.getColumnIndexOrThrow("name"))); + int oi = 0; try { oi = c.getInt(c.getColumnIndexOrThrow("order_num")); } catch (Exception ignored) {} + int sid = 0; try { sid = c.getInt(c.getColumnIndexOrThrow("subject_id")); } catch (Exception ignored) {} + ch.setOrderIndex(oi); + ch.setCourseId(sid); + try { + int kwi = c.getColumnIndex("keywords"); + if (kwi >= 0 && !c.isNull(kwi)) ch.setKeywords(c.getString(kwi)); + } catch (Exception ignored) {} + try { + int ali = c.getColumnIndex("aliases"); + if (ali >= 0 && !c.isNull(ali)) ch.setAliases(c.getString(ali)); + } catch (Exception ignored) {} + list.add(ch); + } + } + closeDatabase(); + return list; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/repository/DatabaseHelper.java b/src/main/java/com/example/myapplication/repository/DatabaseHelper.java new file mode 100644 index 0000000..1c6a03d --- /dev/null +++ b/src/main/java/com/example/myapplication/repository/DatabaseHelper.java @@ -0,0 +1,368 @@ +package com.example.myapplication.repository; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +/** + * Central SQLite helper creating tables for T_User, T_Note, T_Comment, T_Like, T_Course. + */ +public class DatabaseHelper extends SQLiteOpenHelper { + public static final String DB_NAME = "mynote.db"; + private static final int DB_VERSION = 10; + + public DatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + @Override + public void onConfigure(SQLiteDatabase db) { + db.setForeignKeyConstraintsEnabled(true); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE IF NOT EXISTS T_User (" + + "account TEXT PRIMARY KEY," + + "name TEXT," + + "password TEXT," + + "type INTEGER," + + "grade TEXT," + + "major_id INTEGER," + + "point INTEGER," + + "FOREIGN KEY(major_id) REFERENCES T_Major(id) ON DELETE SET NULL" + + ")"); + + db.execSQL("CREATE TABLE IF NOT EXISTS T_Major (" + + "id INTEGER PRIMARY KEY," + + "name TEXT," + + "code TEXT," + + "description TEXT" + + ")"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10001,'计算机科学与技术','CS','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10002,'软件工程','SE','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10003,'电子信息工程','EE','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10004,'电气工程及其自动化','EEA','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10005,'机械工程','ME','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10006,'土木工程','CE','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10007,'建筑学','ARCH','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10008,'材料科学与工程','MSE','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10009,'数学与应用数学','MATH','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10010,'物理学','PHYS','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10011,'化学','CHEM','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10012,'生物科学','BIO','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10013,'医学','MED','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10014,'经济学','ECON','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10015,'金融学','FIN','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10016,'会计学','ACC','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10017,'工商管理','MBA','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10018,'法学','LAW','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10019,'教育学','EDU','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10020,'外国语言文学','LANG','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10021,'思想政治教育','IDEO','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10022,'信息安全','INFOSEC','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10023,'安全工程','SAFEENG','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10024,'应用心理学','APPSY','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10025,'物流管理','LOGMGT','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10026,'飞行器质量与可靠性','AEROQUAL','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10027,'无人驾驶航空器系统工程','UAVSYS','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10028,'飞行器运维工程','AEROMAINT','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10029,'交通工程','TRAENG','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10030,'智慧交通','SMARTTRA','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10031,'交通管理','TRAMGT','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10032,'交通运输','TRANSPORT','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10033,'应用气象学','APPMETEOR','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10034,'飞行技术','FLYTECH','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10035,'机械电子工程','MECHATRON','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10036,'飞行器动力工程','AEROPROP','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10037,'材料科学与工程','MSE2','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10038,'飞行器制造工程','AEROMFG','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10039,'飞行器设计与工程','AERODESIGN','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10040,'油气储运工程','OILGASTR','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10041,'通信工程','COMM','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10042,'物联网工程','IOT','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10043,'自动化','AUTO','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10044,'人工智能','AI','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10045,'信息与计算科学','INFOCS','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10046,'统计学','STAT','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10047,'材料物理','MATERIALPHY','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10048,'材料化学','MATERIALCHEM','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10049,'财务管理','FINMGT','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10050,'公共事业管理','PUBMGT','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10051,'经济与金融','ECONFIN','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10052,'工商管理','BUSADM','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10053,'翻译','TRANS','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10054,'英语','ENGLISH','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10055,'空中乘务','CABIN','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10056,'航空服务艺术与管理','AIRSRV','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10057,'民航空中安全保卫','AIRSEC','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10058,'航空安防管理','AIRSAFE','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10059,'民航安全技术管理','AVSAFETECH','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10060,'飞机电子设备维修','AVIONICSR','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10061,'飞机电设备维修','AIRPOWER','')"); + db.execSQL("INSERT OR IGNORE INTO T_Major (id,name,code,description) VALUES (10062,'民航运输服务','AIRTRANS','')"); + + db.execSQL("CREATE TABLE IF NOT EXISTS T_Subject (" + + "id INTEGER PRIMARY KEY," + + "name TEXT," + + "code TEXT," + + "description TEXT" + + ")"); + + // 课程种子(按常见课程与贵校专业扩展) + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20001,'人工智能','AI','机器学习、深度学习、强化学习')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20002,'数据结构与算法','DSA','线性表、树、图、排序与查找')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20003,'操作系统','OS','进程与线程、调度、内存管理、文件系统')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20004,'计算机网络','NET','TCP/IP、路由、交换、应用层协议')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20005,'数据库系统','DB','SQL、事务、索引、范式与设计')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20006,'信号与系统','SIGSYS','时域与频域、卷积、傅里叶、拉普拉斯')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20007,'数字通信','DIGCOM','调制解调、编码、容量、误码率')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20008,'交通工程导论','TRAENG','道路、交通流、信控、设施设计')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20009,'飞行原理','FLYPRIN','空气动力、升力阻力、机翼特性')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20010,'材料科学基础','MSEBAS','晶体结构、相图、力学与热学性能')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20011,'安全工程导论','SAFEENG','风险评估、隐患排查、应急管理')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20012,'应用气象学','APPMET','环流、边界层、观测与预报')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20013,'高等数学','ADVMath','微积分、线性代数、概率统计')"); + + + db.execSQL("CREATE TABLE IF NOT EXISTS T_Major_Subject (" + + "major_id INTEGER," + + "subject_id INTEGER," + + "grade TEXT NOT NULL DEFAULT ''," + + "PRIMARY KEY(major_id, subject_id, grade)," + + "FOREIGN KEY(major_id) REFERENCES T_Major(id) ON DELETE CASCADE," + + "FOREIGN KEY(subject_id) REFERENCES T_Subject(id) ON DELETE CASCADE" + + ")"); + + // 计算机学院专业-课程映射(按年级限定) + String[] grades = new String[]{"大一","大二","大三","大四"}; + for (String g : grades) { + // 计算机科学与技术(10001)→ 数据结构、操作系统、计算机网络、数据库系统 + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10001,20002,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10001,20003,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10001,20004,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10001,20005,'" + g + "')"); + // 人工智能(10044)→ 人工智能、以及通用基础课 + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20001,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20002,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20003,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20004,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20005,'" + g + "')"); + // 公共数学基础 + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10001,20013,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20013,'" + g + "')"); + } + + db.execSQL("CREATE TABLE IF NOT EXISTS T_Chapter (" + + "id INTEGER PRIMARY KEY," + + "subject_id INTEGER," + + "name TEXT," + + "code TEXT," + + "description TEXT," + + "keywords TEXT," + + "aliases TEXT," + + "order_num INTEGER," + + "FOREIGN KEY(subject_id) REFERENCES T_Subject(id) ON DELETE CASCADE" + + ")"); + db.execSQL("CREATE INDEX IF NOT EXISTS idx_chapter_subject_order ON T_Chapter(subject_id, order_num)"); + + // 章节种子(含关键词与别名,便于自动分类) + // 人工智能 + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30001,20001,'机器学习基础',1,'监督学习,非监督学习,回归,分类,交叉验证','machine learning,ml,supervised,unsupervised')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30002,20001,'深度学习',2,'神经网络,卷积,反向传播,激活函数,梯度下降','deep learning,cnn,rnn,bp,activation')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30003,20001,'强化学习',3,'马尔可夫决策,策略,价值函数,探索利用','reinforcement learning,mdp,policy,value,exploration')"); + // 数据结构与算法 + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30011,20002,'线性结构',1,'数组,链表,栈,队列','array,list,stack,queue')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30012,20002,'树与图',2,'二叉树,平衡树,堆,图遍历,最短路径','tree,graph,dfs,bfs,heap,dijkstra')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30013,20002,'排序与查找',3,'快速排序,归并排序,哈希,二分查找','quicksort,mergesort,hash,binary search')"); + // 操作系统 + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30021,20003,'进程与线程',1,'调度,同步,死锁,临界区','process,thread,schedule,lock')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30022,20003,'内存管理',2,'分页,分段,虚拟内存,置换算法','memory,virtual,paging,mmu')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30023,20003,'文件系统',3,'索引,日志,磁盘,目录结构','filesystem,inode,journal,disk')"); + // 计算机网络 + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30031,20004,'网络层',1,'路由,IP,子网,拥塞','ip,routing,subnet,congestion')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30032,20004,'传输层',2,'TCP,UDP,流控,拥塞控制','tcp,udp,flow')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30033,20004,'应用层',3,'HTTP,DNS,SMTP,FTP','http,dns,email,ftp')"); + // 数据库系统 + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30041,20005,'关系模型与SQL',1,'范式,主键,外键,查询,连接','sql,normal form,join')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30042,20005,'事务与并发',2,'ACID,锁,隔离级别,日志','transaction,acid,lock,isolation')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30043,20005,'索引与优化',3,'B树,哈希索引,执行计划,优化器','index,b-tree,optimizer')"); + // 信号与系统 / 数字通信 + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30051,20006,'时域与频域',1,'卷积,傅里叶变换,频谱','convolution,fourier,spectrum')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30061,20007,'调制与编码',1,'ASK,PSK,QAM,信道编码','modulation,qam,channel coding')"); + // 交通工程 / 飞行原理 / 材料科学基础 / 安全工程 / 应用气象学 + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30071,20008,'信号控制',1,'交叉口,配时,路口控制','signal control,intersection')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30081,20009,'空气动力学基础',1,'升力,阻力,机翼,攻角','aerodynamics,lift,drag,airfoil')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30091,20010,'晶体与相图',1,'晶体结构,相变,相图','crystal,phase diagram')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30101,20011,'风险评估',1,'危险源,隐患,评估方法,应急','risk,hazard,assessment,emergency')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30111,20012,'天气系统',1,'环流,锋面,降水,预报','circulation,front,precipitation,forecast')"); + // 高等数学 + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30121,20013,'微积分',1,'极限,导数,积分,泰勒展开','calculus,derivative,integral,taylor')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30122,20013,'线性代数',2,'矩阵,向量,特征值,特征向量,秩','linear algebra,matrix,eigenvalue,eigenvector,rank')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30123,20013,'概率论与数理统计',3,'概率分布,方差,期望,假设检验,样本','probability,distribution,variance,expectation,hypothesis test')"); + + db.execSQL("CREATE TABLE IF NOT EXISTS T_Note (" + + "id INTEGER PRIMARY KEY," + + "title TEXT," + + "content TEXT," + + "file_path TEXT," + + "uploader_id TEXT," + + "chapter_id INTEGER," + + "download_count INTEGER DEFAULT 0," + + "like_count INTEGER DEFAULT 0," + + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP," + + "FOREIGN KEY(uploader_id) REFERENCES T_User(account) ON DELETE SET NULL," + + "FOREIGN KEY(chapter_id) REFERENCES T_Chapter(id) ON DELETE SET NULL" + + ")"); + + db.execSQL("CREATE INDEX IF NOT EXISTS idx_user_grade ON T_User(grade)"); + db.execSQL("CREATE INDEX IF NOT EXISTS idx_user_major_id ON T_User(major_id)"); + + db.execSQL("CREATE TABLE IF NOT EXISTS T_Comment (" + + "id INTEGER PRIMARY KEY," + + "user_id TEXT," + + "note_id INTEGER," + + "content TEXT," + + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP," + + "FOREIGN KEY(user_id) REFERENCES T_User(account) ON DELETE SET NULL," + + "FOREIGN KEY(note_id) REFERENCES T_Note(id) ON DELETE CASCADE" + + ")"); + + db.execSQL("CREATE TABLE IF NOT EXISTS T_Like (" + + "user_id TEXT," + + "note_id INTEGER," + + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP," + + "PRIMARY KEY(user_id, note_id)," + + "FOREIGN KEY(user_id) REFERENCES T_User(account) ON DELETE CASCADE," + + "FOREIGN KEY(note_id) REFERENCES T_Note(id) ON DELETE CASCADE" + + ")"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + try { + if (oldVersion < 2) { + db.execSQL("CREATE TABLE IF NOT EXISTS T_Note_new (id INTEGER PRIMARY KEY AUTOINCREMENT,uploader_id TEXT,course_id INTEGER,content TEXT,download_count INTEGER DEFAULT 0,created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,title TEXT,file_path TEXT,FOREIGN KEY(uploader_id) REFERENCES T_User(account) ON DELETE SET NULL)"); + db.execSQL("INSERT INTO T_Note_new (id, uploader_id, course_id, content, download_count, created, title, file_path) SELECT id, uploader_id, course_id, COALESCE(ocr_text, description), download_count, created, title, file_path FROM T_Note"); + db.execSQL("DROP TABLE IF EXISTS T_Note"); + db.execSQL("ALTER TABLE T_Note_new RENAME TO T_Note"); + } + + if (oldVersion < 3) { + db.execSQL("ALTER TABLE T_User ADD COLUMN points INTEGER DEFAULT 10"); + } + + if (oldVersion < 5) { + db.execSQL("CREATE TABLE IF NOT EXISTS T_User_new (account TEXT PRIMARY KEY,name TEXT,password TEXT,type INTEGER,mobile TEXT,grade TEXT,major TEXT,point INTEGER)"); + db.execSQL("INSERT INTO T_User_new (account,name,password,type,mobile,grade,major,point) SELECT account,name,password,type,mobile,CASE WHEN grade IS NULL THEN NULL ELSE CAST(grade AS TEXT) END,major,COALESCE(points,0) FROM T_User"); + db.execSQL("DROP TABLE IF EXISTS T_User"); + db.execSQL("ALTER TABLE T_User_new RENAME TO T_User"); + + onCreate(db); + + db.execSQL("CREATE TABLE IF NOT EXISTS T_Note_new (id INTEGER PRIMARY KEY,title TEXT,description TEXT,file_path TEXT,uploader_id TEXT,chapter_id INTEGER,ocr_text TEXT,ocr_status TEXT,download_count INTEGER DEFAULT 0,like_count INTEGER DEFAULT 0,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY(uploader_id) REFERENCES T_User(account) ON DELETE SET NULL,FOREIGN KEY(chapter_id) REFERENCES T_Chapter(id) ON DELETE SET NULL)"); + db.execSQL("INSERT INTO T_Note_new (id,title,description,file_path,uploader_id,chapter_id,ocr_text,ocr_status,download_count,like_count,created_at) SELECT n.id,n.title,COALESCE(n.content,''),n.file_path,n.uploader_id,NULL,NULL,NULL,COALESCE(n.download_count,0),(SELECT COUNT(*) FROM T_Like l WHERE l.note_id = n.id),COALESCE(n.created,CURRENT_TIMESTAMP) FROM T_Note n"); + db.execSQL("DROP TABLE IF EXISTS T_Note"); + db.execSQL("ALTER TABLE T_Note_new RENAME TO T_Note"); + + db.execSQL("CREATE TABLE IF NOT EXISTS T_Comment_new (id INTEGER PRIMARY KEY,user_id TEXT,note_id INTEGER,content TEXT,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY(user_id) REFERENCES T_User(account) ON DELETE SET NULL,FOREIGN KEY(note_id) REFERENCES T_Note(id) ON DELETE CASCADE)"); + db.execSQL("INSERT INTO T_Comment_new (id,user_id,note_id,content,created_at) SELECT id,uploader_id,note_id,content,created_at FROM T_Comment"); + db.execSQL("DROP TABLE IF EXISTS T_Comment"); + db.execSQL("ALTER TABLE T_Comment_new RENAME TO T_Comment"); + } + + if (oldVersion < 6) { + db.execSQL("CREATE TABLE IF NOT EXISTS T_User_new (account TEXT PRIMARY KEY,name TEXT,password TEXT,type INTEGER,mobile TEXT,grade TEXT,major_id INTEGER,point INTEGER,FOREIGN KEY(major_id) REFERENCES T_Major(id) ON DELETE SET NULL)"); + db.execSQL("INSERT INTO T_User_new (account,name,password,type,mobile,grade,major_id,point) SELECT account,name,password,type,mobile,grade,(SELECT id FROM T_Major m WHERE m.name LIKE '%'||COALESCE(T_User.major,'')||'%' OR m.code LIKE '%'||COALESCE(T_User.major,'')||'%' LIMIT 1),COALESCE(point,0) FROM T_User"); + db.execSQL("DROP TABLE IF EXISTS T_User"); + db.execSQL("ALTER TABLE T_User_new RENAME TO T_User"); + } + + if (oldVersion < 7) { + db.execSQL("CREATE TABLE IF NOT EXISTS T_Note_new (id INTEGER PRIMARY KEY,title TEXT,content TEXT,file_path TEXT,uploader_id TEXT,chapter_id INTEGER,download_count INTEGER DEFAULT 0,like_count INTEGER DEFAULT 0,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY(uploader_id) REFERENCES T_User(account) ON DELETE SET NULL,FOREIGN KEY(chapter_id) REFERENCES T_Chapter(id) ON DELETE SET NULL)"); + db.execSQL("INSERT INTO T_Note_new (id,title,content,file_path,uploader_id,chapter_id,download_count,like_count,created_at) SELECT n.id,n.title,COALESCE(n.description,''),n.file_path,n.uploader_id,n.chapter_id,COALESCE(n.download_count,0),COALESCE(n.like_count,(SELECT COUNT(*) FROM T_Like l WHERE l.note_id = n.id)),COALESCE(n.created_at,CURRENT_TIMESTAMP) FROM T_Note n"); + db.execSQL("DROP TABLE IF EXISTS T_Note"); + db.execSQL("ALTER TABLE T_Note_new RENAME TO T_Note"); + } + + if (oldVersion < 8) { + db.execSQL("CREATE TABLE IF NOT EXISTS T_User_new (account TEXT PRIMARY KEY,name TEXT,password TEXT,type INTEGER,grade TEXT,major_id INTEGER,point INTEGER,FOREIGN KEY(major_id) REFERENCES T_Major(id) ON DELETE SET NULL)"); + db.execSQL("INSERT INTO T_User_new (account,name,password,type,grade,major_id,point) SELECT account,name,password,type,grade,major_id,point FROM T_User"); + db.execSQL("DROP TABLE IF EXISTS T_User"); + db.execSQL("ALTER TABLE T_User_new RENAME TO T_User"); + + db.execSQL("CREATE TABLE IF NOT EXISTS T_Major_Subject_new (major_id INTEGER,subject_id INTEGER,grade TEXT NOT NULL DEFAULT '',PRIMARY KEY(major_id, subject_id, grade),FOREIGN KEY(major_id) REFERENCES T_Major(id) ON DELETE CASCADE,FOREIGN KEY(subject_id) REFERENCES T_Subject(id) ON DELETE CASCADE)"); + db.execSQL("INSERT INTO T_Major_Subject_new (major_id,subject_id,grade) SELECT major_id,subject_id,'' FROM T_Major_Subject"); + db.execSQL("DROP TABLE IF EXISTS T_Major_Subject"); + db.execSQL("ALTER TABLE T_Major_Subject_new RENAME TO T_Major_Subject"); + } + + if (oldVersion < 9) { + try { db.execSQL("ALTER TABLE T_Chapter ADD COLUMN keywords TEXT"); } catch (Exception ignored) {} + try { db.execSQL("ALTER TABLE T_Chapter ADD COLUMN aliases TEXT"); } catch (Exception ignored) {} + } + if (oldVersion < 10) { + String[] grades2 = new String[]{"大一","大二","大三","大四"}; + for (String g : grades2) { + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10001,20013,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20013,'" + g + "')"); + } + } + if (oldVersion < 10) { + // 补种课程与章节(老安装无需清数据即可获得新课程/章节) + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20001,'人工智能','AI','机器学习、深度学习、强化学习')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20002,'数据结构与算法','DSA','线性表、树、图、排序与查找')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20003,'操作系统','OS','进程与线程、调度、内存管理、文件系统')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20004,'计算机网络','NET','TCP/IP、路由、交换、应用层协议')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20005,'数据库系统','DB','SQL、事务、索引、范式与设计')"); + db.execSQL("INSERT OR IGNORE INTO T_Subject (id,name,code,description) VALUES (20013,'高等数学','ADVMath','微积分、线性代数、概率统计')"); + + // 补种关键章节与关键词/别名 + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30001,20001,'机器学习基础',1,'监督学习,非监督学习,回归,分类,交叉验证','machine learning,ml,supervised,unsupervised')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30002,20001,'深度学习',2,'神经网络,卷积,反向传播,激活函数,梯度下降','deep learning,cnn,rnn,bp,activation')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30003,20001,'强化学习',3,'马尔可夫决策,策略,价值函数,探索利用','reinforcement learning,mdp,policy,value,exploration')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30011,20002,'线性结构',1,'数组,链表,栈,队列','array,list,stack,queue')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30012,20002,'树与图',2,'二叉树,平衡树,堆,图遍历,最短路径','tree,graph,dfs,bfs,heap,dijkstra')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30013,20002,'排序与查找',3,'快速排序,归并排序,哈希,二分查找','quicksort,mergesort,hash,binary search')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30021,20003,'进程与线程',1,'调度,同步,死锁,临界区','process,thread,schedule,lock')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30022,20003,'内存管理',2,'分页,分段,虚拟内存,置换算法','memory,virtual,paging,mmu')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30023,20003,'文件系统',3,'索引,日志,磁盘,目录结构','filesystem,inode,journal,disk')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30031,20004,'网络层',1,'路由,IP,子网,拥塞','ip,routing,subnet,congestion')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30032,20004,'传输层',2,'TCP,UDP,流控,拥塞控制','tcp,udp,flow')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30033,20004,'应用层',3,'HTTP,DNS,SMTP,FTP','http,dns,email,ftp')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30041,20005,'关系模型与SQL',1,'范式,主键,外键,查询,连接','sql,normal form,join')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30042,20005,'事务与并发',2,'ACID,锁,隔离级别,日志','transaction,acid,lock,isolation')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30043,20005,'索引与优化',3,'B树,哈希索引,执行计划,优化器','index,b-tree,optimizer')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30121,20013,'微积分',1,'极限,导数,积分,泰勒展开','calculus,derivative,integral,taylor')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30122,20013,'线性代数',2,'矩阵,向量,特征值,特征向量,秩','linear algebra,matrix,eigenvalue,eigenvector,rank')"); + db.execSQL("INSERT OR IGNORE INTO T_Chapter (id,subject_id,name,order_num,keywords,aliases) VALUES (30123,20013,'概率论与数理统计',3,'概率分布,方差,期望,假设检验,样本','probability,distribution,variance,expectation,hypothesis test')"); + + // 计算机学院两专业课程映射(含公共数学) + String[] grades2 = new String[]{"大一","大二","大三","大四"}; + for (String g : grades2) { + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10001,20002,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10001,20003,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10001,20004,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10001,20005,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20001,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20002,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20003,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20004,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20005,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10001,20013,'" + g + "')"); + db.execSQL("INSERT OR IGNORE INTO T_Major_Subject (major_id,subject_id,grade) VALUES (10044,20013,'" + g + "')"); + } + } + } catch (Exception e) { + db.execSQL("DROP TABLE IF EXISTS T_Comment"); + db.execSQL("DROP TABLE IF EXISTS T_Like"); + db.execSQL("DROP TABLE IF EXISTS T_Note"); + db.execSQL("DROP TABLE IF EXISTS T_Chapter"); + db.execSQL("DROP TABLE IF EXISTS T_Major_Subject"); + db.execSQL("DROP TABLE IF EXISTS T_Subject"); + db.execSQL("DROP TABLE IF EXISTS T_Major"); + db.execSQL("DROP TABLE IF EXISTS T_User"); + onCreate(db); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/repository/DatabaseManager.java b/src/main/java/com/example/myapplication/repository/DatabaseManager.java new file mode 100644 index 0000000..406646c --- /dev/null +++ b/src/main/java/com/example/myapplication/repository/DatabaseManager.java @@ -0,0 +1,33 @@ +package com.example.myapplication.repository; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; + +public final class DatabaseManager { + private static DatabaseHelper helper; + private static SQLiteDatabase db; + + private DatabaseManager() {} + + public static synchronized void init(Context ctx) { + if (helper == null) { + helper = new DatabaseHelper(ctx.getApplicationContext()); + db = helper.getWritableDatabase(); // 打开并保持 + } + } + + public static synchronized SQLiteDatabase getDb() { + if (db == null && helper != null) { + db = helper.getWritableDatabase(); + } + return db; + } + + public static synchronized void close() { + if (helper != null) { + helper.close(); + db = null; + helper = null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/repository/LikeRepository.java b/src/main/java/com/example/myapplication/repository/LikeRepository.java new file mode 100644 index 0000000..afb9584 --- /dev/null +++ b/src/main/java/com/example/myapplication/repository/LikeRepository.java @@ -0,0 +1,81 @@ +package com.example.myapplication.repository; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +public class LikeRepository { + private final Context context; + private DatabaseHelper helper; + private SQLiteDatabase db; + + public LikeRepository(Context context) { this.context = context.getApplicationContext(); } + + public void openDatabase() { + if (helper == null) helper = new DatabaseHelper(context); + if (db == null || !db.isOpen()) db = helper.getWritableDatabase(); + } + + public void closeDatabase() { if (db != null && db.isOpen()) db.close(); db = null; } + + public boolean addLike(String userId, long noteId) { + openDatabase(); + boolean ok = false; + db.beginTransaction(); + try { + ContentValues cv = new ContentValues(); + cv.put("user_id", userId); + cv.put("note_id", noteId); + long r = db.insertWithOnConflict("T_Like", null, cv, SQLiteDatabase.CONFLICT_IGNORE); + if (r != -1) { + db.execSQL("UPDATE T_Note SET like_count = COALESCE(like_count,0) + 1 WHERE id=?", new Object[]{noteId}); + ok = true; + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + closeDatabase(); + return ok; + } + + public boolean removeLike(String userId, long noteId) { + openDatabase(); + boolean ok = false; + db.beginTransaction(); + try { + int rows = db.delete("T_Like", "user_id=? AND note_id=?", new String[]{userId, String.valueOf(noteId)}); + if (rows > 0) { + db.execSQL("UPDATE T_Note SET like_count = CASE WHEN COALESCE(like_count,0) > 0 THEN like_count - 1 ELSE 0 END WHERE id=?", new Object[]{noteId}); + ok = true; + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + closeDatabase(); + return ok; + } + + public boolean checkUserLikeStatus(String userId, long noteId) { + openDatabase(); + boolean exists = false; + try (Cursor c = db.query("T_Like", new String[]{"user_id"}, "user_id=? AND note_id=?", + new String[]{userId, String.valueOf(noteId)}, null, null, null)) { + exists = c != null && c.moveToFirst(); + } + closeDatabase(); + return exists; + } + + public int getLikeCount(long noteId) { + openDatabase(); + int count = 0; + try (Cursor c = db.rawQuery("SELECT COUNT(*) FROM T_Like WHERE note_id=?", new String[]{String.valueOf(noteId)})) { + if (c != null && c.moveToFirst()) count = c.getInt(0); + } + closeDatabase(); + return count; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/repository/MajorRepository.java b/src/main/java/com/example/myapplication/repository/MajorRepository.java new file mode 100644 index 0000000..a488d33 --- /dev/null +++ b/src/main/java/com/example/myapplication/repository/MajorRepository.java @@ -0,0 +1,56 @@ +package com.example.myapplication.repository; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.example.myapplication.domain.Major; + +import java.util.ArrayList; +import java.util.List; + +public class MajorRepository { + private final Context context; + private DatabaseHelper helper; + private SQLiteDatabase db; + + public MajorRepository(Context context) { this.context = context.getApplicationContext(); } + + public void openDatabase() { + if (helper == null) helper = new DatabaseHelper(context); + if (db == null || !db.isOpen()) db = helper.getWritableDatabase(); + } + + public void closeDatabase() { if (db != null && db.isOpen()) db.close(); db = null; } + + public List findByNameLike(String keywords, int limit) { + openDatabase(); + List list = new ArrayList<>(); + String like = "%" + (keywords == null ? "" : keywords.trim()) + "%"; + try (Cursor c = db.query("T_Major", null, "name LIKE ? OR code LIKE ?", + new String[]{like, like}, null, null, "name ASC", String.valueOf(Math.max(1, limit)) )) { + while (c != null && c.moveToNext()) list.add(map(c)); + } + closeDatabase(); + return list; + } + + public Major getById(long id) { + openDatabase(); + Major m = null; + try (Cursor c = db.query("T_Major", null, "id=?", new String[]{String.valueOf(id)}, null, null, null)) { + if (c != null && c.moveToFirst()) m = map(c); + } + closeDatabase(); + return m; + } + + private Major map(Cursor c) { + return new Major( + c.getLong(c.getColumnIndexOrThrow("id")), + c.getString(c.getColumnIndexOrThrow("name")), + c.getString(c.getColumnIndexOrThrow("code")), + c.getString(c.getColumnIndexOrThrow("description")) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/repository/NoteRepository.java b/src/main/java/com/example/myapplication/repository/NoteRepository.java new file mode 100644 index 0000000..5ca748c --- /dev/null +++ b/src/main/java/com/example/myapplication/repository/NoteRepository.java @@ -0,0 +1,417 @@ +package com.example.myapplication.repository; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.example.myapplication.domain.Note; + +import java.util.ArrayList; +import java.util.List; + +public class NoteRepository { + private final Context context; + private DatabaseHelper helper; + private SQLiteDatabase db; + + private static final java.util.Map> recommendedCache = new java.util.LinkedHashMap>() { + @Override + protected boolean removeEldestEntry(java.util.Map.Entry> eldest) { + return size() > 20; // 最多缓存20个键 + } + }; + + public static void clearRecommendedCacheFor(Long userMajorId, String userGradeText) { + String majorKey = userMajorId == null ? "" : String.valueOf(userMajorId); + String gradeArg = userGradeText == null ? "" : userGradeText; + String cacheKey = majorKey + "|" + gradeArg; + recommendedCache.remove(cacheKey); + } + + public NoteRepository(Context context) { this.context = context.getApplicationContext(); } + + public void openDatabase() { + if (helper == null) helper = new DatabaseHelper(context); + if (db == null || !db.isOpen()) db = helper.getWritableDatabase(); + } + + public void closeDatabase() { if (db != null && db.isOpen()) db.close(); db = null; } + + public void close() { closeDatabase(); } + + /** + * 插入笔记对象(用于NoteUploadActivity) + * @param note 笔记对象 + * @return 插入成功返回笔记ID,失败返回-1 + */ + public long insertNote(Note note) { + if (note == null) return -1; + + openDatabase(); + try { + ContentValues cv = new ContentValues(); + cv.put("title", note.getTitle()); + cv.put("content", note.getDescription()); + cv.put("file_path", note.getFilePath()); + cv.put("chapter_id", note.getChapterId()); + cv.put("uploader_id", note.getUploaderId()); + + // 处理新的字段 + if (note.getOcrText() != null) { + cv.put("ocr_text", note.getOcrText()); + } + if (note.getUploadTime() > 0) { + cv.put("upload_time", note.getUploadTime()); + } + if (note.getUpdateTime() > 0) { + cv.put("update_time", note.getUpdateTime()); + } + + long result = db.insert("T_Note", null, cv); + return result; + } catch (Exception e) { + e.printStackTrace(); + return -1; + } finally { + closeDatabase(); + } + } + + // 保存(long 版本) + public boolean saveNote(String title, String content, String filePath, long courseId, String uploaderId) { + openDatabase(); + ContentValues cv = new ContentValues(); + cv.put("title", title); + cv.put("content", content); + cv.put("file_path", filePath); + if (courseId <= 0) { cv.putNull("chapter_id"); } else { cv.put("chapter_id", courseId); } + if (uploaderId == null || uploaderId.isEmpty()) { cv.putNull("uploader_id"); } else { cv.put("uploader_id", uploaderId); } + + long r = db.insert("T_Note", null, cv); + closeDatabase(); + return r != -1; + } + + // 保存(Long 版本) + public boolean saveNote(String title, String content, String filePath, Long courseId, String uploaderId) { + openDatabase(); + ContentValues cv = new ContentValues(); + cv.put("title", title); + cv.put("content", content); + cv.put("file_path", filePath); + if (courseId == null) { cv.putNull("chapter_id"); } else { cv.put("chapter_id", courseId); } + if (uploaderId == null || uploaderId.isEmpty()) { cv.putNull("uploader_id"); } else { cv.put("uploader_id", uploaderId); } + + long r = db.insert("T_Note", null, cv); + closeDatabase(); + return r != -1; + } + + // 新增重载:支持可空的 courseId / uploaderId + public boolean saveNoteWithSentinel(String title, String description, String filePath, long courseIdOrMinus1, String uploaderId) { + openDatabase(); + ContentValues cv = new ContentValues(); + cv.put("title", title); + cv.put("content", description); + cv.put("file_path", filePath); + if (courseIdOrMinus1 == -1L) { + cv.putNull("chapter_id"); + } else { + cv.put("chapter_id", courseIdOrMinus1); + } + + if (uploaderId == null || uploaderId.isEmpty()) { + cv.putNull("uploader_id"); + } else { + cv.put("uploader_id", uploaderId); + } + + long r = db.insert("T_Note", null, cv); + closeDatabase(); + return r != -1; + } + + public boolean deleteNote(long noteId) { + com.example.myapplication.domain.Note n = getNoteById(noteId); + String currentUser = com.example.myapplication.service.AuthService.getCurrentUser(context); + if (n == null || n.getUploaderId() == null || currentUser == null || !n.getUploaderId().equals(currentUser)) { + return false; + } + openDatabase(); + int rows = db.delete("T_Note", "id=?", new String[]{String.valueOf(noteId)}); + closeDatabase(); + return rows > 0; + } + + public boolean updateNote(long noteId, ContentValues updateData) { + boolean needsOwner = false; + if (updateData != null) { + needsOwner = updateData.containsKey("title") + || updateData.containsKey("content") + || updateData.containsKey("file_path") + || updateData.containsKey("chapter_id"); + } + if (needsOwner) { + com.example.myapplication.domain.Note n = getNoteById(noteId); + String currentUser = com.example.myapplication.service.AuthService.getCurrentUser(context); + if (n == null || n.getUploaderId() == null || currentUser == null || !n.getUploaderId().equals(currentUser)) { + return false; + } + } + openDatabase(); + int rows = db.update("T_Note", updateData, "id=?", new String[]{String.valueOf(noteId)}); + closeDatabase(); + return rows > 0; + } + + public Note getNoteById(long noteId) { + openDatabase(); + Note note = null; + try (Cursor c = db.query("T_Note", null, "id=?", new String[]{String.valueOf(noteId)}, null, null, null)) { + if (c != null && c.moveToFirst()) { + note = map(c); + } + } + closeDatabase(); + return note; + } + + public List getNotesByUser(String userId) { + openDatabase(); + List list = new ArrayList<>(); + try (Cursor c = db.query("T_Note", null, "uploader_id=?", new String[]{userId}, null, null, "created_at DESC")) { + while (c != null && c.moveToNext()) list.add(map(c)); + } + closeDatabase(); + return list; + } + + public List findNotesByKeywords(String keywords, String filters) { + openDatabase(); + List list = new ArrayList<>(); + String q = keywords == null ? "" : keywords.trim(); + String like = "%" + q + "%"; + + // 1) 章节匹配 + List chapterIds = new ArrayList<>(); + try (Cursor c1 = db.rawQuery("SELECT id FROM T_Chapter WHERE name LIKE ? OR code LIKE ? ORDER BY order_num ASC", new String[]{like, like})) { + while (c1 != null && c1.moveToNext()) chapterIds.add(c1.getLong(0)); + } + if (!chapterIds.isEmpty()) { + StringBuilder in = new StringBuilder(); + for (int i = 0; i < chapterIds.size(); i++) { if (i > 0) in.append(","); in.append("?"); } + String sql = "SELECT * FROM T_Note WHERE chapter_id IN (" + in + ") ORDER BY created_at DESC"; + String[] args = new String[chapterIds.size()]; + for (int i = 0; i < chapterIds.size(); i++) args[i] = String.valueOf(chapterIds.get(i)); + try (Cursor c = db.rawQuery(sql, args)) { while (c != null && c.moveToNext()) list.add(map(c)); } + if (!list.isEmpty()) { closeDatabase(); return list; } + } + + // 2) 科目匹配(包含描述) + List subjectIds = new ArrayList<>(); + try (Cursor c2 = db.rawQuery("SELECT id FROM T_Subject WHERE name LIKE ? OR code LIKE ? OR description LIKE ? ORDER BY name ASC", new String[]{like, like, like})) { + while (c2 != null && c2.moveToNext()) subjectIds.add(c2.getLong(0)); + } + if (!subjectIds.isEmpty()) { + StringBuilder in = new StringBuilder(); + for (int i = 0; i < subjectIds.size(); i++) { if (i > 0) in.append(","); in.append("?"); } + String sql = "SELECT n.* FROM T_Note n WHERE n.chapter_id IN (SELECT ch.id FROM T_Chapter ch WHERE ch.subject_id IN (" + in + ")) ORDER BY n.created_at DESC"; + String[] args = new String[subjectIds.size()]; + for (int i = 0; i < subjectIds.size(); i++) args[i] = String.valueOf(subjectIds.get(i)); + try (Cursor c = db.rawQuery(sql, args)) { while (c != null && c.moveToNext()) list.add(map(c)); } + if (!list.isEmpty()) { closeDatabase(); return list; } + } + + // 3) 专业匹配 + List majorIds = new ArrayList<>(); + try (Cursor c3 = db.rawQuery("SELECT id FROM T_Major WHERE name LIKE ? OR code LIKE ? ORDER BY name ASC", new String[]{like, like})) { + while (c3 != null && c3.moveToNext()) majorIds.add(c3.getLong(0)); + } + if (!majorIds.isEmpty()) { + StringBuilder in = new StringBuilder(); + for (int i = 0; i < majorIds.size(); i++) { if (i > 0) in.append(","); in.append("?"); } + String sql = "SELECT n.* FROM T_Note n WHERE n.chapter_id IN (SELECT ch.id FROM T_Chapter ch WHERE ch.subject_id IN (SELECT ms.subject_id FROM T_Major_Subject ms WHERE ms.major_id IN (" + in + "))) ORDER BY n.created_at DESC"; + String[] args = new String[majorIds.size()]; + for (int i = 0; i < majorIds.size(); i++) args[i] = String.valueOf(majorIds.get(i)); + try (Cursor c = db.rawQuery(sql, args)) { while (c != null && c.moveToNext()) list.add(map(c)); } + if (!list.isEmpty()) { closeDatabase(); return list; } + } + + // 4) 标题/内容模糊 + String selection = "title LIKE ? OR content LIKE ?"; + String[] args = new String[]{like, like}; + try (Cursor c = db.query("T_Note", null, selection, args, null, null, "created_at DESC")) { + while (c != null && c.moveToNext()) list.add(map(c)); + } + closeDatabase(); + return list; + } + + // 新增:按标题模糊查询,仅匹配 title + public java.util.List findNotesByTitleLike(String keywords) { + openDatabase(); + java.util.List list = new java.util.ArrayList<>(); + String like = "%" + (keywords == null ? "" : keywords) + "%"; + try (android.database.Cursor c = db.query( + "T_Note", + null, + "title LIKE ?", + new String[]{like}, + null, + null, + "created_at DESC" + )) { + while (c != null && c.moveToNext()) list.add(map(c)); + } + closeDatabase(); + return list; + } + + public java.util.List getRecentNotes(int limit) { + openDatabase(); + java.util.List list = new java.util.ArrayList<>(); + String orderBy = "created_at DESC"; + String limitStr = String.valueOf(Math.max(1, limit)); + try (android.database.Cursor c = db.query("T_Note", null, null, null, null, null, orderBy, limitStr)) { + while (c != null && c.moveToNext()) list.add(map(c)); + } + closeDatabase(); + return list; + } + + private Note map(Cursor c) { + Integer pg = null; + try { + int gi = c.getColumnIndex("publisher_grade"); + if (gi >= 0 && !c.isNull(gi)) pg = c.getInt(gi); + } catch (Exception ignored) {} + String pm = null; + try { int mi = c.getColumnIndex("publisher_major"); if (mi >= 0) pm = c.getString(mi); } catch (Exception ignored) {} + long chapterId = 0; + try { chapterId = c.getLong(c.getColumnIndexOrThrow("chapter_id")); } catch (Exception ignored) {} + String created = null; + try { created = c.getString(c.getColumnIndexOrThrow("created_at")); } catch (Exception ignored) {} + int likeCount = 0; + try { likeCount = c.getInt(c.getColumnIndexOrThrow("like_count")); } catch (Exception ignored) {} + String content = null; + try { content = c.getString(c.getColumnIndexOrThrow("content")); } catch (Exception ignored) {} + return new Note( + c.getLong(c.getColumnIndexOrThrow("id")), + c.getString(c.getColumnIndexOrThrow("uploader_id")), + chapterId, + content, + c.getInt(c.getColumnIndexOrThrow("download_count")), + likeCount, + created, + c.getString(c.getColumnIndexOrThrow("title")), + c.getString(c.getColumnIndexOrThrow("file_path")), + pg, + pm + ); + } + + public int getTotalLikesByUser(String userId) { + openDatabase(); + int total = 0; + try (android.database.Cursor c = db.rawQuery("SELECT COALESCE(SUM(like_count),0) FROM T_Note WHERE uploader_id=?", new String[]{userId})) { + if (c != null && c.moveToFirst()) total = c.getInt(0); + } + closeDatabase(); + return total; + } + + // 推荐查询:按优先级排序(同专业同年级 > 同专业 > 同年级 > 基础学科),支持分页 + public List getRecommendedNotes(Long userMajorId, String userGradeText, int limit, int offset) { + openDatabase(); + List list = new ArrayList<>(); + + String gradeArg = userGradeText == null ? "" : userGradeText; + String majorKey = userMajorId == null ? "" : String.valueOf(userMajorId); + String cacheKey = majorKey + "|" + gradeArg; + if (offset == 0) { + java.util.List cached = recommendedCache.get(cacheKey); + if (cached != null) { + for (int i = 0; i < Math.min(limit, cached.size()); i++) list.add(cached.get(i)); + closeDatabase(); + return list; + } + } + + String sqlPrimary = + "SELECT n.*, " + + " m.name AS publisher_major_name, " + + " u.grade AS publisher_grade_text, " + + " CASE " + + " WHEN u.major_id = ? AND u.grade = ? THEN 1000 " + + " WHEN u.major_id = ? THEN 800 " + + " WHEN u.grade = ? THEN 600 " + + " ELSE 0 " + + " END AS relevance " + + "FROM T_Note n " + + "LEFT JOIN T_User u ON u.account = n.uploader_id " + + "LEFT JOIN T_Major m ON m.id = u.major_id " + + "LEFT JOIN T_Chapter ch ON ch.id = n.chapter_id " + + "LEFT JOIN T_Subject s ON s.id = ch.subject_id " + + "WHERE NOT (s.id = 20013 OR s.name LIKE '%高等数学%' OR s.code = 'ADVMath') " + + "ORDER BY relevance DESC, n.like_count DESC, n.created_at DESC " + + "LIMIT ? OFFSET ?"; + + String[] argsPrimary = new String[]{ + userMajorId == null ? "-1" : String.valueOf(userMajorId), + gradeArg, + userMajorId == null ? "-1" : String.valueOf(userMajorId), + gradeArg, + String.valueOf(limit), + String.valueOf(offset) + }; + + try (Cursor c = db.rawQuery(sqlPrimary, argsPrimary)) { + while (c != null && c.moveToNext()) { + Note n = map(c); + try { + int mi = c.getColumnIndex("publisher_major_name"); + if (mi >= 0 && !c.isNull(mi)) n.setPublisherMajor(c.getString(mi)); + } catch (Exception ignored) {} + list.add(n); + } + } + + if (list.isEmpty()) { + String sqlFallback = + "SELECT n.*, " + + " m.name AS publisher_major_name, " + + " u.grade AS publisher_grade_text " + + "FROM T_Note n " + + "LEFT JOIN T_User u ON u.account = n.uploader_id " + + "LEFT JOIN T_Major m ON m.id = u.major_id " + + "LEFT JOIN T_Chapter ch ON ch.id = n.chapter_id " + + "LEFT JOIN T_Subject s ON s.id = ch.subject_id " + + "WHERE (s.id = 20013 OR s.name LIKE '%高等数学%' OR s.code = 'ADVMath') " + + "ORDER BY n.like_count DESC, n.created_at DESC " + + "LIMIT ? OFFSET ?"; + + String[] argsFallback = new String[]{ + String.valueOf(limit), + String.valueOf(offset) + }; + + try (Cursor c2 = db.rawQuery(sqlFallback, argsFallback)) { + while (c2 != null && c2.moveToNext()) { + Note n = map(c2); + try { + int mi = c2.getColumnIndex("publisher_major_name"); + if (mi >= 0 && !c2.isNull(mi)) n.setPublisherMajor(c2.getString(mi)); + } catch (Exception ignored) {} + list.add(n); + } + } + } + + if (offset == 0) { + recommendedCache.put(cacheKey, new java.util.ArrayList<>(list)); + } + closeDatabase(); + return list; + } +} diff --git a/src/main/java/com/example/myapplication/repository/UserRepository.java b/src/main/java/com/example/myapplication/repository/UserRepository.java new file mode 100644 index 0000000..cf8951c --- /dev/null +++ b/src/main/java/com/example/myapplication/repository/UserRepository.java @@ -0,0 +1,154 @@ +package com.example.myapplication.repository; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.example.myapplication.domain.User; + +public class UserRepository { + private final Context context; + private DatabaseHelper helper; + private SQLiteDatabase db; + + public UserRepository(Context context) { + this.context = context.getApplicationContext(); + } + + public void openDatabase() { + if (helper == null) helper = new DatabaseHelper(context); + if (db == null || !db.isOpen()) db = helper.getWritableDatabase(); + } + + public void closeDatabase() { + if (db != null && db.isOpen()) db.close(); + db = null; + } + + public boolean insertUser(User user) { + openDatabase(); + ContentValues cv = new ContentValues(); + cv.put("account", user.getAccount()); + cv.put("name", user.getName()); + cv.put("password", user.getPassword()); + cv.put("type", user.getType()); + cv.put("point", user.getPoints()); + if (user.getGrade() != null) cv.put("grade", String.valueOf(user.getGrade())); else cv.putNull("grade"); + if (user.getMajorId() != null) cv.put("major_id", user.getMajorId()); else cv.putNull("major_id"); + long r = db.insertWithOnConflict("T_User", null, cv, SQLiteDatabase.CONFLICT_REPLACE); + closeDatabase(); + return r != -1; + } + + public boolean insertUserWithGradeText(User user, String gradeText) { + openDatabase(); + ContentValues cv = new ContentValues(); + cv.put("account", user.getAccount()); + cv.put("name", user.getName()); + cv.put("password", user.getPassword()); + cv.put("type", user.getType()); + cv.put("point", user.getPoints()); + if (gradeText != null && !gradeText.isEmpty()) cv.put("grade", gradeText); else cv.putNull("grade"); + if (user.getMajorId() != null) cv.put("major_id", user.getMajorId()); else cv.putNull("major_id"); + long r = db.insertWithOnConflict("T_User", null, cv, SQLiteDatabase.CONFLICT_REPLACE); + closeDatabase(); + return r != -1; + } + + public boolean deleteUser(User user) { + openDatabase(); + int rows = db.delete("T_User", "account=?", new String[]{user.getAccount()}); + closeDatabase(); + return rows > 0; + } + + public boolean updateUser(User user) { + openDatabase(); + ContentValues cv = new ContentValues(); + cv.put("name", user.getName()); + cv.put("password", user.getPassword()); + cv.put("type", user.getType()); + if (user.getPoints() >= 0) cv.put("point", user.getPoints()); + if (user.getGrade() != null) cv.put("grade", String.valueOf(user.getGrade())); + if (user.getMajorId() != null) cv.put("major_id", user.getMajorId()); + int rows = db.update("T_User", cv, "account=?", new String[]{user.getAccount()}); + closeDatabase(); + return rows > 0; + } + + public User getUserByAccount(String account) { + openDatabase(); + User user = null; + try (Cursor c = db.query("T_User", null, "account=?", new String[]{account}, null, null, null)) { + if (c != null && c.moveToFirst()) { + Integer grade = null; + try { + int gi = c.getColumnIndex("grade"); + if (gi >= 0 && !c.isNull(gi)) { + String gs = c.getString(gi); + try { grade = gs == null ? null : Integer.parseInt(gs.trim()); } catch (Exception e) { grade = null; } + } + } catch (Exception ignored) {} + Long majorId = null; + try { int mi = c.getColumnIndex("major_id"); if (mi >= 0 && !c.isNull(mi)) majorId = c.getLong(mi); } catch (Exception ignored) {} + user = new User( + c.getString(c.getColumnIndexOrThrow("account")), + c.getString(c.getColumnIndexOrThrow("name")), + c.getString(c.getColumnIndexOrThrow("password")), + c.getInt(c.getColumnIndexOrThrow("type")), + null, + c.getInt(c.getColumnIndexOrThrow("point")), + grade, + majorId + ); + } + } + closeDatabase(); + return user; + } + + public boolean verifyUserValidity(String account, String password) { + openDatabase(); + boolean ok = false; + try (Cursor c = db.query("T_User", new String[]{"account"}, "account=? AND password=?", + new String[]{account, password}, null, null, null)) { + ok = c != null && c.moveToFirst(); + } + closeDatabase(); + return ok; + } + + public int getPoints(String account) { + openDatabase(); + int pts = 0; + try (Cursor c = db.query("T_User", new String[]{"point"}, "account=?", + new String[]{account}, null, null, null)) { + if (c != null && c.moveToFirst()) pts = c.getInt(0); + } + closeDatabase(); + return pts; + } + + public String getGradeText(String account) { + openDatabase(); + String grade = null; + try (Cursor c = db.query("T_User", new String[]{"grade"}, "account=?", + new String[]{account}, null, null, null)) { + if (c != null && c.moveToFirst()) { + int idx = c.getColumnIndex("grade"); + if (idx >= 0 && !c.isNull(idx)) grade = c.getString(idx); + } + } + closeDatabase(); + return grade; + } + + public boolean addPoints(String account, int delta) { + openDatabase(); + db.execSQL("UPDATE T_User SET point = COALESCE(point,0) + ? WHERE account=?", + new Object[]{delta, account}); + closeDatabase(); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/AuthService.java b/src/main/java/com/example/myapplication/service/AuthService.java new file mode 100644 index 0000000..977bfd3 --- /dev/null +++ b/src/main/java/com/example/myapplication/service/AuthService.java @@ -0,0 +1,75 @@ +package com.example.myapplication.service; + +import android.content.Context; +import android.content.SharedPreferences; + +public class AuthService { + public static boolean login(Context ctx, String username, String password) { + if (username == null || password == null) return false; + username = username.trim(); + password = password.trim(); + if (username.isEmpty() || password.isEmpty()) return false; + + // verify against repository + com.example.myapplication.repository.UserRepository repo = new com.example.myapplication.repository.UserRepository(ctx); + boolean valid = repo.verifyUserValidity(username, password); + if (!valid) return false; + + SharedPreferences prefs = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + prefs.edit() + .putString("currentUser", username) + .putBoolean("isLoggedIn", true) + .apply(); + return true; + } + + public static void logout(Context ctx) { + SharedPreferences prefs = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + prefs.edit() + .remove("isLoggedIn") + .remove("currentUser") + .remove("searchKeyword") + .remove("filterSubject") + .remove("currentNoteId") + .apply(); + } + + public static boolean isLoggedIn(Context ctx) { + SharedPreferences prefs = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + return prefs.getBoolean("isLoggedIn", false); + } + + public static String getCurrentUser(Context ctx) { + SharedPreferences prefs = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + return prefs.getString("currentUser", "张三"); + } + + public static String validateRegistration(String username, String password, String confirm, String gradeName, Long majorId) { + if (username == null || password == null || confirm == null || gradeName == null || majorId == null) return "请输入完整信息"; + username = username.trim(); + password = password.trim(); + confirm = confirm.trim(); + if (username.length() < 3 || username.length() > 20) { + return "用户名长度应为3-20个字符"; + } + if (password.length() < 6) { + return "密码长度不能少于6位"; + } + if (!password.equals(confirm)) { + return "两次输入的密码不一致"; + } + java.util.List allowed = java.util.Arrays.asList("大一","大二","大三","大四"); + if (!allowed.contains(gradeName)) { + return "请选择正确的年级"; + } + return null; // null 表示校验通过 + } + + // Persist user to repository after validation + public static boolean register(Context ctx, String username, String password, String gradeName, Long majorId) { + com.example.myapplication.repository.UserRepository repo = new com.example.myapplication.repository.UserRepository(ctx); + com.example.myapplication.domain.User u = + new com.example.myapplication.domain.User(username, username, password, 0, "", 10, null, majorId); + return repo.insertUserWithGradeText(u, gradeName); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/ClassificationService.java b/src/main/java/com/example/myapplication/service/ClassificationService.java new file mode 100644 index 0000000..00e90c3 --- /dev/null +++ b/src/main/java/com/example/myapplication/service/ClassificationService.java @@ -0,0 +1,168 @@ +package com.example.myapplication.service; + +import android.content.Context; +import android.text.TextUtils; +import com.example.myapplication.repository.CourseRepository; +import com.example.myapplication.domain.Course; +import com.example.myapplication.domain.Chapter; +import com.hankcs.hanlp.HanLP; +import com.hankcs.hanlp.seg.common.Term; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public class ClassificationService { + public static class Result { + public String category = ""; + public String chapter = ""; + public String bestId = ""; + public String bestTitle = ""; + public double confidence = 0.0; + public boolean isSegmented = false; + public java.util.List topCandidates = new java.util.ArrayList<>(); + } + + public static class Candidate { + public long chapterId; + public long subjectId; + public String title; + public double score; + } + + private final Context context; + private final CourseRepository courseRepository; + private boolean hanLPInitialized = false; + + public ClassificationService(Context ctx) { + this.context = ctx == null ? null : ctx.getApplicationContext(); + this.courseRepository = new CourseRepository(this.context); + try { + List testSegment = HanLP.segment("测试HanLP"); + hanLPInitialized = testSegment != null; + } catch (Throwable e) { + hanLPInitialized = false; + } + } + + public Result classifyWithSegmentation(String text) { + Result out = new Result(); + if (text == null || text.trim().isEmpty()) return out; + Set noteTokens = extractKeywords(text); + List courses = courseRepository.getAllCourses(); + double bestScore = 0.0; + Course bestCourse = null; + Chapter bestChapter = null; + java.util.List all = new java.util.ArrayList<>(); + for (Course course : courses) { + List chapters = courseRepository.getChaptersBySubjectId(course.getId()); + for (Chapter ch : chapters) { + double score = calculateMatchScore(noteTokens, course, ch); + if (score > 0) { + if (score > bestScore) { + bestScore = score; + bestCourse = course; + bestChapter = ch; + } + Candidate cand = new Candidate(); + cand.chapterId = ch.getId(); + cand.subjectId = course.getId(); + cand.title = safe(ch.getName()); + cand.score = score; + all.add(cand); + } + } + } + if (bestCourse != null && bestChapter != null) { + out.category = safe(courseName(bestCourse)); + out.chapter = safe(bestChapter.getName()); + out.bestId = String.valueOf(bestChapter.getId()); + out.bestTitle = out.chapter; + out.confidence = Math.max(0.0, Math.min(1.0, bestScore)); + } + if (!all.isEmpty()) { + java.util.Collections.sort(all, (a,b) -> Double.compare(b.score, a.score)); + int k = Math.min(3, all.size()); + for (int i = 0; i < k; i++) out.topCandidates.add(all.get(i)); + } + return out; + } + + private String courseName(Course c) { + String n = c.getName(); + if (TextUtils.isEmpty(n)) n = c.getDescription(); + return n == null ? "" : n; + } + + private String safe(String s) { return s == null ? "" : s; } + + private Set extractKeywords(String text) { + Set tokens = new HashSet<>(); + if (hanLPInitialized) { + try { + List terms = HanLP.segment(text); + for (Term t : terms) { + String w = t.word; + if (w != null && w.length() > 1) tokens.add(w.toLowerCase(Locale.ROOT)); + } + return tokens; + } catch (Throwable ignored) {} + } + String[] parts = text.toLowerCase(Locale.ROOT).split("[^\u4e00-\u9fa5a-zA-Z0-9]+"); + for (String p : parts) if (p.length() > 1) tokens.add(p); + return tokens; + } + + private double calculateMatchScore(Set noteTokens, Course course, Chapter ch) { + double total = 0.0; + List titleTokens = tokenize(ch.getName()); + List keywordTokens = splitComma(ch.getKeywords()); + List aliasTokens = splitComma(ch.getAliases()); + total += overlapScore(noteTokens, keywordTokens) * 0.5; + total += overlapScore(noteTokens, titleTokens) * 0.3; + total += overlapScore(noteTokens, aliasTokens) * 0.2; + if (total < 0.2) { + List courseTokens = tokenize(courseName(course)); + total = Math.max(total, overlapScore(noteTokens, courseTokens) * 0.2); + } + return total; + } + + private List tokenize(String text) { + List out = new ArrayList<>(); + if (text == null || text.isEmpty()) return out; + if (hanLPInitialized) { + try { + List terms = HanLP.segment(text); + for (Term t : terms) { + String w = t.word; + if (w != null && w.length() > 1) out.add(w.toLowerCase(Locale.ROOT)); + } + return out; + } catch (Throwable ignored) {} + } + String[] parts = text.toLowerCase(Locale.ROOT).split("[^\u4e00-\u9fa5a-zA-Z0-9]+"); + for (String p : parts) if (p.length() > 1) out.add(p); + return out; + } + + private List splitComma(String s) { + List out = new ArrayList<>(); + if (s == null || s.trim().isEmpty()) return out; + String[] arr = s.split(","); + for (String x : arr) { + String v = x.trim(); + if (!v.isEmpty()) out.add(v.toLowerCase(Locale.ROOT)); + } + return out; + } + + private double overlapScore(Set noteTokens, List targetTokens) { + if (noteTokens.isEmpty() || targetTokens == null || targetTokens.isEmpty()) return 0.0; + int hit = 0; + for (String t : targetTokens) if (noteTokens.contains(t)) hit++; + double base = (double) hit / (double) Math.max(1, targetTokens.size()); + return Math.max(0.0, Math.min(1.0, base)); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/CommentService.java b/src/main/java/com/example/myapplication/service/CommentService.java new file mode 100644 index 0000000..b227051 --- /dev/null +++ b/src/main/java/com/example/myapplication/service/CommentService.java @@ -0,0 +1,37 @@ +package com.example.myapplication.service; + +import android.content.Context; + +import com.example.myapplication.domain.Comment; + +import java.util.List; + +public class CommentService { + public static List listComments(Context ctx, long noteId) { + com.example.myapplication.repository.CommentRepository repo = new com.example.myapplication.repository.CommentRepository(ctx); + return repo.listByNoteId(noteId); + } + + public static String validateContent(String content) { + if (content == null) return "内容不能为空"; + String t = content.trim(); + if (t.isEmpty()) return "内容不能为空"; + if (t.length() > 500) return "内容过长"; + return null; + } + + public static boolean addComment(Context ctx, String userId, long noteId, String content) { + String err = validateContent(content); + if (err != null) return false; + if (userId == null || userId.trim().isEmpty()) return false; + com.example.myapplication.repository.CommentRepository repo = new com.example.myapplication.repository.CommentRepository(ctx); + long r = repo.add(userId, noteId, content.trim()); + return r != -1; + } + + public static boolean deleteOwn(Context ctx, long id) { + String userId = com.example.myapplication.service.AuthService.getCurrentUser(ctx); + com.example.myapplication.repository.CommentRepository repo = new com.example.myapplication.repository.CommentRepository(ctx); + return repo.delete(id, userId); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/CourseSyncService.java b/src/main/java/com/example/myapplication/service/CourseSyncService.java new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/example/myapplication/service/DataInitializer.java b/src/main/java/com/example/myapplication/service/DataInitializer.java new file mode 100644 index 0000000..06a0fc3 --- /dev/null +++ b/src/main/java/com/example/myapplication/service/DataInitializer.java @@ -0,0 +1,160 @@ +package com.example.myapplication.service; + +import android.content.Context; +import android.util.Log; + +import com.example.myapplication.domain.Chapter; +import com.example.myapplication.domain.Course; +import com.example.myapplication.repository.CourseRepository; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * 数据初始化器 - 负责加载课程大纲数据到数据库 + */ +public class DataInitializer { + private static final String TAG = "DataInitializer"; + private final Context context; + private final CourseRepository courseRepository; + + // 课程大纲文件列表 + private static final String[] COURSE_OUTLINE_FILES = {}; + + public DataInitializer(Context context) { + this.context = context.getApplicationContext(); + this.courseRepository = new CourseRepository(context); + } + + /** + * 初始化所有课程数据 + */ + public void initializeAllCourses() { + Log.i(TAG, "开始初始化课程数据..."); + + try { + courseRepository.openDatabase(); + + // 检查是否已有课程数据 + List existingCourses = courseRepository.getAllCourses(); + if (!existingCourses.isEmpty()) { + Log.i(TAG, "数据库中已有课程数据,跳过初始化"); + return; + } + + // 兼容现有数据库:不再从assets加载,直接返回 + + Log.i(TAG, "课程数据初始化完成"); + + } catch (Exception e) { + Log.e(TAG, "初始化课程数据失败", e); + } finally { + courseRepository.closeDatabase(); + } + } + + /** + * 从assets加载课程数据 + */ + private void loadCourseFromAssets(String fileName) throws Exception { } + + /** + * 解析课程基本信息 + */ + private Course parseCourse(JSONObject jsonObject) throws Exception { + String courseId = jsonObject.optString("id"); + String courseTitle = jsonObject.optString("title"); + + if (courseId.isEmpty() || courseTitle.isEmpty()) { + // 尝试其他字段名 + courseId = jsonObject.optString("courseId"); + courseTitle = jsonObject.optString("courseTitle"); + } + + if (courseId.isEmpty() || courseTitle.isEmpty()) { + return null; + } + + Course course = new Course(); + course.setId(Integer.parseInt(courseId.replaceAll("\\D", ""))); + course.setName(courseTitle); + course.setCode(courseId); + course.setDescription(jsonObject.optString("description", "")); + + return course; + } + + /** + * 解析章节信息 + */ + private List parseChapters(JSONObject jsonObject, int courseId) throws Exception { + List chapters = new ArrayList<>(); + + JSONArray nodesArray = jsonObject.optJSONArray("nodes"); + if (nodesArray == null) { + nodesArray = jsonObject.optJSONArray("children"); + } + + if (nodesArray != null) { + for (int i = 0; i < nodesArray.length(); i++) { + JSONObject node = nodesArray.getJSONObject(i); + parseChapterNode(node, courseId, chapters, 0); + } + } + + return chapters; + } + + /** + * 递归解析章节节点 + */ + private void parseChapterNode(JSONObject node, int courseId, List chapters, int level) throws Exception { + String chapterId = node.optString("id"); + String title = node.optString("title"); + + if (!chapterId.isEmpty() && !title.isEmpty()) { + Chapter chapter = new Chapter(); + chapter.setId(Integer.parseInt(chapterId.replaceAll("\\D", ""))); + chapter.setName(title); + chapter.setCourseId(courseId); + chapter.setOrderIndex(chapters.size() + 1); + + // 设置关键词 + JSONArray keywordsArray = node.optJSONArray("keywords"); + if (keywordsArray != null && keywordsArray.length() > 0) { + StringBuilder keywords = new StringBuilder(); + for (int i = 0; i < keywordsArray.length(); i++) { + if (i > 0) keywords.append(","); + keywords.append(keywordsArray.getString(i)); + } + chapter.setKeywords(keywords.toString()); + } + + // 设置别名 + JSONArray aliasesArray = node.optJSONArray("aliases"); + if (aliasesArray != null && aliasesArray.length() > 0) { + StringBuilder aliases = new StringBuilder(); + for (int i = 0; i < aliasesArray.length(); i++) { + if (i > 0) aliases.append(","); + aliases.append(aliasesArray.getString(i)); + } + chapter.setAliases(aliases.toString()); + } + + chapters.add(chapter); + } + + // 递归处理子节点 + JSONArray childrenArray = node.optJSONArray("children"); + if (childrenArray != null) { + for (int i = 0; i < childrenArray.length(); i++) { + JSONObject childNode = childrenArray.getJSONObject(i); + parseChapterNode(childNode, courseId, chapters, level + 1); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/DownloadService.java b/src/main/java/com/example/myapplication/service/DownloadService.java new file mode 100644 index 0000000..d814213 --- /dev/null +++ b/src/main/java/com/example/myapplication/service/DownloadService.java @@ -0,0 +1,118 @@ +package com.example.myapplication.service; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.OutputStream; + +public class DownloadService { + public static boolean saveToDownloads(Context ctx, String srcPath, String displayName, String mime) { + if (srcPath == null || srcPath.isEmpty()) return false; + File src = new File(srcPath); + if (!src.exists()) return false; + if (displayName == null || displayName.trim().isEmpty()) { + displayName = src.getName(); + } + if (mime == null || mime.isEmpty()) mime = guessMime(displayName); + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ContentResolver resolver = ctx.getContentResolver(); + ContentValues values = new ContentValues(); + values.put(MediaStore.Downloads.DISPLAY_NAME, displayName); + values.put(MediaStore.Downloads.MIME_TYPE, mime); + values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS); + Uri uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values); + if (uri == null) return false; + try (FileInputStream in = new FileInputStream(src); + OutputStream out = resolver.openOutputStream(uri)) { + if (out == null) return false; + byte[] buf = new byte[8192]; + int len; + while ((len = in.read(buf)) != -1) out.write(buf, 0, len); + out.flush(); + } + return true; + } else { + File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + if (dir != null && !dir.exists()) dir.mkdirs(); + File dst = new File(dir, displayName); + try (FileInputStream in = new FileInputStream(src); + FileOutputStream out = new FileOutputStream(dst)) { + byte[] buf = new byte[8192]; + int len; + while ((len = in.read(buf)) != -1) out.write(buf, 0, len); + out.flush(); + } + return true; + } + } catch (Exception e) { + android.util.Log.e("DownloadService", "saveToDownloads failed", e); + return false; + } + } + + public static String guessMime(String name) { + String lower = name.toLowerCase(); + if (lower.endsWith(".pdf")) return "application/pdf"; + if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg"; + if (lower.endsWith(".png")) return "image/png"; + if (lower.endsWith(".txt")) return "text/plain"; + if (lower.endsWith(".doc")) return "application/msword"; + if (lower.endsWith(".docx")) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + return "application/octet-stream"; + } + + public static boolean saveTextToDownloads(Context ctx, String displayName, String mime, String content) { + android.net.Uri uri = saveTextToDownloadsReturnUri(ctx, displayName, mime, content); + return uri != null; + } + + public static android.net.Uri saveTextToDownloadsReturnUri(Context ctx, String displayName, String mime, String content) { + if (displayName == null || displayName.trim().isEmpty()) displayName = System.currentTimeMillis() + ".txt"; + if (mime == null || mime.isEmpty()) mime = "text/plain"; + if (content == null) content = ""; + try { + byte[] data = content.getBytes("UTF-8"); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + android.content.ContentResolver resolver = ctx.getContentResolver(); + android.content.ContentValues values = new android.content.ContentValues(); + values.put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, displayName); + values.put(android.provider.MediaStore.MediaColumns.MIME_TYPE, mime); + values.put(android.provider.MediaStore.MediaColumns.RELATIVE_PATH, android.os.Environment.DIRECTORY_DOWNLOADS); + values.put(android.provider.MediaStore.MediaColumns.IS_PENDING, 1); + android.net.Uri uri = resolver.insert(android.provider.MediaStore.Files.getContentUri("external"), values); + if (uri == null) return null; + try (java.io.OutputStream out = resolver.openOutputStream(uri)) { + if (out == null) return null; + out.write(data); + out.flush(); + } + android.content.ContentValues done = new android.content.ContentValues(); + done.put(android.provider.MediaStore.MediaColumns.IS_PENDING, 0); + resolver.update(uri, done, null, null); + return uri; + } else { + java.io.File dir = android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOWNLOADS); + if (dir != null && !dir.exists()) dir.mkdirs(); + java.io.File dst = new java.io.File(dir, displayName); + try (java.io.FileOutputStream out = new java.io.FileOutputStream(dst)) { + out.write(data); + out.flush(); + } + return android.net.Uri.fromFile(dst); + } + } catch (Exception e) { + android.util.Log.e("DownloadService", "saveTextToDownloadsReturnUri failed", e); + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/EnhancedKeywordClassifier.java b/src/main/java/com/example/myapplication/service/EnhancedKeywordClassifier.java new file mode 100644 index 0000000..8176fe0 --- /dev/null +++ b/src/main/java/com/example/myapplication/service/EnhancedKeywordClassifier.java @@ -0,0 +1,136 @@ +package com.example.myapplication.service; + +import android.content.Context; +import android.util.Log; +import com.hankcs.hanlp.HanLP; +import com.hankcs.hanlp.seg.common.Term; +import com.hankcs.hanlp.dictionary.stopword.CoreStopWordDictionary; +import java.util.ArrayList; +import java.util.List; + +public class EnhancedKeywordClassifier { + private final Context context; + private boolean hanLPInitialized = false; + + public static class Result { + public String bestId = ""; + public String bestTitle = ""; + public double confidence = 0.0; + public boolean isSegmented = false; + public List topCandidates = new ArrayList<>(); + } + + public static class ClassificationResult { + public String category = ""; + public String chapter = ""; + public String bestId = ""; + public String bestTitle = ""; + public double confidence = 0.0; + public boolean isSegmented = false; + public List topCandidates = new ArrayList<>(); + } + + public EnhancedKeywordClassifier(Context ctx) { + this.context = ctx == null ? null : ctx.getApplicationContext(); + try { + List testSegment = HanLP.segment("测试HanLP"); + hanLPInitialized = true; + Log.i("HanLP", "HanLP初始化成功,分词测试:" + testSegment.toString()); + } catch (Throwable e) { + Log.w("HanLP", "HanLP初始化失败,将使用基础分类: " + String.valueOf(e)); + hanLPInitialized = false; + } + } + + public EnhancedKeywordClassifier() { + this(null); + } + + public ClassificationResult classifyWithSegmentation(String text) { + Result base = performBasicClassification(text); + ClassificationResult resultOut = new ClassificationResult(); + resultOut.bestId = base.bestId; + resultOut.bestTitle = base.bestTitle; + resultOut.category = base.bestTitle; + resultOut.chapter = base.bestTitle; + resultOut.confidence = base.confidence; + resultOut.isSegmented = false; + resultOut.topCandidates = base.topCandidates; + if (text == null || text.trim().isEmpty()) { + resultOut.bestTitle = "默认分类"; + resultOut.category = resultOut.bestTitle; + resultOut.chapter = resultOut.bestTitle; + resultOut.confidence = 0.0; + return resultOut; + } + if (hanLPInitialized && base.confidence < 0.7) { + try { + List terms = HanLP.segment(text); + StringBuilder segmentedText = new StringBuilder(); + StringBuilder keywordsText = new StringBuilder(); + for (Term term : terms) { + String word = term.word; + String nature = term.nature != null ? term.nature.toString() : ""; + if (word.length() > 1 && !CoreStopWordDictionary.contains(word)) { + segmentedText.append(word).append(" "); + if (nature.startsWith("n") || nature.startsWith("v") || nature.startsWith("a")) { + keywordsText.append(word).append(" "); + } + } + } + String enhancedText = segmentedText.toString() + " " + keywordsText.toString(); + Result segmentedBase = performBasicClassification(enhancedText); + ClassificationResult segmentedOut = new ClassificationResult(); + segmentedOut.bestId = segmentedBase.bestId; + segmentedOut.bestTitle = segmentedBase.bestTitle; + segmentedOut.category = segmentedBase.bestTitle; + segmentedOut.chapter = segmentedBase.bestTitle; + segmentedOut.confidence = segmentedBase.confidence; + segmentedOut.isSegmented = true; + segmentedOut.topCandidates = segmentedBase.topCandidates; + if (segmentedOut.confidence > resultOut.confidence) { + segmentedOut.confidence = Math.min(0.95, segmentedOut.confidence * 1.2); + return segmentedOut; + } + } catch (Throwable e) { + Log.w("HanLP", "HanLP分词失败,使用基础分类: " + String.valueOf(e)); + } + } + return resultOut; + } + + private Result performBasicClassification(String text) { + Result result = new Result(); + if (text == null) return result; + String lowerText = text.toLowerCase(); + if (lowerText.contains("程序") || lowerText.contains("java") || lowerText.contains("方法") || lowerText.contains("类")) { + result.bestTitle = "程序设计"; + result.confidence = 0.8; + } else if (lowerText.contains("数据结构") || lowerText.contains("数组") || lowerText.contains("链表") || lowerText.contains("栈") || lowerText.contains("队列")) { + result.bestTitle = "数据结构"; + result.confidence = 0.75; + } else if (lowerText.contains("高等数学") || lowerText.contains("微积分") || lowerText.contains("导数") || lowerText.contains("积分") || lowerText.contains("极限")) { + result.bestTitle = "高等数学"; + result.confidence = 0.7; + } else if (lowerText.contains("人工智能") || lowerText.contains("ai") || lowerText.contains("机器学习") || lowerText.contains("深度学习")) { + result.bestTitle = "人工智能"; + result.confidence = 0.8; + } else if (lowerText.contains("物联网") || lowerText.contains("iot") || lowerText.contains("传感器") || lowerText.contains("rfid")) { + result.bestTitle = "物联网"; + result.confidence = 0.75; + } else if (lowerText.contains("英语") || lowerText.contains("english")) { + result.bestTitle = "大学英语"; + result.confidence = 0.6; + } else if (lowerText.contains("数据库") || lowerText.contains("sql") || lowerText.contains("关系模型")) { + result.bestTitle = "数据库系统"; + result.confidence = 0.75; + } else if (lowerText.contains("机器人") || lowerText.contains("robot") || lowerText.contains("robotics")) { + result.bestTitle = "第1章 机器人学概述"; + result.confidence = 0.7; + } else { + result.bestTitle = "程序设计"; + result.confidence = 0.5; + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/LikeService.java b/src/main/java/com/example/myapplication/service/LikeService.java new file mode 100644 index 0000000..cad7e75 --- /dev/null +++ b/src/main/java/com/example/myapplication/service/LikeService.java @@ -0,0 +1,28 @@ +package com.example.myapplication.service; + +public class LikeService { + public static int increment(int current) { + return current + 1; + } + + // 新增:使用点赞仓库 + public static boolean addLike(android.content.Context ctx, String userId, long noteId) { + com.example.myapplication.repository.LikeRepository repo = new com.example.myapplication.repository.LikeRepository(ctx); + return repo.addLike(userId, noteId); + } + + public static boolean removeLike(android.content.Context ctx, String userId, long noteId) { + com.example.myapplication.repository.LikeRepository repo = new com.example.myapplication.repository.LikeRepository(ctx); + return repo.removeLike(userId, noteId); + } + + public static boolean hasLiked(android.content.Context ctx, String userId, long noteId) { + com.example.myapplication.repository.LikeRepository repo = new com.example.myapplication.repository.LikeRepository(ctx); + return repo.checkUserLikeStatus(userId, noteId); + } + + public static int likeCount(android.content.Context ctx, long noteId) { + com.example.myapplication.repository.LikeRepository repo = new com.example.myapplication.repository.LikeRepository(ctx); + return repo.getLikeCount(noteId); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/NoteService.java b/src/main/java/com/example/myapplication/service/NoteService.java new file mode 100644 index 0000000..682e1de --- /dev/null +++ b/src/main/java/com/example/myapplication/service/NoteService.java @@ -0,0 +1,20 @@ +package com.example.myapplication.service; + +import android.content.Context; +import android.content.SharedPreferences; + +public class NoteService { + public static void setCurrentNoteId(Context ctx, int id) { + SharedPreferences prefs = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + prefs.edit().putInt("currentNoteId", id).apply(); + } + + public static int getCurrentNoteId(Context ctx) { + SharedPreferences prefs = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + return prefs.getInt("currentNoteId", -1); + } + + public static int incrementLikeCount(int current) { + return current + 1; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/OCRService.java b/src/main/java/com/example/myapplication/service/OCRService.java new file mode 100644 index 0000000..d8fb572 --- /dev/null +++ b/src/main/java/com/example/myapplication/service/OCRService.java @@ -0,0 +1,313 @@ +package com.example.myapplication.service; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; +import android.util.Log; +import com.google.mlkit.vision.common.InputImage; +import com.google.mlkit.vision.text.TextRecognition; +import com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions; +// 拉丁识别器(如依赖不可用,仍保留中文识别路径) +// import com.google.mlkit.vision.text.latin.TextRecognizerOptions; + +import java.io.File; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; +import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.text.PDFTextStripper; + +public class OCRService { + public interface Callback { + void onSuccess(String text); + void onFailure(Exception e); + } + + public static void recognize(Context ctx, InputImage image, Callback cb) { + com.google.mlkit.vision.text.TextRecognizer recognizer = + TextRecognition.getClient(new ChineseTextRecognizerOptions.Builder().build()); + recognizer.process(image) + .addOnSuccessListener(visionText -> { + if (cb != null) cb.onSuccess(visionText.getText()); + }) + .addOnFailureListener(e -> { + if (cb != null) cb.onFailure(e); + }); + } + + /** + * 从文件路径提取文本的便捷方法 + * @param filePath 文件路径 + * @return 提取的文本,如果失败返回空字符串 + */ + public static String extractTextFromFile(Context ctx, String filePath) { + if (filePath == null || filePath.isEmpty()) { + return ""; + } + + try { + File file = new File(filePath); + if (!file.exists()) { + Log.e("OCRService", "文件不存在: " + filePath); + return ""; + } + + // 根据文件扩展名判断处理方式 + String extension = getFileExtension(filePath); + if (isImageFile(extension)) { + // 首次:标准预处理 + String txt = extractTextFromImage(ctx, filePath, false); + if (txt != null && !txt.trim().isEmpty()) return txt; + // 兜底:高对比度预处理重试一次 + return extractTextFromImage(ctx, filePath, true); + } else if (isPdfFile(extension)) { + return extractTextFromPdf(ctx, filePath); + } else { + Log.w("OCRService", "不支持的文件类型: " + extension); + return ""; + } + } catch (Exception e) { + Log.e("OCRService", "处理文件失败: " + filePath, e); + return ""; + } + } + + private static String extractTextFromImage(Context ctx, String imagePath, boolean highContrast) { + try { + Bitmap bitmap = loadAndPreprocessImage(imagePath, 2500, highContrast); + if (bitmap == null) { + Log.e("OCRService", "无法解码图片: " + imagePath); + return ""; + } + Bitmap bin = adaptiveBinarize(bitmap); + Bitmap cropped = cropContent(bin); + java.util.List lines = segmentLines(cropped); + StringBuilder sb = new StringBuilder(); + for (Bitmap ln : lines) { + String t = com.example.myapplication.service.TesseractService.recognizeBitmap(ctx, ln, "chi_sim", 7, 1); + if (t != null && !t.trim().isEmpty()) { + if (sb.length() > 0) sb.append('\n'); + sb.append(t.trim()); + } + ln.recycle(); + } + if (sb.length() > 0) { + if (bitmap != null && !bitmap.isRecycled()) bitmap.recycle(); + if (bin != null && !bin.isRecycled()) bin.recycle(); + if (cropped != null && !cropped.isRecycled()) cropped.recycle(); + return sb.toString(); + } + + InputImage image = InputImage.fromBitmap(bitmap, 0); + com.google.mlkit.vision.text.TextRecognizer recognizer = + TextRecognition.getClient(new ChineseTextRecognizerOptions.Builder().build()); + final String[] result = {""}; + final Exception[] exception = {null}; + CountDownLatch latch = new CountDownLatch(1); + recognizer.process(image) + .addOnSuccessListener(visionText -> { result[0] = visionText.getText(); latch.countDown(); }) + .addOnFailureListener(e -> { exception[0] = e; latch.countDown(); }); + try { latch.await(5, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.e("OCRService", "OCR处理被中断", e); } + if (bitmap != null && !bitmap.isRecycled()) bitmap.recycle(); + if (bin != null && !bin.isRecycled()) bin.recycle(); + if (cropped != null && !cropped.isRecycled()) cropped.recycle(); + if (exception[0] != null) return ""; + return result[0] == null ? "" : result[0]; + } catch (Exception e) { + Log.e("OCRService", "图片OCR处理失败: " + imagePath, e); + return ""; + } + } + + + private static Bitmap loadAndPreprocessImage(String path, int maxDim, boolean highContrast) { + try { + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap src = BitmapFactory.decodeFile(path, opts); + if (src == null) return null; + + int w = src.getWidth(); + int h = src.getHeight(); + int max = Math.max(w, h); + float scale = maxDim > 0 && max > maxDim ? (maxDim * 1f / max) : 1f; + Bitmap scaled = src; + if (scale < 1f) { + int nw = Math.max(1, (int) (w * scale)); + int nh = Math.max(1, (int) (h * scale)); + scaled = Bitmap.createScaledBitmap(src, nw, nh, true); + if (scaled != src) src.recycle(); + } + + if (!highContrast) return scaled; + + Bitmap out = Bitmap.createBitmap(scaled.getWidth(), scaled.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(out); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + ColorMatrix cm = new ColorMatrix(); + float c = 1.35f; // 对比度提升 + cm.set(new float[]{ + c, 0, 0, 0, 0, + 0, c, 0, 0, 0, + 0, 0, c, 0, 0, + 0, 0, 0, 1, 0 + }); + paint.setColorFilter(new ColorMatrixColorFilter(cm)); + canvas.drawBitmap(scaled, 0, 0, paint); + if (scaled != out) scaled.recycle(); + return out; + } catch (Exception e) { + Log.e("OCRService", "预处理失败: " + path, e); + return BitmapFactory.decodeFile(path); + } + } + + private static Bitmap adaptiveBinarize(Bitmap src) { + int w = src.getWidth(); + int h = src.getHeight(); + int[] pix = new int[w * h]; + src.getPixels(pix, 0, w, 0, 0, w, h); + int[] gray = new int[w * h]; + for (int i = 0; i < pix.length; i++) { + int p = pix[i]; + int r = (p >> 16) & 0xFF; + int g = (p >> 8) & 0xFF; + int b = p & 0xFF; + gray[i] = (int)(0.299 * r + 0.587 * g + 0.114 * b); + } + int win = Math.max(15, Math.min(w, 25)); + int hw = win / 2; + long[] sum = new long[w * h]; + for (int y = 0; y < h; y++) { + long row = 0; + for (int x = 0; x < w; x++) { + row += gray[y * w + x]; + sum[y * w + x] = row + (y > 0 ? sum[(y - 1) * w + x] : 0); + } + } + int[] out = new int[w * h]; + int c = 10; + for (int y = 0; y < h; y++) { + int y0 = Math.max(0, y - hw); + int y1 = Math.min(h - 1, y + hw); + for (int x = 0; x < w; x++) { + int x0 = Math.max(0, x - hw); + int x1 = Math.min(w - 1, x + hw); + int area = (x1 - x0 + 1) * (y1 - y0 + 1); + long s = sum[y1 * w + x1] - (x0 > 0 ? sum[y1 * w + (x0 - 1)] : 0) - (y0 > 0 ? sum[(y0 - 1) * w + x1] : 0) + (y0 > 0 && x0 > 0 ? sum[(y0 - 1) * w + (x0 - 1)] : 0); + int mean = (int)(s / area); + int g = gray[y * w + x]; + int v = g < mean - c ? 0 : 255; + out[y * w + x] = 0xFF000000 | (v << 16) | (v << 8) | v; + } + } + Bitmap bin = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + bin.setPixels(out, 0, w, 0, 0, w, h); + return bin; + } + + private static Bitmap cropContent(Bitmap src) { + int w = src.getWidth(); + int h = src.getHeight(); + int[] pix = new int[w * h]; + src.getPixels(pix, 0, w, 0, 0, w, h); + int top = 0, bottom = h - 1, left = 0, right = w - 1; + int thr = Math.max(10, w / 50); + for (int y = 0; y < h; y++) { + int cnt = 0; + for (int x = 0; x < w; x++) { if ((pix[y * w + x] & 0xFF) < 128) cnt++; } + if (cnt > thr) { top = y; break; } + } + for (int y = h - 1; y >= 0; y--) { + int cnt = 0; + for (int x = 0; x < w; x++) { if ((pix[y * w + x] & 0xFF) < 128) cnt++; } + if (cnt > thr) { bottom = y; break; } + } + for (int x = 0; x < w; x++) { + int cnt = 0; + for (int y = 0; y < h; y++) { if ((pix[y * w + x] & 0xFF) < 128) cnt++; } + if (cnt > Math.max(5, h / 80)) { left = x; break; } + } + for (int x = w - 1; x >= 0; x--) { + int cnt = 0; + for (int y = 0; y < h; y++) { if ((pix[y * w + x] & 0xFF) < 128) cnt++; } + if (cnt > Math.max(5, h / 80)) { right = x; break; } + } + int cw = Math.max(1, right - left + 1); + int ch = Math.max(1, bottom - top + 1); + Bitmap out = Bitmap.createBitmap(src, left, top, cw, ch); + return out; + } + + private static java.util.List segmentLines(Bitmap src) { + int w = src.getWidth(); + int h = src.getHeight(); + int[] pix = new int[w * h]; + src.getPixels(pix, 0, w, 0, 0, w, h); + int[] proj = new int[h]; + for (int y = 0; y < h; y++) { + int cnt = 0; + for (int x = 0; x < w; x++) { if ((pix[y * w + x] & 0xFF) < 128) cnt++; } + proj[y] = cnt; + } + java.util.List lines = new java.util.ArrayList<>(); + int minRow = Math.max(8, w / 80); + int y = 0; + while (y < h) { + while (y < h && proj[y] <= minRow) y++; + if (y >= h) break; + int start = y; + while (y < h && proj[y] > minRow) y++; + int end = y - 1; + int lh = Math.max(1, end - start + 1); + Bitmap ln = Bitmap.createBitmap(src, 0, start, w, lh); + lines.add(ln); + } + return lines; + } + + private static String extractTextFromPdf(Context ctx, String filePath) { + try { + if (ctx != null) { + try { PDFBoxResourceLoader.init(ctx.getApplicationContext()); } catch (Exception ignored) {} + } + PDDocument doc = PDDocument.load(new File(filePath)); + try { + PDFTextStripper stripper = new PDFTextStripper(); + try { stripper.setSortByPosition(true); } catch (Exception ignored) {} + String text = stripper.getText(doc); + if (text != null && !text.trim().isEmpty() && text.trim().length() >= 500) return text; + } finally { + doc.close(); + } + } catch (Exception e) { + Log.w("OCRService", "PDF文本直读失败,路径: " + filePath, e); + } + PdfOcrService.Config cfg = new PdfOcrService.Config(); + String ocrText = PdfOcrService.recognizePdf(ctx, filePath, cfg); + return ocrText == null ? "" : ocrText; + } + + private static String getFileExtension(String filePath) { + int lastDot = filePath.lastIndexOf('.'); + if (lastDot > 0 && lastDot < filePath.length() - 1) { + return filePath.substring(lastDot + 1).toLowerCase(); + } + return ""; + } + + private static boolean isImageFile(String extension) { + return extension.equals("jpg") || extension.equals("jpeg") || + extension.equals("png") || extension.equals("bmp") || + extension.equals("webp"); + } + + private static boolean isPdfFile(String extension) { + return extension.equals("pdf"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/PdfOcrService.java b/src/main/java/com/example/myapplication/service/PdfOcrService.java new file mode 100644 index 0000000..2810e0a --- /dev/null +++ b/src/main/java/com/example/myapplication/service/PdfOcrService.java @@ -0,0 +1,170 @@ +package com.example.myapplication.service; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; + +import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; +import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.rendering.PDFRenderer; + +import java.io.File; + +public class PdfOcrService { + private static final String TAG = "PdfOcrService"; + public static class Config { + public int maxPages = -1; + public float dpiPrimary = 192f; + public float dpiFallback = 300f; + public int maxSidePx = 3500; + public String lang = "chi_sim+eng"; + public int psmPrimary = 4; // 单列 + public int psmAlt = 6; // 整页 + } + + public static String recognizePdf(Context ctx, String filePath, Config cfg) { + if (cfg == null) cfg = new Config(); + StringBuilder all = new StringBuilder(); + PDDocument doc = null; + try { + try { PDFBoxResourceLoader.init(ctx.getApplicationContext()); } catch (Exception ignored) {} + doc = PDDocument.load(new File(filePath)); + PDFRenderer renderer = new PDFRenderer(doc); + int pageCount = doc.getNumberOfPages(); + int limit = cfg.maxPages <= 0 ? pageCount : Math.min(pageCount, cfg.maxPages); + for (int i = 0; i < limit; i++) { + Bitmap bmp = null; + try { + bmp = renderer.renderImageWithDPI(i, cfg.dpiPrimary); + // 限制长边 + int w = bmp.getWidth(); + int h = bmp.getHeight(); + int max = Math.max(w, h); + if (max > cfg.maxSidePx) { + float scale = cfg.maxSidePx * 1f / max; + int nw = Math.max(1, (int)(w * scale)); + int nh = Math.max(1, (int)(h * scale)); + bmp = Bitmap.createScaledBitmap(bmp, nw, nh, true); + } + java.util.List cols = segmentColumns(bmp); + String pageText; + if (cols != null && cols.size() > 1) { + StringBuilder sb = new StringBuilder(); + for (Bitmap colBmp : cols) { + String ca = TesseractService.recognizeBitmap(ctx, colBmp, cfg.lang, cfg.psmPrimary, 1); + String cb = TesseractService.recognizeBitmap(ctx, colBmp, cfg.lang, cfg.psmAlt, 1); + String ct = (cb != null && (ca == null || cb.length() > ca.length())) ? cb : ca; + if (ct != null && !ct.trim().isEmpty()) { + if (sb.length() > 0) sb.append('\n'); + sb.append(ct.trim()); + } + try { if (colBmp != null && !colBmp.isRecycled()) colBmp.recycle(); } catch (Exception ignored) {} + } + pageText = sb.toString(); + } else { + String a = TesseractService.recognizeBitmap(ctx, bmp, cfg.lang, cfg.psmPrimary, 1); + String b = TesseractService.recognizeBitmap(ctx, bmp, cfg.lang, cfg.psmAlt, 1); + pageText = (b != null && (a == null || b.length() > a.length())) ? b : a; + } + // 若文本过短,提升DPI重渲染重试 + if (pageText == null || pageText.trim().length() < 200) { + try { if (bmp != null && !bmp.isRecycled()) bmp.recycle(); } catch (Exception ignored) {} + bmp = renderer.renderImageWithDPI(i, cfg.dpiFallback); + int w2 = bmp.getWidth(); int h2 = bmp.getHeight(); int max2 = Math.max(w2, h2); + if (max2 > cfg.maxSidePx) { + float scale2 = cfg.maxSidePx * 1f / max2; + int nw2 = Math.max(1, (int)(w2 * scale2)); + int nh2 = Math.max(1, (int)(h2 * scale2)); + bmp = Bitmap.createScaledBitmap(bmp, nw2, nh2, true); + } + String a2 = TesseractService.recognizeBitmap(ctx, bmp, cfg.lang, cfg.psmPrimary, 1); + String b2 = TesseractService.recognizeBitmap(ctx, bmp, cfg.lang, cfg.psmAlt, 1); + pageText = (b2 != null && (a2 == null || b2.length() > a2.length())) ? b2 : a2; + } + // 若仍短,再做高对比度二值化并用稀疏文本模式捕捉残缺部分 + if (pageText == null || pageText.trim().length() < 200) { + Bitmap hc = toHighContrast(bmp); + String s11 = TesseractService.recognizeBitmap(ctx, hc, cfg.lang, 11, 1); // Sparse text + String s06 = TesseractService.recognizeBitmap(ctx, hc, cfg.lang, cfg.psmAlt, 1); + String best = (s06 != null && (s11 == null || s06.length() > s11.length())) ? s06 : s11; + if (best != null && !best.trim().isEmpty()) { + pageText = (pageText == null ? "" : pageText); + if (!pageText.isEmpty()) pageText += "\n"; + pageText += best.trim(); + } + try { if (hc != null && !hc.isRecycled()) hc.recycle(); } catch (Exception ignored) {} + } + if (pageText != null && !pageText.trim().isEmpty()) { + if (all.length() > 0) all.append('\n'); + all.append(pageText.trim()); + } + } catch (Exception e) { + Log.w(TAG, "渲染或识别第" + (i+1) + "页失败", e); + } finally { + try { if (bmp != null && !bmp.isRecycled()) bmp.recycle(); } catch (Exception ignored) {} + } + } + } catch (Exception e) { + Log.e(TAG, "PDF识别失败: " + filePath, e); + } finally { + try { if (doc != null) doc.close(); } catch (Exception ignored) {} + } + return all.toString(); + } + + private static java.util.List segmentColumns(Bitmap src) { + int w = src.getWidth(); + int h = src.getHeight(); + int[] pix = new int[w * h]; + src.getPixels(pix, 0, w, 0, 0, w, h); + int[] projX = new int[w]; + for (int x = 0; x < w; x++) { + int cnt = 0; + for (int y = 0; y < h; y++) { + int p = pix[y * w + x] & 0xFF; + if (p < 160) cnt++; + } + projX[x] = cnt; + } + java.util.List ranges = new java.util.ArrayList<>(); + int minCol = Math.max(h / 60, 10); + int x = 0; + while (x < w) { + while (x < w && projX[x] <= minCol) x++; + if (x >= w) break; + int start = x; + while (x < w && projX[x] > minCol) x++; + int end = x - 1; + if (end - start + 1 >= w / 10) ranges.add(new int[]{start, end}); + } + if (ranges.size() <= 1) return java.util.Collections.emptyList(); + java.util.List cols = new java.util.ArrayList<>(); + for (int[] r : ranges) { + int cw = Math.max(1, r[1] - r[0] + 1); + Bitmap col = Bitmap.createBitmap(src, r[0], 0, cw, h); + cols.add(col); + } + return cols; + } + + private static Bitmap toHighContrast(Bitmap src) { + try { + Bitmap out = Bitmap.createBitmap(src.getWidth(), src.getHeight(), Bitmap.Config.ARGB_8888); + android.graphics.Canvas canvas = new android.graphics.Canvas(out); + android.graphics.Paint paint = new android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG); + android.graphics.ColorMatrix cm = new android.graphics.ColorMatrix(); + float c = 1.4f; + cm.set(new float[]{ + c, 0, 0, 0, 0, + 0, c, 0, 0, 0, + 0, 0, c, 0, 0, + 0, 0, 0, 1, 0 + }); + paint.setColorFilter(new android.graphics.ColorMatrixColorFilter(cm)); + canvas.drawBitmap(src, 0, 0, paint); + return out; + } catch (Exception e) { + return src; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/RegistrationService.java b/src/main/java/com/example/myapplication/service/RegistrationService.java new file mode 100644 index 0000000..168961f --- /dev/null +++ b/src/main/java/com/example/myapplication/service/RegistrationService.java @@ -0,0 +1,20 @@ +package com.example.myapplication.service; + +public class RegistrationService { + public static String validateRegistration(String username, String password, String confirm) { + if (username == null || password == null || confirm == null) return "请输入完整信息"; + username = username.trim(); + password = password.trim(); + confirm = confirm.trim(); + if (username.length() < 3 || username.length() > 20) { + return "用户名长度应为3-20个字符"; + } + if (password.length() < 6) { + return "密码长度不能少于6位"; + } + if (!password.equals(confirm)) { + return "两次输入的密码不一致"; + } + return null; // null 表示校验通过 + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/SearchService.java b/src/main/java/com/example/myapplication/service/SearchService.java new file mode 100644 index 0000000..f30b592 --- /dev/null +++ b/src/main/java/com/example/myapplication/service/SearchService.java @@ -0,0 +1,16 @@ +package com.example.myapplication.service; + +import android.content.Context; +import android.content.SharedPreferences; + +public class SearchService { + public static void saveKeyword(Context ctx, String keyword) { + SharedPreferences prefs = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + prefs.edit().putString("searchKeyword", keyword).apply(); + } + + public static java.util.List search(Context ctx, String query) { + com.example.myapplication.repository.NoteRepository repo = new com.example.myapplication.repository.NoteRepository(ctx); + return repo.findNotesByKeywords(query, null); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/TesseractService.java b/src/main/java/com/example/myapplication/service/TesseractService.java new file mode 100644 index 0000000..18c8cff --- /dev/null +++ b/src/main/java/com/example/myapplication/service/TesseractService.java @@ -0,0 +1,69 @@ +package com.example.myapplication.service; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; + +public class TesseractService { + private static final String TAG = "TesseractService"; + private static final String TESSDATA_DIR = "tessdata"; + private static final String[] REQUIRED_LANGS = new String[]{"chi_sim.traineddata", "eng.traineddata"}; + + public static boolean ensureTessDataInstalled(Context ctx) { + try { + File tessDir = new File(ctx.getFilesDir(), TESSDATA_DIR); + if (!tessDir.exists()) tessDir.mkdirs(); + // 拷贝 assets/tessdata 下的模型到 files/tessdata + for (String lang : REQUIRED_LANGS) { + File out = new File(tessDir, lang); + if (out.exists() && out.length() > 0) continue; + try (InputStream is = ctx.getAssets().open(TESSDATA_DIR + "/" + lang)) { + try (FileOutputStream fos = new FileOutputStream(out)) { + byte[] buf = new byte[8192]; + int r; + while ((r = is.read(buf)) > 0) fos.write(buf, 0, r); + } + } catch (Exception e) { + Log.w(TAG, "缺少模型文件: " + lang + ",请将其放入 assets/tessdata/", e); + return false; + } + } + return true; + } catch (Exception e) { + Log.e(TAG, "安装tessdata失败", e); + return false; + } + } + + public static String recognizeBitmap(Context ctx, Bitmap bmp, String lang, int psm, int oem) { + if (bmp == null) return ""; + boolean ok = ensureTessDataInstalled(ctx); + if (!ok) return ""; + String dataPath = ctx.getFilesDir().getAbsolutePath(); + com.googlecode.tesseract.android.TessBaseAPI api = new com.googlecode.tesseract.android.TessBaseAPI(); + try { + if (!api.init(dataPath, lang)) { + Log.e(TAG, "Tesseract初始化失败"); + return ""; + } + try { + api.setPageSegMode(psm); + } catch (Throwable ignored) {} + try { + api.setVariable("user_defined_dpi", "300"); + } catch (Throwable ignored) {} + api.setImage(bmp); + String text = api.getUTF8Text(); + return text == null ? "" : text; + } catch (Exception e) { + Log.e(TAG, "Tesseract识别失败", e); + return ""; + } finally { + try { api.end(); } catch (Throwable ignored) {} + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/UploadService.java b/src/main/java/com/example/myapplication/service/UploadService.java new file mode 100644 index 0000000..157ad54 --- /dev/null +++ b/src/main/java/com/example/myapplication/service/UploadService.java @@ -0,0 +1,94 @@ +package com.example.myapplication.service; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.provider.OpenableColumns; +import android.util.Log; + +import com.google.mlkit.vision.common.InputImage; +import com.google.mlkit.vision.text.TextRecognition; +import com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions; + +// Keep only PDFBox and I/O for text extraction +import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.text.PDFTextStripper; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class UploadService { + public interface ProgressListener { + void onProgress(int progress); + void onCompleted(); + } + + public static void simulateUpload(Handler handler, ProgressListener listener) { + for (int i = 1; i <= 10; i++) { + int progress = i * 10; + handler.postDelayed(() -> { + if (listener != null) listener.onProgress(progress); + if (progress == 100 && listener != null) listener.onCompleted(); + }, i * 200); + } + } + + public static String getMimeType(Context ctx, Uri uri) { + ContentResolver cr = ctx.getContentResolver(); + return cr.getType(uri); + } + + public static String getDisplayName(Context ctx, Uri uri) { + String name = null; + try (Cursor cursor = ctx.getContentResolver() + .query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + name = cursor.getString(0); + } + } catch (Exception ignored) {} + return name; + } + + // Removed: OcrCallback interface and any runTextRecognition code. + // OCR must be performed via OCRService.recognize(...) + + public static String extractPdfText(Context ctx, Uri uri) throws IOException { + InputStream inputStream = ctx.getContentResolver().openInputStream(uri); + if (inputStream == null) return ""; + java.io.File tmp = java.io.File.createTempFile("ocr_pdf_", ".pdf", ctx.getCacheDir()); + try (java.io.FileOutputStream fos = new java.io.FileOutputStream(tmp)) { + byte[] buf = new byte[8192]; + int len; + while ((len = inputStream.read(buf)) != -1) fos.write(buf, 0, len); + } finally { inputStream.close(); } + + String full = com.example.myapplication.service.OCRService.extractTextFromFile(ctx, tmp.getAbsolutePath()); + try { tmp.delete(); } catch (Exception ignored) {} + return full == null ? "" : full; + } + + public static String extractPlainText(Context ctx, Uri uri) throws IOException { + InputStream inputStream = ctx.getContentResolver().openInputStream(uri); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + inputStream.close(); + return result.toString("UTF-8"); + } + + public static String guessSubject(String lower, String[] dictMath, String[] dictPhysics, String[] dictChem, String[] dictEnglish, String[] dictCS, String[] dictRobotics) { + for (String k : dictRobotics) { if (lower.contains(k.toLowerCase())) return "机器人学导论"; } + for (String k : dictCS) { if (lower.contains(k.toLowerCase())) return "计算机基础"; } + for (String k : dictMath) { if (lower.contains(k.toLowerCase())) return "数学"; } + for (String k : dictPhysics) { if (lower.contains(k.toLowerCase())) return "物理"; } + for (String k : dictChem) { if (lower.contains(k.toLowerCase())) return "化学"; } + for (String k : dictEnglish) { if (lower.contains(k.toLowerCase())) return "英语"; } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/service/UserService.java b/src/main/java/com/example/myapplication/service/UserService.java new file mode 100644 index 0000000..9990395 --- /dev/null +++ b/src/main/java/com/example/myapplication/service/UserService.java @@ -0,0 +1,16 @@ +package com.example.myapplication.service; + +import android.content.Context; +import android.content.SharedPreferences; + +public class UserService { + public static String getUser(Context ctx) { + SharedPreferences prefs = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + String account = prefs.getString("currentUser", null); + if (account == null) return "张三"; + + com.example.myapplication.repository.UserRepository repo = new com.example.myapplication.repository.UserRepository(ctx); + com.example.myapplication.domain.User u = repo.getUserByAccount(account); + return u != null ? u.getName() : account; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/ui/LoginUI.java b/src/main/java/com/example/myapplication/ui/LoginUI.java new file mode 100644 index 0000000..9bccaa9 --- /dev/null +++ b/src/main/java/com/example/myapplication/ui/LoginUI.java @@ -0,0 +1,44 @@ +package com.example.myapplication.ui; +import com.example.myapplication.R; +import com.example.myapplication.Utils; +import com.example.myapplication.service.AuthService; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; + +public class LoginUI extends AppCompatActivity { + + private SharedPreferences prefs; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + prefs = getSharedPreferences("app_prefs", MODE_PRIVATE); + + EditText usernameEt = findViewById(R.id.usernameInput); + EditText passwordEt = findViewById(R.id.passwordInput); + Button loginBtn = findViewById(R.id.loginBtn); + TextView registerLink = findViewById(R.id.registerLink); + + loginBtn.setOnClickListener(v -> { + String username = usernameEt.getText().toString().trim(); + String password = passwordEt.getText().toString().trim(); + if (!AuthService.login(this, username, password)) { + Utils.toast(this, "请输入用户名和密码"); + return; + } + Utils.toast(this, "登录成功!即将跳转到主界面"); + startActivity(new Intent(this, MainUI.class)); + finish(); + }); + + registerLink.setOnClickListener(v -> + startActivity(new Intent(this, RegisterUI.class))); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/ui/MainUI.java b/src/main/java/com/example/myapplication/ui/MainUI.java new file mode 100644 index 0000000..14b2e5d --- /dev/null +++ b/src/main/java/com/example/myapplication/ui/MainUI.java @@ -0,0 +1,302 @@ +package com.example.myapplication.ui; +import com.example.myapplication.R; +import com.example.myapplication.Utils; +import com.example.myapplication.service.AuthService; +import com.example.myapplication.service.NoteService; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.widget.Button; +import android.widget.TextView; +import android.text.TextUtils; +import androidx.appcompat.app.AppCompatActivity; + +public class MainUI extends AppCompatActivity { + private SharedPreferences prefs; + private int recommendedOffset = 0; + private final int recommendedPageSize = 10; + private int recommendedFilterMode = 0; // 0:全部 1:同专业 2:同年级 + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + // 全局初始化数据库连接(可选,但推荐) + try { + com.example.myapplication.repository.DatabaseManager.init(getApplicationContext()); + } catch (Exception e) { + android.util.Log.e("MainUI", "Database init failed", e); + } + prefs = getSharedPreferences("app_prefs", MODE_PRIVATE); + + boolean isLoggedIn = AuthService.isLoggedIn(this); + if (!isLoggedIn) { + Utils.toast(this, "请先登录"); + startActivity(new Intent(this, LoginUI.class)); + finish(); + return; + } + + String currentUser = AuthService.getCurrentUser(this); + TextView welcomeTv = findViewById(R.id.welcomeText); + String majorName = null; + String gradeText = null; + com.example.myapplication.repository.UserRepository ur0 = new com.example.myapplication.repository.UserRepository(this); + com.example.myapplication.domain.User u0 = ur0.getUserByAccount(currentUser); + gradeText = ur0.getGradeText(currentUser); + if (u0 != null && u0.getMajorId() != null) { + com.example.myapplication.repository.MajorRepository mr0 = new com.example.myapplication.repository.MajorRepository(this); + com.example.myapplication.domain.Major m0 = mr0.getById(u0.getMajorId()); + if (m0 != null) majorName = m0.getName(); + } + String extra = ""; + if (majorName != null) extra += "(专业:" + majorName + ")"; + if (gradeText != null && !gradeText.isEmpty()) extra += (extra.isEmpty() ? "(" : " ") + "年级:" + gradeText + (extra.contains("(") ? "" : ")"); + welcomeTv.setText("欢迎," + currentUser + (extra.isEmpty() ? "" : " " + extra)); + + Button logoutBtn = findViewById(R.id.logoutBtn); + Button searchBtn = findViewById(R.id.searchBtn); + Button uploadBtn = findViewById(R.id.uploadBtn); + Button userCenterBtn = findViewById(R.id.userCenterBtn); + + searchBtn.setOnClickListener(v -> startActivity(new Intent(this, SearchUI.class))); + uploadBtn.setOnClickListener(v -> startActivity(new Intent(this, UploadUI.class))); + userCenterBtn.setOnClickListener(v -> startActivity(new Intent(this, UserCenterUI.class))); + + logoutBtn.setOnClickListener(v -> { + AuthService.logout(this); + Utils.toast(this, "已退出登录"); + startActivity(new Intent(this, LoginUI.class)); + finish(); + }); + + + + // 动态渲染 + renderRecommendedNotes(true); + renderLatestNotes(); + } + + @Override + protected void onResume() { + super.onResume(); + renderRecommendedNotes(false); + renderLatestNotes(); + } + + private void renderLatestNotes() { + android.widget.LinearLayout container = findViewById(R.id.latestNotesContainer); + if (container == null) return; + + container.removeAllViews(); + + com.example.myapplication.repository.NoteRepository repo = + new com.example.myapplication.repository.NoteRepository(this); + java.util.List notes = repo.getRecentNotes(20); + + if (notes == null || notes.isEmpty()) { + android.widget.TextView empty = new android.widget.TextView(this); + empty.setText("暂无笔记,点击“上传笔记”添加吧~"); + empty.setTextColor(android.graphics.Color.parseColor("#666666")); + container.addView(empty); + return; + } + + for (com.example.myapplication.domain.Note n : notes) { + android.widget.LinearLayout item = new android.widget.LinearLayout(this); + item.setOrientation(android.widget.LinearLayout.VERTICAL); + item.setBackgroundResource(R.drawable.card_background); + int pad = (int) (20 * getResources().getDisplayMetrics().density); + item.setPadding(pad, pad, pad, pad); + + android.widget.LinearLayout.LayoutParams lp = + new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT + ); + lp.topMargin = (int) (12 * getResources().getDisplayMetrics().density); + item.setLayoutParams(lp); + item.setElevation(4f); + + // 标题 + android.widget.TextView title = new android.widget.TextView(this); + title.setText(n.getTitle() != null ? n.getTitle() : "(无标题)"); + title.setTextColor(getResources().getColor(R.color.text_primary)); + title.setTextSize(18); + title.setTypeface(android.graphics.Typeface.DEFAULT_BOLD); + title.setLayoutParams(new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT)); + + // 元信息(上传者、时间、下载数/点赞数占位) + android.widget.TextView meta = new android.widget.TextView(this); + String uploader = n.getUploaderId() != null ? n.getUploaderId() : "未知"; + String created = n.getCreated() != null ? n.getCreated() : ""; + int likes = n.getLikeCount(); + meta.setText("上传者:" + uploader + (TextUtils.isEmpty(created) ? "" : " | " + created) + " | 👍 " + likes); + meta.setTextColor(getResources().getColor(R.color.text_secondary)); + meta.setTextSize(14); + meta.setLayoutParams(new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT)); + + // 描述摘要 + android.widget.TextView desc = new android.widget.TextView(this); + String d = n.getContent(); + if (d == null) d = ""; + if (d.length() > 80) d = d.substring(0, 80) + "..."; + desc.setText(TextUtils.isEmpty(d) ? "暂无内容" : d); + desc.setTextColor(getResources().getColor(R.color.text_secondary)); + desc.setTextSize(15); + desc.setLineSpacing(0, 1.4f); + desc.setLayoutParams(new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT)); + + item.addView(title); + item.addView(meta); + item.addView(desc); + + item.setOnClickListener(v -> openNote((int) n.getId())); + container.addView(item); + } + } + + private void openNote(int id) { + NoteService.setCurrentNoteId(this, id); + Intent intent = new Intent(this, NoteDetailUI.class); + intent.putExtra("noteId", id); + startActivity(intent); + } + + private void renderRecommendedNotes(boolean reset) { + android.widget.LinearLayout container = findViewById(R.id.recommendedNotesContainer); + android.widget.Button refreshBtn = findViewById(R.id.recommendedRefreshBtn); + if (container == null) return; + if (refreshBtn != null) refreshBtn.setOnClickListener(v -> renderRecommendedNotes(true)); + + if (reset) { + recommendedOffset = 0; + container.removeAllViews(); + } + + String currentUserId = AuthService.getCurrentUser(this); + com.example.myapplication.repository.UserRepository ur = new com.example.myapplication.repository.UserRepository(this); + com.example.myapplication.domain.User u = ur.getUserByAccount(currentUserId); + Long majorId2 = u != null ? u.getMajorId() : null; + String gradeText2 = ur.getGradeText(currentUserId); + String majorName2 = null; + if (majorId2 != null) { + com.example.myapplication.repository.MajorRepository mr2 = new com.example.myapplication.repository.MajorRepository(this); + com.example.myapplication.domain.Major m2 = mr2.getById(majorId2); + if (m2 != null) majorName2 = m2.getName(); + } + + if (reset) { + com.example.myapplication.repository.NoteRepository.clearRecommendedCacheFor(majorId2, gradeText2); + } + com.example.myapplication.repository.NoteRepository repo = new com.example.myapplication.repository.NoteRepository(this); + java.util.List notes = repo.getRecommendedNotes(majorId2, gradeText2, recommendedPageSize, recommendedOffset); + + if (notes == null || notes.isEmpty()) { + if (recommendedOffset == 0) { + android.widget.TextView empty = new android.widget.TextView(this); + empty.setText("暂无推荐笔记,完善个人信息或上传吧~"); + empty.setTextColor(android.graphics.Color.parseColor("#666666")); + container.addView(empty); + } + return; + } + + for (com.example.myapplication.domain.Note n : notes) { + String pubMajorName = null; + String pubGradeText = null; + if (n.getUploaderId() != null) { + com.example.myapplication.repository.UserRepository urp = new com.example.myapplication.repository.UserRepository(this); + com.example.myapplication.domain.User pu = urp.getUserByAccount(n.getUploaderId()); + if (pu != null && pu.getMajorId() != null) { + com.example.myapplication.repository.MajorRepository mrp = new com.example.myapplication.repository.MajorRepository(this); + com.example.myapplication.domain.Major mp = mrp.getById(pu.getMajorId()); + if (mp != null) pubMajorName = mp.getName(); + } + pubGradeText = urp.getGradeText(n.getUploaderId()); + } + String relTag = buildRelevanceTagText(majorName2, gradeText2, pubMajorName, pubGradeText); + if (relTag == null) relTag = ""; + // 客户端过滤 + if (recommendedFilterMode == 1 && (relTag == null || !relTag.contains("同专业"))) continue; + if (recommendedFilterMode == 2 && (relTag == null || !relTag.contains("同年级"))) continue; + android.widget.LinearLayout item = new android.widget.LinearLayout(this); + item.setOrientation(android.widget.LinearLayout.VERTICAL); + item.setBackgroundResource(R.drawable.card_background); + int pad = (int) (20 * getResources().getDisplayMetrics().density); + item.setPadding(pad, pad, pad, pad); + + android.widget.LinearLayout.LayoutParams lp = + new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT + ); + lp.topMargin = (int) (12 * getResources().getDisplayMetrics().density); + item.setLayoutParams(lp); + item.setElevation(4f); + + // 标题 + android.widget.TextView title = new android.widget.TextView(this); + title.setText(n.getTitle() != null ? n.getTitle() : "(无标题)"); + title.setTextColor(getResources().getColor(R.color.text_primary)); + title.setTextSize(18); + title.setTypeface(android.graphics.Typeface.DEFAULT_BOLD); + title.setLayoutParams(new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT)); + + // 元信息(相关性标签) + android.widget.TextView meta = new android.widget.TextView(this); + + String created = n.getCreated() != null ? n.getCreated() : ""; + int likes2 = n.getLikeCount(); + meta.setText((TextUtils.isEmpty(relTag) ? "" : (relTag + " | ")) + (TextUtils.isEmpty(created) ? "" : (created)) + " | 👍 " + likes2); + meta.setTextColor(getResources().getColor(R.color.text_secondary)); + meta.setTextSize(14); + meta.setLayoutParams(new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT)); + + // 描述摘要 + android.widget.TextView desc = new android.widget.TextView(this); + String d = n.getContent(); + if (d == null) d = ""; + if (d.length() > 80) d = d.substring(0, 80) + "..."; + desc.setText(TextUtils.isEmpty(d) ? "暂无内容" : d); + desc.setTextColor(getResources().getColor(R.color.text_secondary)); + desc.setTextSize(15); + desc.setLineSpacing(0, 1.4f); + desc.setLayoutParams(new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT)); + + item.addView(title); + item.addView(meta); + item.addView(desc); + + item.setOnClickListener(v -> openNote((int) n.getId())); + container.addView(item); + } + + recommendedOffset += notes.size(); + } + + private String buildRelevanceTagText(String userMajor, String userGradeText, String pubMajor, String pubGradeText) { + java.util.List tags = new java.util.ArrayList<>(); + if (userMajor != null && pubMajor != null && userMajor.equals(pubMajor)) tags.add("同专业"); + if (userGradeText != null && pubGradeText != null && userGradeText.equals(pubGradeText)) tags.add("同年级"); + if (tags == null || tags.isEmpty()) return ""; + StringBuilder sb = new StringBuilder(); + for (String t : tags) { + if (sb.length() > 0) sb.append("、"); + sb.append(t); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/ui/NoteDetailUI.java b/src/main/java/com/example/myapplication/ui/NoteDetailUI.java new file mode 100644 index 0000000..90aec22 --- /dev/null +++ b/src/main/java/com/example/myapplication/ui/NoteDetailUI.java @@ -0,0 +1,213 @@ +package com.example.myapplication.ui; +import com.example.myapplication.R; +import com.example.myapplication.Utils; +import com.example.myapplication.service.NoteService; +import android.os.Bundle; +import android.widget.Button; +import android.widget.TextView; +import android.text.TextUtils; +import androidx.appcompat.app.AppCompatActivity; + +public class NoteDetailUI extends androidx.appcompat.app.AppCompatActivity { + private int likeCount = 18; // 示例初始点赞数 + + @Override + protected void onCreate(android.os.Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_note_detail); + + int noteId = getIntent().getIntExtra("noteId", 1); + + TextView titleTv = findViewById(R.id.noteTitle); + TextView metaTv = findViewById(R.id.noteMeta); + TextView bodyTv = findViewById(R.id.noteBody); + android.widget.EditText editTitleEt = findViewById(R.id.editNoteTitle); + android.widget.EditText editBodyEt = findViewById(R.id.editNoteBody); + android.widget.LinearLayout editActions = findViewById(R.id.editActions); + android.widget.Button startEditBtn = findViewById(R.id.startEditBtn); + android.widget.Button saveEditBtn = findViewById(R.id.saveEditBtn); + android.widget.Button deleteNoteBtn = findViewById(R.id.deleteNoteBtn); + TextView likeTv = findViewById(R.id.likeCount); + Button backBtn = findViewById(R.id.backBtn); + Button likeBtn = findViewById(R.id.likeBtn); + Button downloadBtn = findViewById(R.id.downloadBtn); + android.widget.LinearLayout commentList = findViewById(R.id.commentListContainer); + android.widget.EditText commentInput = findViewById(R.id.commentInput); + android.widget.Button commentSubmit = findViewById(R.id.commentSubmitBtn); + + // 读取数据库中的真实笔记 + com.example.myapplication.repository.NoteRepository repo = + new com.example.myapplication.repository.NoteRepository(this); + com.example.myapplication.domain.Note note = repo.getNoteById(noteId); + if (note == null) { + com.example.myapplication.Utils.toast(this, "未找到该笔记"); + finish(); + return; + } + + titleTv.setText(note.getTitle() != null ? note.getTitle() : "(无标题)"); + + String uploader = note.getUploaderId() != null ? note.getUploaderId() : "未知"; + String created = note.getCreated() != null ? note.getCreated() : ""; + String fileSummary = summarizeFile(note.getFilePath()); + metaTv.setText("👤 " + uploader + + (TextUtils.isEmpty(created) ? "" : " 📅 " + created) + + (TextUtils.isEmpty(fileSummary) ? "" : " 💾 " + fileSummary)); + + // 修复:正文统一使用 content,不再重复声明 bodyTv,也不访问 description/ocrText + String content = note.getContent(); + bodyTv.setText(content != null && !content.trim().isEmpty() ? content : "暂无内容"); + + String currentUser = com.example.myapplication.service.AuthService.getCurrentUser(this); + boolean isOwner = note.getUploaderId() != null && note.getUploaderId().equals(currentUser); + editActions.setVisibility(android.view.View.GONE); + + // 数据库读取真实点赞数与用户点赞状态 + int dbLikeCount = note.getLikeCount(); + boolean[] liked = new boolean[] { + com.example.myapplication.service.LikeService.hasLiked(this, currentUser, noteId) + }; + likeTv.setText("👍 " + dbLikeCount); + likeBtn.setText(liked[0] ? "取消点赞" : "点赞"); + + backBtn.setOnClickListener(v -> finish()); + likeBtn.setOnClickListener(v -> { + if (!com.example.myapplication.service.AuthService.isLoggedIn(this)) { + com.example.myapplication.Utils.toast(this, "请先登录"); + return; + } + boolean ok; + if (liked[0]) { + ok = com.example.myapplication.service.LikeService.removeLike(this, currentUser, noteId); + if (ok) liked[0] = false; + } else { + ok = com.example.myapplication.service.LikeService.addLike(this, currentUser, noteId); + if (ok) liked[0] = true; + } + com.example.myapplication.domain.Note refreshed = repo.getNoteById(noteId); + int newCount = refreshed != null ? refreshed.getLikeCount() : 0; + likeTv.setText("👍 " + newCount); + likeBtn.setText(liked[0] ? "取消点赞" : "点赞"); + }); + + downloadBtn.setOnClickListener(v -> { + String title = note.getTitle() != null ? note.getTitle() : "未命名笔记"; + String body = note.getContent() != null ? note.getContent() : ""; + StringBuilder sb = new StringBuilder(); + sb.append(title).append("\n"); + if (created != null && !created.isEmpty()) sb.append("时间: ").append(created).append("\n"); + sb.append("作者: ").append(uploader).append("\n\n"); + sb.append(body); + String filename = sanitizeFileName(title) + ".txt"; + android.net.Uri saved = com.example.myapplication.service.DownloadService.saveTextToDownloadsReturnUri(this, filename, "text/plain", sb.toString()); + if (saved != null) { + com.example.myapplication.Utils.toast(this, "已保存笔记内容到下载"); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + try { + android.content.Intent view = new android.content.Intent(android.content.Intent.ACTION_VIEW); + view.setDataAndType(saved, "text/plain"); + view.addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(view); + } catch (Exception ignored) {} + } + android.content.ContentValues cv = new android.content.ContentValues(); + cv.put("download_count", note.getDownloadCount() + 1); + new com.example.myapplication.repository.NoteRepository(this).updateNote(noteId, cv); + } else { + com.example.myapplication.Utils.toast(this, "保存失败,请检查权限或存储空间"); + } + }); + + java.util.List cs = com.example.myapplication.service.CommentService.listComments(this, noteId); + commentList.removeAllViews(); + if (cs != null && !cs.isEmpty()) { + for (com.example.myapplication.domain.Comment cm : cs) { + android.widget.LinearLayout item = new android.widget.LinearLayout(this); + item.setOrientation(android.widget.LinearLayout.VERTICAL); + android.widget.TextView meta = new android.widget.TextView(this); + meta.setText((cm.getUploaderId() == null ? "匿名" : cm.getUploaderId()) + " " + (cm.getCreatedAt() == null ? "" : cm.getCreatedAt())); + meta.setTextColor(android.graphics.Color.parseColor("#666666")); + android.widget.TextView body = new android.widget.TextView(this); + body.setText(cm.getContent() == null ? "" : cm.getContent()); + body.setTextColor(android.graphics.Color.parseColor("#333333")); + item.addView(meta); + item.addView(body); + item.setPadding(0, 12, 0, 12); + commentList.addView(item); + } + } else { + android.widget.TextView empty = new android.widget.TextView(this); + empty.setText("暂无评论,来说两句吧~"); + empty.setTextColor(android.graphics.Color.parseColor("#999999")); + commentList.addView(empty); + } + + if (commentSubmit != null) { + commentSubmit.setOnClickListener(v -> { + if (!com.example.myapplication.service.AuthService.isLoggedIn(this)) { + com.example.myapplication.Utils.toast(this, "请先登录"); + return; + } + String userId2 = com.example.myapplication.service.AuthService.getCurrentUser(this); + String txt = commentInput != null ? commentInput.getText().toString() : ""; + boolean ok = com.example.myapplication.service.CommentService.addComment(this, userId2, noteId, txt); + if (!ok) { + com.example.myapplication.Utils.toast(this, "评论失败,检查内容长度"); + return; + } + com.example.myapplication.Utils.toast(this, "发布成功"); + if (commentInput != null) commentInput.setText(""); + java.util.List cs2 = com.example.myapplication.service.CommentService.listComments(this, noteId); + commentList.removeAllViews(); + if (cs2 != null && !cs2.isEmpty()) { + for (com.example.myapplication.domain.Comment cm : cs2) { + android.widget.LinearLayout item = new android.widget.LinearLayout(this); + item.setOrientation(android.widget.LinearLayout.VERTICAL); + android.widget.TextView meta = new android.widget.TextView(this); + meta.setText((cm.getUploaderId() == null ? "匿名" : cm.getUploaderId()) + " " + (cm.getCreatedAt() == null ? "" : cm.getCreatedAt())); + meta.setTextColor(android.graphics.Color.parseColor("#666666")); + android.widget.TextView body = new android.widget.TextView(this); + body.setText(cm.getContent() == null ? "" : cm.getContent()); + body.setTextColor(android.graphics.Color.parseColor("#333333")); + item.addView(meta); + item.addView(body); + item.setPadding(0, 12, 0, 12); + commentList.addView(item); + } + } + }); + } + } + + // 简单的文件信息摘要:扩展名 + 大小(若可读) + private String summarizeFile(String path) { + if (path == null || path.isEmpty()) return ""; + String ext = ""; + int dot = path.lastIndexOf('.'); + if (dot >= 0 && dot < path.length() - 1) { + ext = path.substring(dot + 1).toUpperCase(); + } + java.io.File f = new java.io.File(path); + String size = ""; + if (f.exists()) { + long bytes = f.length(); + if (bytes > 1024 * 1024) { + size = String.format(java.util.Locale.getDefault(), "%.1fMB", bytes / 1024f / 1024f); + } else if (bytes > 1024) { + size = String.format(java.util.Locale.getDefault(), "%.0fKB", bytes / 1024f); + } else { + size = bytes + "B"; + } + } + if (!ext.isEmpty() && !size.isEmpty()) return ext + ", " + size; + if (!ext.isEmpty()) return ext; + return size; + } + + private String sanitizeFileName(String name) { + String n = name == null ? "note" : name.trim(); + n = n.replaceAll("[\\/:*?\"<>|]", "_"); + if (n.isEmpty()) n = "note"; + return n; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/ui/NoteEditUI.java b/src/main/java/com/example/myapplication/ui/NoteEditUI.java new file mode 100644 index 0000000..c8bb998 --- /dev/null +++ b/src/main/java/com/example/myapplication/ui/NoteEditUI.java @@ -0,0 +1,78 @@ +package com.example.myapplication.ui; + +import androidx.appcompat.app.AppCompatActivity; + +public class NoteEditUI extends AppCompatActivity { + private long noteId; + + @Override + protected void onCreate(android.os.Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(com.example.myapplication.R.layout.activity_note_edit); + + noteId = getIntent().getLongExtra("noteId", -1); + if (noteId <= 0) { + int nid = com.example.myapplication.service.NoteService.getCurrentNoteId(this); + noteId = nid; + } + android.widget.EditText titleEt = findViewById(com.example.myapplication.R.id.editNoteTitle); + android.widget.EditText contentEt = findViewById(com.example.myapplication.R.id.editNoteContent); + android.widget.Button saveBtn = findViewById(com.example.myapplication.R.id.saveNoteBtn); + android.widget.Button cancelBtn = findViewById(com.example.myapplication.R.id.cancelEditBtn); + android.widget.Button deleteBtn = findViewById(com.example.myapplication.R.id.deleteNoteBtn); + + if (noteId > 0) { + com.example.myapplication.repository.NoteRepository repo = new com.example.myapplication.repository.NoteRepository(this); + com.example.myapplication.domain.Note note = repo.getNoteById(noteId); + if (note != null) { + if (titleEt != null) titleEt.setText(note.getTitle() != null ? note.getTitle() : ""); + if (contentEt != null) contentEt.setText(note.getContent() != null ? note.getContent() : ""); + } + } + + saveBtn.setOnClickListener(v -> { + String title = titleEt.getText().toString().trim(); + String content = contentEt.getText().toString().trim(); + if (title.isEmpty()) { + com.example.myapplication.Utils.toast(this, "标题不能为空"); + return; + } + // 仅允许上传者编辑 + com.example.myapplication.repository.NoteRepository repo0 = new com.example.myapplication.repository.NoteRepository(this); + com.example.myapplication.domain.Note n0 = repo0.getNoteById(noteId); + String currentUser = com.example.myapplication.service.AuthService.getCurrentUser(this); + if (n0 != null && n0.getUploaderId() != null && !n0.getUploaderId().equals(currentUser)) { + com.example.myapplication.Utils.toast(this, "仅作者可编辑"); + return; + } + android.content.ContentValues cv = new android.content.ContentValues(); + cv.put("title", title); + cv.put("content", content); + boolean ok = new com.example.myapplication.repository.NoteRepository(this).updateNote(noteId, cv); + com.example.myapplication.Utils.toast(this, ok ? "保存成功" : "保存失败"); + if (ok) finish(); + }); + + cancelBtn.setOnClickListener(v -> finish()); + + deleteBtn.setOnClickListener(v -> { + android.app.AlertDialog.Builder b = new android.app.AlertDialog.Builder(this); + b.setTitle("删除笔记"); + b.setMessage("确定删除这条笔记吗?"); + b.setPositiveButton("删除", (d, which) -> { + com.example.myapplication.repository.NoteRepository repo0 = new com.example.myapplication.repository.NoteRepository(this); + com.example.myapplication.domain.Note n0 = repo0.getNoteById(noteId); + String currentUser = com.example.myapplication.service.AuthService.getCurrentUser(this); + if (n0 != null && n0.getUploaderId() != null && !n0.getUploaderId().equals(currentUser)) { + com.example.myapplication.Utils.toast(this, "仅作者可删除"); + return; + } + boolean ok = new com.example.myapplication.repository.NoteRepository(this).deleteNote(noteId); + com.example.myapplication.Utils.toast(this, ok ? "已删除" : "删除失败"); + if (ok) finish(); + }); + b.setNegativeButton("取消", (d, which) -> {}); + b.show(); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/ui/NoteUploadActivity.java b/src/main/java/com/example/myapplication/ui/NoteUploadActivity.java new file mode 100644 index 0000000..74c458b --- /dev/null +++ b/src/main/java/com/example/myapplication/ui/NoteUploadActivity.java @@ -0,0 +1,477 @@ +package com.example.myapplication; + +import android.app.Activity; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.OpenableColumns; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.card.MaterialCardView; +import com.google.android.material.textfield.TextInputEditText; + +import com.example.myapplication.domain.Chapter; +// import com.example.myapplication.domain.ClassificationResult; // 使用ClassificationService.Result替代 +import com.example.myapplication.domain.Course; +import com.example.myapplication.domain.Note; +import com.example.myapplication.repository.CourseRepository; +import com.example.myapplication.repository.NoteRepository; +import com.example.myapplication.service.ClassificationService; +import com.example.myapplication.service.OCRService; +import com.example.myapplication.service.ClassificationService.Result; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class NoteUploadActivity extends AppCompatActivity { + private static final String TAG = "NoteUploadActivity"; + private static final int PICK_FILE_REQUEST = 1; + + // UI组件 + private Button btnSelectFile, btnUpload, btnCancel, btnAcceptClassification, btnRejectClassification; + private TextInputEditText etNoteTitle, etNoteDescription; + private TextView tvSelectedFile, tvClassificationResult, tvClassificationConfidence; + private MaterialCardView cardClassification, cardManualSelection; + private Spinner spinnerCourse, spinnerChapter; + private ProgressBar progressBar; + + // 数据存储 + private CourseRepository courseRepository; + private NoteRepository noteRepository; + private ClassificationService classificationService; + // 状态变量 + private Uri selectedFileUri; + private String savedFilePath; + private ClassificationService.Result currentClassification; + private List availableCourses; + private List availableChapters; + + // Activity结果启动器 + private ActivityResultLauncher filePickerLauncher; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_note_upload); + + initViews(); + initRepositories(); + setupListeners(); + setupFilePicker(); + loadAvailableCourses(); + } + + private void initViews() { + btnSelectFile = findViewById(R.id.btn_select_file); + btnUpload = findViewById(R.id.btn_upload); + btnCancel = findViewById(R.id.btn_cancel); + btnAcceptClassification = findViewById(R.id.btn_accept_classification); + btnRejectClassification = findViewById(R.id.btn_reject_classification); + + etNoteTitle = findViewById(R.id.et_note_title); + etNoteDescription = findViewById(R.id.et_note_description); + + tvSelectedFile = findViewById(R.id.tv_selected_file); + tvClassificationResult = findViewById(R.id.tv_classification_result); + tvClassificationConfidence = findViewById(R.id.tv_classification_confidence); + + cardClassification = findViewById(R.id.card_classification); + cardManualSelection = findViewById(R.id.card_manual_selection); + + spinnerCourse = findViewById(R.id.spinner_course); + spinnerChapter = findViewById(R.id.spinner_chapter); + + progressBar = findViewById(R.id.progress_bar); + } + + private void initRepositories() { + courseRepository = new CourseRepository(this); + noteRepository = new NoteRepository(this); + classificationService = new ClassificationService(this); + // OCRService是静态工具类,不需要实例化 + } + + private void setupListeners() { + btnSelectFile.setOnClickListener(v -> openFilePicker()); + btnUpload.setOnClickListener(v -> uploadNote()); + btnCancel.setOnClickListener(v -> finish()); + btnAcceptClassification.setOnClickListener(v -> acceptClassification()); + btnRejectClassification.setOnClickListener(v -> showManualSelection()); + + spinnerCourse.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (position >= 0 && position < availableCourses.size()) { + Course selectedCourse = availableCourses.get(position); + loadChaptersForCourse(String.valueOf(selectedCourse.getId())); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + // Do nothing + } + }); + } + + private void setupFilePicker() { + filePickerLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + Uri uri = result.getData().getData(); + if (uri != null) { + handleFileSelection(uri); + } + } + } + ); + } + + private void openFilePicker() { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("*/*"); + String[] mimeTypes = {"image/*", "application/pdf"}; + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); + intent.addCategory(Intent.CATEGORY_OPENABLE); + + Intent chooser = Intent.createChooser(intent, "选择文件"); + filePickerLauncher.launch(chooser); + } + + private void handleFileSelection(Uri uri) { + selectedFileUri = uri; + String fileName = getFileName(uri); + tvSelectedFile.setText("已选择: " + fileName); + + // 保存文件到应用目录 + savedFilePath = saveFileToAppDirectory(uri); + if (savedFilePath == null) { + Toast.makeText(this, "文件保存失败", Toast.LENGTH_SHORT).show(); + } + } + + private String getFileName(Uri uri) { + String result = null; + if (uri.getScheme().equals("content")) { + Cursor cursor = getContentResolver().query(uri, null, null, null, null); + try { + if (cursor != null && cursor.moveToFirst()) { + int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (nameIndex >= 0) { + result = cursor.getString(nameIndex); + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + if (result == null) { + result = uri.getPath(); + int cut = result.lastIndexOf('/'); + if (cut != -1) { + result = result.substring(cut + 1); + } + } + return result; + } + + private String saveFileToAppDirectory(Uri uri) { + try { + InputStream inputStream = getContentResolver().openInputStream(uri); + if (inputStream == null) return null; + + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); + String fileName = "note_" + timeStamp + getFileExtension(uri); + File outputFile = new File(getFilesDir(), fileName); + + OutputStream outputStream = new FileOutputStream(outputFile); + byte[] buffer = new byte[4096]; + int length; + while ((length = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, length); + } + + inputStream.close(); + outputStream.close(); + + return outputFile.getAbsolutePath(); + } catch (IOException e) { + Log.e(TAG, "保存文件失败", e); + return null; + } + } + + private String getFileExtension(Uri uri) { + String fileName = getFileName(uri); + int lastDotIndex = fileName.lastIndexOf('.'); + return lastDotIndex >= 0 ? fileName.substring(lastDotIndex) : ""; + } + + private void loadAvailableCourses() { + String account = com.example.myapplication.service.AuthService.getCurrentUser(this); + com.example.myapplication.repository.UserRepository ur = new com.example.myapplication.repository.UserRepository(this); + com.example.myapplication.domain.User u = ur.getUserByAccount(account); + Long majorId = u != null ? u.getMajorId() : null; + String gradeText = ur.getGradeText(account); + availableCourses = courseRepository.getCoursesByMajorAndGrade(majorId, gradeText); + if (availableCourses == null || availableCourses.isEmpty()) { + availableCourses = courseRepository.getAllCourses(); + } + if (availableCourses == null || availableCourses.isEmpty()) { + Toast.makeText(this, "暂无课程数据,请稍后重试", Toast.LENGTH_LONG).show(); + } else { + setupCourseSpinner(); + } + } + + private void setupCourseSpinner() { + List courseNames = new ArrayList<>(); + for (Course course : availableCourses) { + courseNames.add(course.getName()); + } + + ArrayAdapter adapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_item, courseNames); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerCourse.setAdapter(adapter); + } + + private void loadChaptersForCourse(String courseId) { + long sid = 0L; + try { sid = Long.parseLong(courseId); } catch (Exception ignored) {} + availableChapters = courseRepository.getChaptersBySubjectId(sid); + List chapterTitles = new ArrayList<>(); + for (Chapter ch : availableChapters) chapterTitles.add(ch.getName()); + if (chapterTitles.isEmpty()) chapterTitles.add("暂无章节"); + ArrayAdapter adapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_item, chapterTitles); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerChapter.setAdapter(adapter); + } + + private void uploadNote() { + String title = etNoteTitle.getText().toString().trim(); + String description = etNoteDescription.getText().toString().trim(); + + if (title.isEmpty()) { + Toast.makeText(this, "请输入笔记标题", Toast.LENGTH_SHORT).show(); + return; + } + + if (selectedFileUri == null) { + Toast.makeText(this, "请选择文件", Toast.LENGTH_SHORT).show(); + return; + } + + showProgress(true); + + // 在后台线程中处理 + new Thread(() -> { + try { + // 1. 执行OCR提取文本 + String ocrText = performOCR(); + + // 2. 创建笔记对象 + Note note = createNote(title, description, ocrText); + + // 3. 进行自动分类 + if (note.getOcrText() != null && !note.getOcrText().isEmpty()) { + performClassification(note); + } else { + runOnUiThread(() -> { + showProgress(false); + showManualSelection(); + }); + return; + } + + runOnUiThread(() -> { + showProgress(false); + showClassificationResult(); + }); + + } catch (Exception e) { + Log.e(TAG, "处理笔记失败", e); + runOnUiThread(() -> { + showProgress(false); + Toast.makeText(this, "处理失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + }); + } + }).start(); + } + + private String performOCR() { + if (savedFilePath == null) return ""; + try { + String text = com.example.myapplication.service.OCRService.extractTextFromFile(this, savedFilePath); + if (text == null || text.trim().isEmpty()) { + Toast.makeText(this, "识别暂不可用,您可手动填写内容", Toast.LENGTH_SHORT).show(); + return ""; + } + return text; + } catch (Exception e) { + Log.e(TAG, "OCR处理失败", e); + Toast.makeText(this, "识别失败,已保留上传与编辑", Toast.LENGTH_SHORT).show(); + return ""; + } + } + + private Note createNote(String title, String description, String ocrText) { + Note note = new Note(); + note.setTitle(title); + note.setDescription(description); + note.setOcrText(ocrText); + note.setFilePath(savedFilePath); + + long currentTime = System.currentTimeMillis(); + note.setUploadTime(currentTime); + note.setUpdateTime(currentTime); + + return note; + } + + private void performClassification(Note note) { + currentClassification = classificationService.classifyWithSegmentation(note.getOcrText()); + } + + private void showClassificationResult() { + if (currentClassification == null) { + Toast.makeText(this, "分类失败", Toast.LENGTH_SHORT).show(); + return; + } + + if (currentClassification.category.isEmpty()) { + tvClassificationResult.setText("无法自动分类,请手动选择"); + tvClassificationConfidence.setText(""); + showManualSelection(); + } else { + String result = String.format("分类结果: %s - %s", currentClassification.category, currentClassification.chapter); + tvClassificationResult.setText(result); + String confidence = String.format("置信度: %.2f%%", currentClassification.confidence * 100); + tvClassificationConfidence.setText(confidence); + if (currentClassification.confidence >= 0.6) { + preselectByClassification(); + cardClassification.setVisibility(View.VISIBLE); + cardManualSelection.setVisibility(View.GONE); + } else { + cardClassification.setVisibility(View.VISIBLE); + cardManualSelection.setVisibility(View.VISIBLE); + Toast.makeText(this, "置信度较低,请确认或手动选择", Toast.LENGTH_SHORT).show(); + } + } + } + + private void preselectByClassification() { + if (availableCourses != null && !availableCourses.isEmpty()) { + int courseIndex = -1; + for (int i = 0; i < availableCourses.size(); i++) { + if (availableCourses.get(i).getName().equals(currentClassification.category)) { + courseIndex = i; + break; + } + } + if (courseIndex >= 0) { + spinnerCourse.setSelection(courseIndex); + Course selectedCourse = availableCourses.get(courseIndex); + loadChaptersForCourse(String.valueOf(selectedCourse.getId())); + if (availableChapters != null && !availableChapters.isEmpty()) { + int chapterIndex = -1; + for (int j = 0; j < availableChapters.size(); j++) { + if (availableChapters.get(j).getName().equals(currentClassification.chapter)) { + chapterIndex = j; + break; + } + } + if (chapterIndex >= 0) spinnerChapter.setSelection(chapterIndex); + } + } + } + } + + private void acceptClassification() { + if (currentClassification == null || currentClassification.category.isEmpty()) { + Toast.makeText(this, "没有有效的分类结果", Toast.LENGTH_SHORT).show(); + return; + } + // 创建最终笔记并保存(科目、章节均使用数据库选择结果) + Note finalNote = createNote( + etNoteTitle.getText().toString().trim(), + etNoteDescription.getText().toString().trim(), + "自动分类: " + currentClassification.category + " - " + currentClassification.chapter + ); + + int coursePos = spinnerCourse.getSelectedItemPosition(); + if (availableCourses == null || availableCourses.isEmpty() || coursePos < 0 || coursePos >= availableCourses.size()) { + Toast.makeText(this, "请选择科目", Toast.LENGTH_SHORT).show(); + showManualSelection(); + return; + } + Course selectedCourse = availableCourses.get(coursePos); + + int chapterPos = spinnerChapter.getSelectedItemPosition(); + if (availableChapters == null || availableChapters.isEmpty() || chapterPos < 0 || chapterPos >= availableChapters.size()) { + Toast.makeText(this, "请选择章节", Toast.LENGTH_SHORT).show(); + showManualSelection(); + return; + } + Chapter selectedChapter = availableChapters.get(chapterPos); + + finalNote.setCourseId(selectedCourse.getId()); + finalNote.setChapterId(selectedChapter.getId()); + finalNote.setUploaderId(com.example.myapplication.service.AuthService.getCurrentUser(this)); + + long result = noteRepository.insertNote(finalNote); + if (result != -1) { + Toast.makeText(this, "笔记上传成功!", Toast.LENGTH_SHORT).show(); + finish(); + } else { + Toast.makeText(this, "保存笔记失败", Toast.LENGTH_SHORT).show(); + } + } + + private void showManualSelection() { + cardClassification.setVisibility(View.GONE); + cardManualSelection.setVisibility(View.VISIBLE); + } + + private void showProgress(boolean show) { + progressBar.setVisibility(show ? View.VISIBLE : View.GONE); + btnUpload.setEnabled(!show); + btnCancel.setEnabled(!show); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (courseRepository != null) { + courseRepository.close(); + } + if (noteRepository != null) { + noteRepository.close(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/ui/RegisterUI.java b/src/main/java/com/example/myapplication/ui/RegisterUI.java new file mode 100644 index 0000000..8d2e54e --- /dev/null +++ b/src/main/java/com/example/myapplication/ui/RegisterUI.java @@ -0,0 +1,78 @@ +package com.example.myapplication.ui; +import com.example.myapplication.R; +import com.example.myapplication.Utils; +import com.example.myapplication.service.AuthService; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Button; +import android.widget.EditText; +import androidx.appcompat.app.AppCompatActivity; + +public class RegisterUI extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_register); + + EditText usernameEt = findViewById(R.id.usernameInput); + EditText passwordEt = findViewById(R.id.passwordInput); + EditText confirmEt = findViewById(R.id.confirmInput); + android.widget.Spinner gradeSp = findViewById(R.id.gradeSpinner); + android.widget.AutoCompleteTextView majorEt = findViewById(R.id.majorInput); + final long[] selectedMajorId = new long[]{-1}; + majorEt.setThreshold(1); + majorEt.addTextChangedListener(new android.text.TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + selectedMajorId[0] = -1; + String q = s == null ? "" : s.toString(); + com.example.myapplication.repository.MajorRepository mr = new com.example.myapplication.repository.MajorRepository(RegisterUI.this); + java.util.List majors = mr.findByNameLike(q, 10); + java.util.List names = new java.util.ArrayList<>(); + for (com.example.myapplication.domain.Major m : majors) names.add(m.getName()); + android.widget.ArrayAdapter ad = new android.widget.ArrayAdapter<>(RegisterUI.this, android.R.layout.simple_dropdown_item_1line, names); + majorEt.setAdapter(ad); + majorEt.showDropDown(); + } + @Override public void afterTextChanged(android.text.Editable s) {} + }); + majorEt.setOnItemClickListener((parent, view, position, id) -> { + String name = (String) parent.getItemAtPosition(position); + com.example.myapplication.repository.MajorRepository mr = new com.example.myapplication.repository.MajorRepository(RegisterUI.this); + java.util.List majors = mr.findByNameLike(name, 1); + if (majors != null && !majors.isEmpty()) selectedMajorId[0] = majors.get(0).getId(); + }); + Button registerBtn = findViewById(R.id.registerBtn); + + java.util.List grades = java.util.Arrays.asList("大一", "大二", "大三", "大四"); + android.widget.ArrayAdapter adapter = new android.widget.ArrayAdapter<>( + this, android.R.layout.simple_spinner_item, grades); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + gradeSp.setAdapter(adapter); + + registerBtn.setOnClickListener(v -> { + String username = usernameEt.getText().toString().trim(); + String password = passwordEt.getText().toString().trim(); + String confirm = confirmEt.getText().toString().trim(); + + String gradeName = null; + try { gradeName = (String) gradeSp.getSelectedItem(); } catch (Exception ignored) {} + Long majorId = selectedMajorId[0] >= 0 ? selectedMajorId[0] : null; + String err = AuthService.validateRegistration(username, password, confirm, gradeName, majorId); + if (err != null) { + Utils.toast(this, err); + return; + } + + boolean ok = AuthService.register(this, username, password, gradeName, majorId); + if (!ok) { + Utils.toast(this, "注册失败,请稍后再试"); + return; + } + + Utils.toast(this, "注册成功!请登录"); + startActivity(new Intent(this, LoginUI.class)); + finish(); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/ui/SearchUI.java b/src/main/java/com/example/myapplication/ui/SearchUI.java new file mode 100644 index 0000000..14f71c6 --- /dev/null +++ b/src/main/java/com/example/myapplication/ui/SearchUI.java @@ -0,0 +1,72 @@ +package com.example.myapplication.ui; +import com.example.myapplication.R; +import android.os.Bundle; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; + +public class SearchUI extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_search); + + Button backBtn = findViewById(R.id.backBtn); + Button searchBtn = findViewById(R.id.searchBtn); + EditText searchInput = findViewById(R.id.searchInput); + TextView resultsInfo = findViewById(R.id.resultsInfo); + android.widget.LinearLayout resultsContainer = findViewById(R.id.resultsContainer); + + backBtn.setOnClickListener(v -> finish()); + searchBtn.setOnClickListener(v -> { + String q = searchInput.getText() == null ? "" : searchInput.getText().toString().trim(); + java.util.List notes = com.example.myapplication.service.SearchService.search(this, q); + resultsContainer.removeAllViews(); + if (notes == null || notes.isEmpty()) { + resultsInfo.setText("未找到匹配的笔记"); + android.widget.TextView empty = new android.widget.TextView(this); + empty.setText("试试输入章节、科目或专业名称进行搜索"); + empty.setTextColor(android.graphics.Color.parseColor("#666666")); + resultsContainer.addView(empty); + } else { + resultsInfo.setText("共 " + notes.size() + " 条结果"); + int pad = (int) (12 * getResources().getDisplayMetrics().density); + for (com.example.myapplication.domain.Note n : notes) { + android.widget.LinearLayout item = new android.widget.LinearLayout(this); + item.setOrientation(android.widget.LinearLayout.VERTICAL); + item.setBackgroundColor(android.graphics.Color.WHITE); + item.setPadding(pad, pad, pad, pad); + + android.widget.TextView title = new android.widget.TextView(this); + title.setText(n.getTitle() != null ? n.getTitle() : "(无标题)"); + title.setTextColor(android.graphics.Color.parseColor("#333333")); + title.setTypeface(android.graphics.Typeface.DEFAULT_BOLD); + + android.widget.TextView meta = new android.widget.TextView(this); + String created = n.getCreated() == null ? "" : n.getCreated(); + int likes = n.getLikeCount(); + meta.setText((created.isEmpty() ? "📅 未知" : "📅 " + created) + " 👍 " + likes); + meta.setTextColor(android.graphics.Color.parseColor("#666666")); + + android.widget.TextView desc = new android.widget.TextView(this); + String content = n.getContent(); + if (content == null) content = ""; + desc.setText(content.length() > 120 ? content.substring(0, 120) + "..." : content); + desc.setTextColor(android.graphics.Color.parseColor("#888888")); + + item.addView(title); + item.addView(meta); + item.addView(desc); + item.setOnClickListener(v2 -> { + com.example.myapplication.service.NoteService.setCurrentNoteId(this, (int) n.getId()); + android.content.Intent intent = new android.content.Intent(this, com.example.myapplication.ui.NoteDetailUI.class); + intent.putExtra("noteId", (int) n.getId()); + startActivity(intent); + }); + resultsContainer.addView(item); + } + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/myapplication/ui/UploadUI.java b/src/main/java/com/example/myapplication/ui/UploadUI.java new file mode 100644 index 0000000..4859cf9 --- /dev/null +++ b/src/main/java/com/example/myapplication/ui/UploadUI.java @@ -0,0 +1,958 @@ +package com.example.myapplication.ui; + +import com.example.myapplication.R; +import com.example.myapplication.Utils; +import com.example.myapplication.service.UploadService; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.net.Uri; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.ImageView; +import android.content.ContentResolver; +import android.database.Cursor; +import android.provider.OpenableColumns; + +import androidx.appcompat.app.AppCompatActivity; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import java.util.List; +import java.util.ArrayList; + +// 添加自动分类功能所需的 import +import android.util.Log; +import android.content.Context; +import android.content.res.AssetManager; +import java.io.InputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +// Google ML Kit文字识别导入 +import com.google.mlkit.vision.common.InputImage; + +// HanLP开源代码导入 +// 删除以下 UI 中不应存在的导入: +// import com.hankcs.hanlp.HanLP; +// import com.hankcs.hanlp.seg.common.Term; +// import com.hankcs.hanlp.dictionary.stopword.CoreStopWordDictionary; + +// 文档处理相关导入 +import com.tom_roush.pdfbox.android.PDFBoxResourceLoader; +import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.text.PDFTextStripper; + +import com.example.myapplication.service.AuthService; +import com.example.myapplication.repository.NoteRepository; +import java.io.File; +import java.io.OutputStream; +import java.io.FileOutputStream; + +public class UploadUI extends AppCompatActivity { + private ProgressBar progressBar; + private TextView progressText; + private Button submitBtn; + private ImageView previewImage; + // 删除:private TextView ocrResultText; + private TextView fileInfoText; + private TextView categorySuggestions; + private LinearLayout fileListContainer; + + private static final int REQ_OPEN_DOCUMENT = 1002; + + private final List selectedFiles = new ArrayList<>(); + private Uri currentPreviewUri; + private TextView classificationResultText; + private com.example.myapplication.service.ClassificationService classificationService; + + // 聚合所有文件的文本内容 + private final StringBuilder aggregatedText = new StringBuilder(); + private final Set processedFiles = new HashSet<>(); + private Long autoChapterId = null; + private Long autoSubjectId = null; + private java.util.List availableSubjects; + private java.util.List availableChapters; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_upload); + + EditText titleEt = findViewById(R.id.noteTitle); + Spinner subjectSp = findViewById(R.id.subjectSpinner); + EditText contentEt = findViewById(R.id.contentInput); // 替换描述为内容 + EditText tagsEt = findViewById(R.id.tagsInput); + progressBar = findViewById(R.id.progressBar); + progressText = findViewById(R.id.progressText); + submitBtn = findViewById(R.id.submitBtn); + + previewImage = findViewById(R.id.previewImage); + // 删除:ocrResultText = findViewById(R.id.ocrResultText); + fileInfoText = findViewById(R.id.fileInfoText); + fileListContainer = findViewById(R.id.fileListContainer); + + // 初始化自动分类功能 + classificationResultText = findViewById(R.id.classificationResultText); + com.tom_roush.pdfbox.android.PDFBoxResourceLoader.init(getApplicationContext()); + try { + classificationService = new com.example.myapplication.service.ClassificationService(this); + } catch (Exception e) { + android.util.Log.e("UploadUI", "ClassificationService init failed", e); + } + + // 初始化科目下拉(数据库) + try { + com.example.myapplication.repository.CourseRepository cr = new com.example.myapplication.repository.CourseRepository(this); + String account = com.example.myapplication.service.AuthService.getCurrentUser(this); + com.example.myapplication.repository.UserRepository ur = new com.example.myapplication.repository.UserRepository(this); + com.example.myapplication.domain.User u = ur.getUserByAccount(account); + Long majorId = u != null ? u.getMajorId() : null; + String gradeText = ur.getGradeText(account); + availableSubjects = cr.getCoursesByMajorAndGrade(majorId, gradeText); + if (availableSubjects == null || availableSubjects.isEmpty()) availableSubjects = cr.getCoursesByMajor(majorId); + if (availableSubjects == null || availableSubjects.isEmpty()) availableSubjects = cr.getAllCourses(); + java.util.List subjectNames = new java.util.ArrayList<>(); + for (com.example.myapplication.domain.Course c : availableSubjects) subjectNames.add(c.getName()); + android.widget.ArrayAdapter subjAdapter = new android.widget.ArrayAdapter<>( + this, android.R.layout.simple_spinner_item, subjectNames); + subjAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + subjectSp.setAdapter(subjAdapter); + subjectSp.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() { + @Override public void onItemSelected(android.widget.AdapterView parent, android.view.View view, int position, long id) { + if (position >= 0 && position < availableSubjects.size()) { + long sid = availableSubjects.get(position).getId(); + loadChaptersForSubject(sid); + } + } + @Override public void onNothingSelected(android.widget.AdapterView parent) {} + }); + } catch (Exception e) { + android.util.Log.e("UploadUI", "init subjects failed", e); + } + + findViewById(R.id.selectFileBtn).setOnClickListener(v -> { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] { + "image/*", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "text/plain" + }); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + startActivityForResult(intent, REQ_OPEN_DOCUMENT); + }); + + findViewById(R.id.backBtn).setOnClickListener(v -> finish()); + + submitBtn.setOnClickListener(v -> { + String title = titleEt.getText().toString().trim(); + String content = contentEt.getText().toString().trim(); + if (title.isEmpty()) { + com.example.myapplication.Utils.toast(this, "请输入笔记标题"); + return; + } + simulateUpload(title, content); + }); + } + + // 将 ProgressListener 的实现改回本地 Handler 进度循环,避免生成 UploadUI$1.class + private void simulateUpload(String title, String content) { + submitBtn.setEnabled(false); + progressBar.setProgress(0); + progressText.setText("上传中... 0%"); + progressBar.setVisibility(android.view.View.VISIBLE); + progressText.setVisibility(android.view.View.VISIBLE); + + android.os.Handler handler = new android.os.Handler(); + for (int i = 1; i <= 10; i++) { + final int progress = i * 10; + handler.postDelayed(() -> { + progressBar.setProgress(progress); + progressText.setText("上传中... " + progress + "%"); + if (progress == 100) { + persistNoteAsync(title, content); + } + }, i * 200); + } + } + + // 新增:持久化到 SQLite,并复制首个选择的文件到内部存储 + // 确保这是类级方法,而不是嵌套在 onCreate 里 + private void persistNoteAsync(String title, String content) { + new Thread(() -> { + boolean ok = false; + String errMsg = null; + try { + String uploaderId = com.example.myapplication.service.AuthService.getCurrentUser(this); + + Long courseId = autoChapterId; + try { + android.widget.Spinner chapterSp = findViewById(R.id.chapterSpinner); + if (chapterSp != null && availableChapters != null && !availableChapters.isEmpty()) { + int pos = chapterSp.getSelectedItemPosition(); + if (pos >= 0 && pos < availableChapters.size()) courseId = availableChapters.get(pos).getId(); + } + } catch (Exception ignored) {} + + String storedPath = null; + if (!selectedFiles.isEmpty()) { + android.net.Uri uri = selectedFiles.get(0); + String name = com.example.myapplication.service.UploadService.getDisplayName(this, uri); + java.io.File dir = new java.io.File(getFilesDir(), "notes"); + if (!dir.exists()) dir.mkdirs(); + java.io.File out = new java.io.File( + dir, + System.currentTimeMillis() + "_" + (name == null ? "note" : name) + ); + copyUriToFile(uri, out); + storedPath = out.getAbsolutePath(); + } + + com.example.myapplication.repository.NoteRepository repo = + new com.example.myapplication.repository.NoteRepository(this); + ok = repo.saveNote(title, content, storedPath, courseId, uploaderId); + if (ok && uploaderId != null && !uploaderId.isEmpty()) { + new com.example.myapplication.repository.UserRepository(this) + .addPoints(uploaderId, 10); + } + } catch (Exception e) { + errMsg = e.getMessage(); + android.util.Log.e("UploadUI", "persistNote failed", e); + } + + boolean finalOk = ok; + String finalErr = errMsg; + runOnUiThread(() -> { + submitBtn.setEnabled(true); + if (finalOk) { + progressText.setText("上传成功 100%"); + com.example.myapplication.Utils.toast(this, "上传成功!"); + finish(); + } else { + progressText.setText("上传失败"); + com.example.myapplication.Utils.toast(this, "上传失败" + (finalErr != null ? ":" + finalErr : "")); + } + }); + }).start(); + } + + // 新增:复制文件(使用完整类名,避免 import 依赖) + private void copyUriToFile(android.net.Uri uri, java.io.File outFile) throws java.io.IOException { + try (java.io.InputStream in = getContentResolver().openInputStream(uri); + java.io.OutputStream out = new java.io.FileOutputStream(outFile)) { + byte[] buf = new byte[8192]; + int len; + while ((len = in.read(buf)) != -1) { + out.write(buf, 0, len); + } + out.flush(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode != RESULT_OK || data == null) return; + + // 已删除:REQ_PICK_IMAGE 分支 + + if (requestCode == REQ_OPEN_DOCUMENT) { + if (data.getClipData() != null) { + for (int i = 0; i < data.getClipData().getItemCount(); i++) { + Uri uri = data.getClipData().getItemAt(i).getUri(); + addFileItem(uri); + } + } else { + Uri uri = data.getData(); + if (uri != null) addFileItem(uri); + } + updatePreviewFromFirstImage(); + } + } + + private void autoFillFields(String fullText, String displayName, String mime) { + EditText titleEt = findViewById(R.id.noteTitle); + Spinner subjectSp = findViewById(R.id.subjectSpinner); + EditText contentEt = findViewById(R.id.contentInput); + EditText tagsEt = findViewById(R.id.tagsInput); + + String title = titleEt.getText().toString().trim(); + String fromName = (displayName == null) ? "" : displayName.replaceAll("\\.[^.]+$", ""); + + if (title.isEmpty()) { + if (fullText != null && !fullText.trim().isEmpty()) { + for (String line : fullText.split("\\r?\\n")) { + String t = line.trim(); + if (!t.isEmpty()) { title = t; break; } + } + } + if (title.isEmpty() && !fromName.isEmpty()) title = fromName; + if (!title.isEmpty()) titleEt.setText(title); + } + + String base = (fullText != null && !fullText.isEmpty()) ? fullText : fromName; + String subjectGuess = guessSubjectFromDatabase(base); + setSpinnerSelectionByText(subjectSp, subjectGuess); + + // 仅当内容输入框为空时,才用摘要填充,避免覆盖完整 OCR 文本 + boolean isContentEmpty = contentEt.getText().toString().trim().isEmpty(); + + if (fullText != null && !fullText.trim().isEmpty()) { + String[] lines = fullText.split("\\r?\\n"); + StringBuilder sb = new StringBuilder(); + int count = 0; + for (String l : lines) { + String line = l.trim(); + if (line.isEmpty() || line.startsWith("---") || line.endsWith("---")) continue; + sb.append(line).append(" "); + if (++count >= 5) break; + } + String desc = sb.toString().trim(); + if (!desc.isEmpty() && isContentEmpty) { + if (processedFiles.size() > 1) { + desc = "基于" + processedFiles.size() + "个文件的内容:" + desc; + } + contentEt.setText(desc); + } + } else if (!fromName.isEmpty() && isContentEmpty) { + contentEt.setText("文档:" + fromName + (mime != null ? "(" + mime + ")" : "")); + } + + StringBuilder tags = new StringBuilder(); + + // 首先添加学科名称作为标签 + if (subjectGuess != null) { + tags.append(subjectGuess); + } + + // 然后添加具体的技术标签 + for (String k : new String[]{"DH参数","矩阵变换","坐标系","机械臂","运动学","动力学","雅可比矩阵","线性代数","函数","极限","力学","算法","数据结构","人工智能","机器学习","深度学习","神经网络","计算机视觉","自然语言处理","数据库","操作系统","计算机网络","软件工程","编程语言","Web开发","移动开发"}) { + if ((base + " " + title).toLowerCase().contains(k.toLowerCase())) { + if (tags.length() > 0) tags.append(","); + tags.append(k); + } + } + + if (tags.length() > 0) { + String currentTags = tagsEt.getText().toString().trim(); + if (currentTags.isEmpty()) { + tagsEt.setText(tags.toString()); + } else { + // 合并标签,避免重复 + Set tagSet = new HashSet<>(Arrays.asList(currentTags.split(","))); + for (String tag : tags.toString().split(",")) { + tagSet.add(tag.trim()); + } + StringBuilder sb = new StringBuilder(); + for (String s : tagSet) { + if (s == null) continue; + String trimmed = s.trim(); + if (trimmed.isEmpty()) continue; + if (sb.length() > 0) sb.append(","); + sb.append(trimmed); + } + tagsEt.setText(sb.toString()); + } + } + + // (已删除)智能分类建议的拼接与设置,避免对不存在视图的 setText 调用 + // StringBuilder sug = new StringBuilder(); + // sug.append("智能分类建议:\n"); + // if (subjectGuess != null) sug.append("科目:").append(subjectGuess).append("\n"); + // if (tags.length() > 0) sug.append("标签:").append(tags); + // categorySuggestions.setText(sug.toString()); + + // 触发自动分类 + onOcrTextReady(fullText); + } + + private void setSpinnerSelectionByText(Spinner sp, String text) { + if (text == null) return; + android.widget.ArrayAdapter adapter = (android.widget.ArrayAdapter) sp.getAdapter(); + if (adapter == null) return; + for (int i = 0; i < adapter.getCount(); i++) { + Object item = adapter.getItem(i); + if (item != null && text.equals(item.toString())) { + sp.setSelection(i); + return; + } + } + } + + private String guessSubjectFromDatabase(String base) { + if (base == null) return null; + String lower = base.toLowerCase(); + try { + com.example.myapplication.repository.CourseRepository cr = new com.example.myapplication.repository.CourseRepository(this); + java.util.List subjects = cr.getAllCourses(); + for (com.example.myapplication.domain.Course s : subjects) { + String name = s.getName() == null ? "" : s.getName(); + String code = s.getCode() == null ? "" : s.getCode(); + if (!name.isEmpty() && lower.contains(name.toLowerCase())) return name; + if (!code.isEmpty() && lower.contains(code.toLowerCase())) return name; + java.util.List chapters = cr.getChaptersBySubjectId(s.getId()); + for (com.example.myapplication.domain.Chapter ch : chapters) { + String cn = ch.getName() == null ? "" : ch.getName(); + if (!cn.isEmpty() && lower.contains(cn.toLowerCase())) return name; + } + } + } catch (Exception e) { + android.util.Log.e("UploadUI", "guessSubjectFromDatabase failed", e); + } + return null; + } + + private void addFileItem(android.net.Uri uri) { + if (uri == null) return; + selectedFiles.add(uri); + + String name = UploadService.getDisplayName(this, uri); + String mime = UploadService.getMimeType(this, uri); + + LinearLayout row = new LinearLayout(this); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + TextView tv = new TextView(this); + tv.setText((name == null ? "文件" : name) + " | " + (mime == null ? "" : mime)); + tv.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); + + Button removeBtn = new Button(this); + removeBtn.setText("移除"); + removeBtn.setOnClickListener(v -> { + selectedFiles.remove(uri); + fileListContainer.removeView(row); + + // 清理聚合文本内容 + String fileKey = name + "_" + mime; + if (processedFiles.contains(fileKey)) { + processedFiles.remove(fileKey); + // 重新构建聚合文本 + rebuildAggregatedText(); + } + + if (currentPreviewUri != null && currentPreviewUri.equals(uri)) { + if (previewImage != null) previewImage.setImageDrawable(null); + // 删除旧:ocrResultText.setText(...) + if (fileInfoText != null) fileInfoText.setText("未选择文件"); + updatePreviewFromFirstImage(); + } + }); + + row.addView(tv); + row.addView(removeBtn); + fileListContainer.addView(row); + + // 自动提取文档文本内容 + extractDocumentText(uri, name, mime); + } + + // 文档文本提取方法 + // 将图片 OCR 调用切换到 OCRService(替换原 runTextRecognition 的使用) + private void extractDocumentText(android.net.Uri uri, String displayName, String mimeType) { + new Thread(() -> { + try { + String extractedText = ""; + if (mimeType != null) { + if (mimeType.contains("pdf")) { + extractedText = com.example.myapplication.service.UploadService.extractPdfText(this, uri); + } else if (mimeType.contains("text/plain")) { + extractedText = com.example.myapplication.service.UploadService.extractPlainText(this, uri); + } else if (mimeType.contains("msword") || mimeType.contains("wordprocessingml")) { + runOnUiThread(() -> { + if (fileInfoText != null) { + fileInfoText.setText("Word文档暂不支持,建议转换为PDF或文本格式"); + } + }); + return; + } else if (mimeType.contains("image")) { + runOnUiThread(() -> { + try { + com.google.mlkit.vision.common.InputImage image = + com.google.mlkit.vision.common.InputImage.fromFilePath(this, uri); + com.example.myapplication.service.OCRService.recognize( + this, + image, + new OcrDocCallback(UploadUI.this, displayName, mimeType) + ); + } catch (java.io.IOException e) { + android.util.Log.e("OCR", "图片加载失败", e); + } + }); + return; + } + } + + // 如果提取到文本,在主线程更新UI(增加 null 安全判断) + if (extractedText != null && !extractedText.trim().isEmpty()) { + final String finalText = extractedText; + runOnUiThread(() -> { + onOcrTextReady(finalText); // 直接写入内容框并联动分类 + autoFillFields(finalText, displayName, mimeType); // 保持其他字段自动填充 + }); + } + } catch (Exception e) { + android.util.Log.e("UploadUI", "extractDocumentText failed", e); + } + }).start(); + } + + // PDF文本提取 + private String extractPdfText(Uri uri) throws IOException { + InputStream inputStream = getContentResolver().openInputStream(uri); + PDDocument document = PDDocument.load(inputStream); + PDFTextStripper stripper = new PDFTextStripper(); + + // 限制提取页数,避免处理时间过长 + int pageCount = document.getNumberOfPages(); + if (pageCount > 10) { + stripper.setEndPage(10); // 只提取前10页 + } + + String text = stripper.getText(document); + document.close(); + inputStream.close(); + + return text; + } + + + + // 纯文本文件提取 + private String extractPlainText(Uri uri) throws IOException { + InputStream inputStream = getContentResolver().openInputStream(uri); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + inputStream.close(); + return result.toString("UTF-8"); + } + + private void updatePreviewFromFirstImage() { + for (Uri u : selectedFiles) { + String mime = com.example.myapplication.service.UploadService.getMimeType(this, u); + if (mime != null && mime.startsWith("image/")) { + if (previewImage != null) previewImage.setImageURI(u); + currentPreviewUri = u; + if (fileInfoText != null) { + fileInfoText.setText( + com.example.myapplication.service.UploadService.getDisplayName(this, u) + " | " + mime + ); + } + try { + com.google.mlkit.vision.common.InputImage image = + com.google.mlkit.vision.common.InputImage.fromFilePath(this, u); + com.example.myapplication.service.OCRService.recognize( + this, + image, + new OcrPreviewCallback(UploadUI.this, u, mime) + ); + } catch (Exception e) { + Utils.toast(this, "读取图片失败:" + e.getMessage()); + } + return; + } + } + previewImage.setImageDrawable(null); + currentPreviewUri = null; + } + + // 命名的 OCR 回调类,替代匿名内部类 $1/$2,改为静态内部类以避免某些 D8 处理内部类元数据时的崩溃 + static class OcrDocCallback implements com.example.myapplication.service.OCRService.Callback { + private final UploadUI ui; + private final String displayName; + private final String mimeType; + + OcrDocCallback(UploadUI ui, String displayName, String mimeType) { + this.ui = ui; + this.displayName = displayName; + this.mimeType = mimeType; + } + + @Override + public void onSuccess(String text) { + ui.runOnUiThread(() -> { + android.widget.EditText contentEt = ui.findViewById(R.id.contentInput); + if (contentEt != null) { + contentEt.setText(text != null ? text : ""); + } + ui.onOcrTextReady(text); + ui.autoFillFields(text, displayName, mimeType); + }); + } + + @Override + public void onFailure(Exception e) { + com.example.myapplication.Utils.toast(ui, "识别失败:" + e.getMessage()); + } + } + + private static class OcrPreviewCallback implements com.example.myapplication.service.OCRService.Callback { + private final UploadUI ui; + private final Uri uri; + private final String mime; + OcrPreviewCallback(UploadUI ui, Uri uri, String mime) { + this.ui = ui; + this.uri = uri; + this.mime = mime; + } + @Override + public void onSuccess(String text) { + android.widget.EditText contentEt = ui.findViewById(R.id.contentInput); + if (contentEt != null) { + contentEt.setText((text == null || text.isEmpty()) ? "未识别到文字" : text); + } + ui.autoFillFields(text, + com.example.myapplication.service.UploadService.getDisplayName(ui, uri), + mime); + ui.onOcrTextReady(text); + } + @Override + public void onFailure(Exception e) { + Utils.toast(ui, "读取图片失败:" + e.getMessage()); + } + } + + private String getMimeType(Uri uri) { + ContentResolver cr = getContentResolver(); + return cr.getType(uri); + } + + private String getDisplayName(Uri uri) { + String name = null; + try (Cursor cursor = getContentResolver() + .query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + name = cursor.getString(0); + } + } catch (Exception ignored) {} + return name; + } + + // 安全方法:避免视图为 null 时崩溃 + private void safeSetText(TextView view, CharSequence text) { + if (view != null) { + view.setText(text != null ? text : ""); + } + } + + private void safeSetSelection(Spinner spinner, int position) { + if (spinner != null && position >= 0) { + spinner.setSelection(position); + } + } + + private void onOcrTextReady(String ocrText) { + if (classificationService != null && ocrText != null && ocrText.trim().length() > 0) { + com.example.myapplication.service.ClassificationService.Result res = + classificationService.classifyWithSegmentation(ocrText); + if (classificationResultText != null) { + classificationResultText.setVisibility(android.view.View.VISIBLE); + String confidenceLevel = ""; + if (res.confidence >= 0.8) { + confidenceLevel = "高"; + } else if (res.confidence >= 0.6) { + confidenceLevel = "中"; + } else if (res.confidence >= 0.4) { + confidenceLevel = "低"; + } else { + confidenceLevel = "很低"; + } + classificationResultText.setText( + "推荐分类:" + res.bestTitle + + "(匹配度" + String.format(java.util.Locale.getDefault(), "%.0f%%", res.confidence * 100) + ")" + ); + } + try { + applyAutoChapterSelection(res); + } catch (Exception e) { + android.util.Log.e("UploadUI", "auto chapter match failed", e); + } + + // 低置信度时静默应用最佳候选,不再弹窗打扰用户 + if (res.confidence < 0.6 && res.topCandidates != null && !res.topCandidates.isEmpty()) { + // 直接取第一个候选作为推荐结果应用 + com.example.myapplication.service.ClassificationService.Candidate sel = res.topCandidates.get(0); + autoChapterId = sel.chapterId; + autoSubjectId = sel.subjectId; + Spinner subjectSp = findViewById(R.id.subjectSpinner); + if (subjectSp != null) { + com.example.myapplication.repository.CourseRepository cr = new com.example.myapplication.repository.CourseRepository(this); + java.util.List subs = cr.getAllCourses(); + java.util.List names = new java.util.ArrayList<>(); + int subjIndex = -1; + for (int i = 0; i < subs.size(); i++) { + names.add(subs.get(i).getName()); + if (subs.get(i).getId() == autoSubjectId) subjIndex = i; + } + android.widget.ArrayAdapter subjAdapter = new android.widget.ArrayAdapter<>( + this, android.R.layout.simple_spinner_item, names); + subjAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + subjectSp.setAdapter(subjAdapter); + if (subjIndex >= 0) subjectSp.setSelection(subjIndex); + } + } + } + } + + private void applyAutoChapterSelection(com.example.myapplication.service.ClassificationService.Result res) { + if (res == null || res.bestTitle == null) return; + String hint = res.bestTitle.trim(); + com.example.myapplication.repository.CourseRepository cr = new com.example.myapplication.repository.CourseRepository(this); + java.util.List hits = cr.findChaptersByNameLike(hint, 1); + if (hits == null || hits.isEmpty()) return; + com.example.myapplication.domain.Chapter ch = hits.get(0); + autoChapterId = ch.getId(); + autoSubjectId = (long) ch.getCourseId(); + // 预选中科目下拉 + Spinner subjectSp = findViewById(R.id.subjectSpinner); + if (subjectSp != null) { + if (availableSubjects == null || availableSubjects.isEmpty()) { + String account = com.example.myapplication.service.AuthService.getCurrentUser(this); + com.example.myapplication.repository.UserRepository ur2 = new com.example.myapplication.repository.UserRepository(this); + com.example.myapplication.domain.User u2 = ur2.getUserByAccount(account); + Long majorId = u2 != null ? u2.getMajorId() : null; + String gradeText = ur2.getGradeText(account); + availableSubjects = cr.getCoursesByMajorAndGrade(majorId, gradeText); + if (availableSubjects == null || availableSubjects.isEmpty()) availableSubjects = cr.getCoursesByMajor(majorId); + if (availableSubjects == null || availableSubjects.isEmpty()) availableSubjects = cr.getAllCourses(); + } + int subjIndex = -1; + for (int i = 0; i < availableSubjects.size(); i++) { + if (availableSubjects.get(i).getId() == autoSubjectId) { subjIndex = i; break; } + } + if (subjIndex >= 0) subjectSp.setSelection(subjIndex); + } + loadChaptersForSubject(autoSubjectId); + android.widget.Spinner chapterSp = findViewById(R.id.chapterSpinner); + if (chapterSp != null && availableChapters != null) { + int chIndex = -1; + for (int i = 0; i < availableChapters.size(); i++) if (availableChapters.get(i).getId() == autoChapterId) { chIndex = i; break; } + if (chIndex >= 0) chapterSp.setSelection(chIndex); + } + } + + private void loadChaptersForSubject(long subjectId) { + com.example.myapplication.repository.CourseRepository cr = new com.example.myapplication.repository.CourseRepository(this); + availableChapters = cr.getChaptersBySubjectId(subjectId); + java.util.List names = new java.util.ArrayList<>(); + for (com.example.myapplication.domain.Chapter c : availableChapters) names.add(c.getName()); + android.widget.ArrayAdapter ad = new android.widget.ArrayAdapter<>(this, android.R.layout.simple_spinner_item, names); + ad.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + android.widget.Spinner chapterSp = findViewById(R.id.chapterSpinner); + if (chapterSp != null) chapterSp.setAdapter(ad); + } + + // HanLP增强版分类器(使用真正的HanLP开源代码) + private static class KeywordClassifier { + private final Context context; + private final List leafNodes = new ArrayList<>(); + private final Set stopwords = new HashSet<>(Arrays.asList( + "的", "和", "与", "或", "在", "是", "为", "及", "等", "公式", "计算", "方法", "简介" + )); + private boolean hanLPInitialized = false; + + static class Node { + String id; + String title; + List keywords = new ArrayList<>(); + List aliases = new ArrayList<>(); + } + + static class Result { + String bestId; + String bestTitle; + double confidence; + List topCandidates = new ArrayList<>(); + } + + KeywordClassifier(Context ctx) throws IOException, JSONException { + this.context = ctx.getApplicationContext(); + loadOutlineFromAssets("course_outline_robotics.json"); + + // 初始化HanLP + try { + // 测试HanLP是否可用 + Log.i("HanLP", "HanLP初始化成功"); + } catch (Exception e) { + Log.w("HanLP", "HanLP初始化失败,将使用基础分类: " + e.getMessage()); + } + } + + private void loadOutlineFromAssets(String fileName) throws IOException, JSONException { + String json = readAssetText(fileName); + JSONObject root = new JSONObject(json); + JSONArray nodes = root.getJSONArray("nodes"); + for (int i = 0; i < nodes.length(); i++) { + JSONObject chapter = nodes.getJSONObject(i); + parseNodeRecursive(chapter); + } + } + + private void parseNodeRecursive(JSONObject obj) throws JSONException { + String id = obj.optString("id"); + String title = obj.optString("title"); + List keywords = jsonArrayToList(obj.optJSONArray("keywords")); + List aliases = jsonArrayToList(obj.optJSONArray("aliases")); + JSONArray children = obj.optJSONArray("children"); + + if (children == null || children.length() == 0) { + Node node = new Node(); + node.id = id; + node.title = title; + node.keywords.addAll(keywords); + node.aliases.addAll(aliases); + leafNodes.add(node); + } else { + Node node = new Node(); + node.id = id; + node.title = title; + node.keywords.addAll(keywords); + node.aliases.addAll(aliases); + leafNodes.add(node); + + for (int i = 0; i < children.length(); i++) { + parseNodeRecursive(children.getJSONObject(i)); + } + } + } + + private List jsonArrayToList(JSONArray arr) { + List list = new ArrayList<>(); + if (arr == null) return list; + for (int i = 0; i < arr.length(); i++) { + list.add(arr.optString(i)); + } + return list; + } + + private String readAssetText(String fileName) throws IOException { + AssetManager am = context.getAssets(); + InputStream is = am.open(fileName); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int len; + while ((len = is.read(buf)) != -1) { + baos.write(buf, 0, len); + } + is.close(); + return baos.toString("UTF-8"); + } + } + + // 重新构建聚合文本(当文件被移除时) + private void rebuildAggregatedText() { + // 注意:这是一个简化实现,实际应用中可能需要重新提取所有文件 + // 这里只是清空聚合文本,让用户重新上传文件 + + // 如果还有文件,提示用户重新处理 + if (!selectedFiles.isEmpty()) { + fileInfoText.setText("文件已移除,请重新上传以更新内容分析"); + } else { + fileInfoText.setText("未选择文件"); + // 清空表单 + EditText titleEt = findViewById(R.id.noteTitle); + EditText descEt = findViewById(R.id.contentInput); + EditText tagsEt = findViewById(R.id.tagsInput); + + if (titleEt != null) titleEt.setText(""); + if (descEt != null) descEt.setText(""); + if (tagsEt != null) tagsEt.setText(""); + + if (classificationResultText != null) { + classificationResultText.setVisibility(View.GONE); + } + } + } + + // HanLP增强分类器(真正集成HanLP) + private static class EnhancedKeywordClassifier { + private final Context context; + private boolean hanLPInitialized = false; + + static class Result { + String bestId = ""; + String bestTitle = ""; + double confidence = 0.0; + boolean isSegmented = false; + List topCandidates = new ArrayList<>(); + } + + EnhancedKeywordClassifier(Context ctx) throws IOException, JSONException { + this.context = ctx.getApplicationContext(); + // 移除 HanLP 依赖,UI 层仅使用基础分类逻辑 + hanLPInitialized = false; + } + + Result classifyWithSegmentation(String text) { + Result result = new Result(); + if (text == null || text.trim().isEmpty()) { + result.bestTitle = "未能判定,建议人工选择章节"; + return result; + } + // 仅使用基础分类逻辑,移除 HanLP 相关增强 + result = performBasicClassification(text); + return result; + } + + private Result performBasicClassification(String text) { + Result result = new Result(); + String lowerText = text.toLowerCase(); + + // 机器人学相关关键词匹配 + if (lowerText.contains("机器人") || lowerText.contains("robot") || lowerText.contains("robotics")) { + result.bestTitle = "第1章 机器人学概述"; + result.confidence = 0.8; + } else if (lowerText.contains("坐标") || lowerText.contains("变换") || lowerText.contains("transform") || + lowerText.contains("齐次") || lowerText.contains("homogeneous")) { + result.bestTitle = "第2章 坐标变换与齐次变换"; + result.confidence = 0.75; + } else if (lowerText.contains("运动学") || lowerText.contains("kinematics") || + lowerText.contains("正运动学") || lowerText.contains("逆运动学")) { + result.bestTitle = "第3章 机器人运动学"; + result.confidence = 0.8; + } else if (lowerText.contains("雅可比") || lowerText.contains("jacobian") || + lowerText.contains("速度") || lowerText.contains("velocity")) { + result.bestTitle = "第4章 雅可比矩阵"; + result.confidence = 0.85; + } else if (lowerText.contains("动力学") || lowerText.contains("dynamics") || + lowerText.contains("拉格朗日") || lowerText.contains("lagrange")) { + result.bestTitle = "第5章 机器人动力学"; + result.confidence = 0.8; + } else if (lowerText.contains("轨迹") || lowerText.contains("路径") || lowerText.contains("trajectory") || + lowerText.contains("规划") || lowerText.contains("planning")) { + result.bestTitle = "第6章 轨迹规划"; + result.confidence = 0.75; + } else if (lowerText.contains("控制") || lowerText.contains("control") || + lowerText.contains("pid") || lowerText.contains("反馈")) { + result.bestTitle = "第7章 机器人控制"; + result.confidence = 0.8; + } else { + result.bestTitle = "第1章 机器人学概述"; + result.confidence = 0.5; + } + + return result; + } + } +} + diff --git a/src/main/java/com/example/myapplication/ui/UserCenterUI.java b/src/main/java/com/example/myapplication/ui/UserCenterUI.java new file mode 100644 index 0000000..34dd381 --- /dev/null +++ b/src/main/java/com/example/myapplication/ui/UserCenterUI.java @@ -0,0 +1,225 @@ +package com.example.myapplication.ui; +import com.example.myapplication.R; +import com.example.myapplication.Utils; +import com.example.myapplication.service.UserService; +import android.os.Bundle; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.text.TextUtils; +import androidx.appcompat.app.AppCompatActivity; + +public class UserCenterUI extends AppCompatActivity { + private TextView statUploaded, statLikes, statPoints, majorNameTv, gradeNameTv; + private EditText usernameEt; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_user_center); + + findViewById(R.id.backBtn).setOnClickListener(v -> finish()); + + statUploaded = findViewById(R.id.statUploaded); + statLikes = findViewById(R.id.statLikes); + statPoints = findViewById(R.id.statPoints); + majorNameTv = findViewById(R.id.majorNameTv); + gradeNameTv = findViewById(R.id.gradeNameTv); + + usernameEt = findViewById(R.id.usernameInput); + + + // 可选:去掉写死示例,统一由 refreshStats() 赋值 + statUploaded.setText("15"); + statLikes.setText("128"); + + usernameEt.setText(com.example.myapplication.service.UserService.getUser(this)); + + + + // 刷新数据库统计 + refreshStats(); + // 动态渲染我的笔记 + renderMyNotes(); + + } + + @Override + protected void onResume() { + super.onResume(); + refreshStats(); + renderMyNotes(); + } + + private void refreshStats() { + String account = com.example.myapplication.service.AuthService.getCurrentUser(this); + com.example.myapplication.repository.NoteRepository noteRepo = + new com.example.myapplication.repository.NoteRepository(this); + java.util.List myNotes = + noteRepo.getNotesByUser(account); + int uploadedCount = myNotes != null ? myNotes.size() : 0; + + int totalLikes = new com.example.myapplication.repository.NoteRepository(this) + .getTotalLikesByUser(account); + + com.example.myapplication.repository.UserRepository userRepo = + new com.example.myapplication.repository.UserRepository(this); + int points = userRepo.getPoints(account); + + com.example.myapplication.domain.User u = userRepo.getUserByAccount(account); + String majorName = "未设置"; + String gradeText = null; + if (u != null && u.getMajorId() != null) { + com.example.myapplication.repository.MajorRepository mr = new com.example.myapplication.repository.MajorRepository(this); + com.example.myapplication.domain.Major m = mr.getById(u.getMajorId()); + if (m != null && m.getName() != null) majorName = m.getName(); + } + + statUploaded.setText(String.valueOf(uploadedCount)); + statLikes.setText(String.valueOf(totalLikes)); + statPoints.setText(String.valueOf(points)); + if (majorNameTv != null) majorNameTv.setText("专业:" + majorName); + gradeText = new com.example.myapplication.repository.UserRepository(this).getGradeText(account); + if (gradeNameTv != null) gradeNameTv.setText("年级:" + (gradeText == null ? "未设置" : gradeText)); + } + + // 动态渲染“我的笔记” + private void renderMyNotes() { + android.widget.LinearLayout container = findViewById(R.id.myNotesContainer); + if (container == null) return; + + container.removeAllViews(); + + String account = com.example.myapplication.service.AuthService.getCurrentUser(this); + com.example.myapplication.repository.NoteRepository repo = + new com.example.myapplication.repository.NoteRepository(this); + java.util.List notes = + repo.getNotesByUser(account); + + if (notes == null || notes.isEmpty()) { + android.widget.TextView empty = new android.widget.TextView(this); + empty.setText("暂无笔记,点击“上传笔记”添加吧~"); + empty.setTextColor(android.graphics.Color.parseColor("#666666")); + container.addView(empty); + return; + } + + float density = getResources().getDisplayMetrics().density; + int pad = (int) (12 * density); + int mt = (int) (12 * density); + + com.example.myapplication.repository.LikeRepository likeRepo = + new com.example.myapplication.repository.LikeRepository(this); + + for (com.example.myapplication.domain.Note n : notes) { + android.widget.LinearLayout item = new android.widget.LinearLayout(this); + item.setOrientation(android.widget.LinearLayout.VERTICAL); + item.setBackgroundResource(R.drawable.card_background); + item.setPadding(pad, pad, pad, pad); + + android.widget.LinearLayout.LayoutParams lp = + new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT); + lp.setMargins(0, mt, 0, 0); + item.setLayoutParams(lp); + item.setElevation(4f); + + android.widget.TextView titleTv = new android.widget.TextView(this); + titleTv.setText(n.getTitle() != null ? n.getTitle() : "(无标题)"); + titleTv.setTextColor(getResources().getColor(R.color.text_primary)); + titleTv.setTypeface(android.graphics.Typeface.DEFAULT_BOLD); + titleTv.setTextSize(16); + + int likes = n.getLikeCount(); + String created = n.getCreated() != null ? n.getCreated() : ""; + android.widget.TextView metaTv = new android.widget.TextView(this); + metaTv.setText((TextUtils.isEmpty(created) ? "📅 未知" : "📅 " + created) + " 👍 " + likes); + metaTv.setTextColor(getResources().getColor(R.color.text_secondary)); + metaTv.setTextSize(14); + + item.addView(titleTv); + item.addView(metaTv); + + android.widget.LinearLayout actions = new android.widget.LinearLayout(this); + actions.setOrientation(android.widget.LinearLayout.HORIZONTAL); + android.widget.LinearLayout.LayoutParams alp = new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT + ); + alp.topMargin = (int) (12 * density); + actions.setLayoutParams(alp); + + android.widget.Button viewBtn = new android.widget.Button(this); + viewBtn.setText("查看"); + viewBtn.setAllCaps(false); + viewBtn.setPadding(pad, pad / 2, pad, pad / 2); + viewBtn.setTextColor(android.graphics.Color.WHITE); + androidx.core.view.ViewCompat.setBackgroundTintList(viewBtn, + android.content.res.ColorStateList.valueOf(getResources().getColor(R.color.primary))); + + android.widget.Button editBtn = new android.widget.Button(this); + editBtn.setText("编辑"); + editBtn.setAllCaps(false); + editBtn.setPadding(pad, pad / 2, pad, pad / 2); + editBtn.setTextColor(android.graphics.Color.WHITE); + androidx.core.view.ViewCompat.setBackgroundTintList(editBtn, + android.content.res.ColorStateList.valueOf(getResources().getColor(R.color.success))); + + android.widget.Button delBtn = new android.widget.Button(this); + delBtn.setText("删除"); + delBtn.setAllCaps(false); + delBtn.setPadding(pad, pad / 2, pad, pad / 2); + delBtn.setTextColor(android.graphics.Color.WHITE); + androidx.core.view.ViewCompat.setBackgroundTintList(delBtn, + android.content.res.ColorStateList.valueOf(getResources().getColor(R.color.danger))); + + android.widget.LinearLayout.LayoutParams btnLp = new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT + ); + actions.addView(viewBtn, btnLp); + android.widget.Space sp1 = new android.widget.Space(this); + actions.addView(sp1, new android.widget.LinearLayout.LayoutParams((int)(8 * density), 1)); + actions.addView(editBtn, btnLp); + android.widget.Space sp2 = new android.widget.Space(this); + actions.addView(sp2, new android.widget.LinearLayout.LayoutParams((int)(8 * density), 1)); + actions.addView(delBtn, btnLp); + + viewBtn.setOnClickListener(v -> { + com.example.myapplication.service.NoteService.setCurrentNoteId(this, (int) n.getId()); + android.content.Intent intent = new android.content.Intent(this, com.example.myapplication.ui.NoteDetailUI.class); + intent.putExtra("noteId", (int) n.getId()); + startActivity(intent); + }); + + editBtn.setOnClickListener(v -> { + com.example.myapplication.service.NoteService.setCurrentNoteId(this, (int) n.getId()); + android.content.Intent intent = new android.content.Intent(this, com.example.myapplication.ui.NoteEditUI.class); + startActivity(intent); + }); + + delBtn.setOnClickListener(v -> { + android.app.AlertDialog.Builder b = new android.app.AlertDialog.Builder(this); + b.setTitle("删除笔记"); + b.setMessage("确定要删除这条笔记吗?"); + b.setPositiveButton("删除", (d, which) -> { + boolean okDel = new com.example.myapplication.repository.NoteRepository(this).deleteNote(n.getId()); + if (okDel) { + com.example.myapplication.Utils.toast(this, "已删除"); + refreshStats(); + renderMyNotes(); + } else { + com.example.myapplication.Utils.toast(this, "删除失败"); + } + }); + b.setNegativeButton("取消", (d, which) -> {}); + b.show(); + }); + + item.addView(actions); + + container.addView(item); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/micode/myapplication/database/DatabaseHelper.java b/src/main/java/net/micode/myapplication/database/DatabaseHelper.java new file mode 100644 index 0000000..c55c397 --- /dev/null +++ b/src/main/java/net/micode/myapplication/database/DatabaseHelper.java @@ -0,0 +1,95 @@ +package net.micode.myapplication.database; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class DatabaseHelper extends SQLiteOpenHelper { + private static final String DATABASE_NAME = "MyNote.db"; + private static final int DATABASE_VERSION = 1; + + // 表名 + public static final String TABLE_COURSES = "courses"; + public static final String TABLE_CHAPTERS = "chapters"; + public static final String TABLE_NOTES = "notes"; + + // Courses表列名 + public static final String COLUMN_COURSE_ID = "course_id"; + public static final String COLUMN_COURSE_NAME = "course_name"; + public static final String COLUMN_SCHOOL = "school"; + + // Chapters表列名 + public static final String COLUMN_CHAPTER_ID = "chapter_id"; + public static final String COLUMN_CHAPTER_NUMBER = "chapter_number"; + public static final String COLUMN_CHAPTER_TITLE = "chapter_title"; + public static final String COLUMN_KEYWORDS = "keywords"; + public static final String COLUMN_ALIASES = "aliases"; + + // Notes表列名 + public static final String COLUMN_NOTE_ID = "note_id"; + public static final String COLUMN_TITLE = "title"; + public static final String COLUMN_DESCRIPTION = "description"; + public static final String COLUMN_OCR_TEXT = "ocr_text"; + public static final String COLUMN_FILE_PATH = "file_path"; + public static final String COLUMN_UPLOAD_TIME = "upload_time"; + public static final String COLUMN_UPDATE_TIME = "update_time"; + + // 创建Courses表的SQL语句 + private static final String CREATE_TABLE_COURSES = "CREATE TABLE " + TABLE_COURSES + " (" + + COLUMN_COURSE_ID + " TEXT PRIMARY KEY, " + + COLUMN_COURSE_NAME + " TEXT NOT NULL, " + + COLUMN_SCHOOL + " TEXT" + + ");"; + + // 创建Chapters表的SQL语句 + private static final String CREATE_TABLE_CHAPTERS = "CREATE TABLE " + TABLE_CHAPTERS + " (" + + COLUMN_CHAPTER_ID + " TEXT PRIMARY KEY, " + + COLUMN_COURSE_ID + " TEXT NOT NULL, " + + COLUMN_CHAPTER_NUMBER + " INTEGER NOT NULL, " + + COLUMN_CHAPTER_TITLE + " TEXT NOT NULL, " + + COLUMN_KEYWORDS + " TEXT, " + + COLUMN_ALIASES + " TEXT, " + + "FOREIGN KEY(" + COLUMN_COURSE_ID + ") REFERENCES " + TABLE_COURSES + "(" + COLUMN_COURSE_ID + ")" + + ");"; + + // 创建Notes表的SQL语句 + private static final String CREATE_TABLE_NOTES = "CREATE TABLE " + TABLE_NOTES + " (" + + COLUMN_NOTE_ID + " TEXT PRIMARY KEY, " + + COLUMN_TITLE + " TEXT NOT NULL, " + + COLUMN_DESCRIPTION + " TEXT, " + + COLUMN_OCR_TEXT + " TEXT, " + + COLUMN_COURSE_ID + " TEXT, " + + COLUMN_CHAPTER_ID + " TEXT, " + + COLUMN_FILE_PATH + " TEXT, " + + COLUMN_UPLOAD_TIME + " INTEGER, " + + COLUMN_UPDATE_TIME + " INTEGER, " + + "FOREIGN KEY(" + COLUMN_COURSE_ID + ") REFERENCES " + TABLE_COURSES + "(" + COLUMN_COURSE_ID + "), " + + "FOREIGN KEY(" + COLUMN_CHAPTER_ID + ") REFERENCES " + TABLE_CHAPTERS + "(" + COLUMN_CHAPTER_ID + ")" + + ");"; + + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_COURSES); + db.execSQL(CREATE_TABLE_CHAPTERS); + db.execSQL(CREATE_TABLE_NOTES); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NOTES); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_CHAPTERS); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_COURSES); + onCreate(db); + } + + // 清除所有数据的方法 + public void clearAllData(SQLiteDatabase db) { + db.execSQL("DELETE FROM " + TABLE_NOTES); + db.execSQL("DELETE FROM " + TABLE_CHAPTERS); + db.execSQL("DELETE FROM " + TABLE_COURSES); + } +} \ No newline at end of file diff --git a/src/main/java/net/micode/myapplication/model/Chapter.java b/src/main/java/net/micode/myapplication/model/Chapter.java new file mode 100644 index 0000000..3e9e186 --- /dev/null +++ b/src/main/java/net/micode/myapplication/model/Chapter.java @@ -0,0 +1,101 @@ +package net.micode.myapplication.model; + +import java.util.ArrayList; +import java.util.List; + +public class Chapter { + private String chapterId; + private String courseId; + private int chapterNumber; + private String chapterTitle; + private List keywords; + private List aliases; + + public Chapter() { + this.keywords = new ArrayList<>(); + this.aliases = new ArrayList<>(); + } + + public Chapter(String chapterId, String courseId, int chapterNumber, String chapterTitle) { + this.chapterId = chapterId; + this.courseId = courseId; + this.chapterNumber = chapterNumber; + this.chapterTitle = chapterTitle; + this.keywords = new ArrayList<>(); + this.aliases = new ArrayList<>(); + } + + public String getChapterId() { + return chapterId; + } + + public void setChapterId(String chapterId) { + this.chapterId = chapterId; + } + + public String getCourseId() { + return courseId; + } + + public void setCourseId(String courseId) { + this.courseId = courseId; + } + + public int getChapterNumber() { + return chapterNumber; + } + + public void setChapterNumber(int chapterNumber) { + this.chapterNumber = chapterNumber; + } + + public String getChapterTitle() { + return chapterTitle; + } + + public void setChapterTitle(String chapterTitle) { + this.chapterTitle = chapterTitle; + } + + public List getKeywords() { + return keywords; + } + + public void setKeywords(List keywords) { + this.keywords = keywords; + } + + public List getAliases() { + return aliases; + } + + public void setAliases(List aliases) { + this.aliases = aliases; + } + + public void addKeyword(String keyword) { + if (this.keywords == null) { + this.keywords = new ArrayList<>(); + } + this.keywords.add(keyword); + } + + public void addAlias(String alias) { + if (this.aliases == null) { + this.aliases = new ArrayList<>(); + } + this.aliases.add(alias); + } + + @Override + public String toString() { + return "Chapter{" + + "chapterId='" + chapterId + '\'' + + ", courseId='" + courseId + '\'' + + ", chapterNumber=" + chapterNumber + + ", chapterTitle='" + chapterTitle + '\'' + + ", keywords=" + keywords + + ", aliases=" + aliases + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/net/micode/myapplication/model/ClassificationResult.java b/src/main/java/net/micode/myapplication/model/ClassificationResult.java new file mode 100644 index 0000000..4eb25e2 --- /dev/null +++ b/src/main/java/net/micode/myapplication/model/ClassificationResult.java @@ -0,0 +1,60 @@ +package net.micode.myapplication.model; + +public class ClassificationResult { + private Course course; + private Chapter chapter; + private double confidenceScore; + private String reason; + + public ClassificationResult() { + } + + public ClassificationResult(Course course, Chapter chapter, double confidenceScore, String reason) { + this.course = course; + this.chapter = chapter; + this.confidenceScore = confidenceScore; + this.reason = reason; + } + + public Course getCourse() { + return course; + } + + public void setCourse(Course course) { + this.course = course; + } + + public Chapter getChapter() { + return chapter; + } + + public void setChapter(Chapter chapter) { + this.chapter = chapter; + } + + public double getConfidenceScore() { + return confidenceScore; + } + + public void setConfidenceScore(double confidenceScore) { + this.confidenceScore = confidenceScore; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + @Override + public String toString() { + return "ClassificationResult{" + + "course=" + (course != null ? course.getCourseName() : "null") + + ", chapter=" + (chapter != null ? chapter.getChapterTitle() : "null") + + ", confidenceScore=" + confidenceScore + + ", reason='" + reason + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/net/micode/myapplication/model/Course.java b/src/main/java/net/micode/myapplication/model/Course.java new file mode 100644 index 0000000..2fc7303 --- /dev/null +++ b/src/main/java/net/micode/myapplication/model/Course.java @@ -0,0 +1,49 @@ +package net.micode.myapplication.model; + +public class Course { + private String courseId; + private String courseName; + private String school; + + public Course() { + } + + public Course(String courseId, String courseName, String school) { + this.courseId = courseId; + this.courseName = courseName; + this.school = school; + } + + public String getCourseId() { + return courseId; + } + + public void setCourseId(String courseId) { + this.courseId = courseId; + } + + public String getCourseName() { + return courseName; + } + + public void setCourseName(String courseName) { + this.courseName = courseName; + } + + public String getSchool() { + return school; + } + + public void setSchool(String school) { + this.school = school; + } + + @Override + public String toString() { + return "Course{" + + "courseId='" + courseId + '\'' + + ", courseName='" + courseName + '\'' + + ", school='" + school + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/net/micode/myapplication/model/Note.java b/src/main/java/net/micode/myapplication/model/Note.java new file mode 100644 index 0000000..c71d07d --- /dev/null +++ b/src/main/java/net/micode/myapplication/model/Note.java @@ -0,0 +1,111 @@ +package net.micode.myapplication.model; + +public class Note { + private String noteId; + private String title; + private String description; + private String ocrText; + private String courseId; + private String chapterId; + private String filePath; + private long uploadTime; + private long updateTime; + + public Note() { + this.uploadTime = System.currentTimeMillis(); + this.updateTime = this.uploadTime; + } + + public Note(String noteId, String title, String description, String filePath) { + this.noteId = noteId; + this.title = title; + this.description = description; + this.filePath = filePath; + this.uploadTime = System.currentTimeMillis(); + this.updateTime = this.uploadTime; + } + + public String getNoteId() { + return noteId; + } + + public void setNoteId(String noteId) { + this.noteId = noteId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getOcrText() { + return ocrText; + } + + public void setOcrText(String ocrText) { + this.ocrText = ocrText; + } + + public String getCourseId() { + return courseId; + } + + public void setCourseId(String courseId) { + this.courseId = courseId; + } + + public String getChapterId() { + return chapterId; + } + + public void setChapterId(String chapterId) { + this.chapterId = chapterId; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + public long getUploadTime() { + return uploadTime; + } + + public void setUploadTime(long uploadTime) { + this.uploadTime = uploadTime; + } + + public long getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(long updateTime) { + this.updateTime = updateTime; + } + + @Override + public String toString() { + return "Note{" + + "noteId='" + noteId + '\'' + + ", title='" + title + '\'' + + ", description='" + description + '\'' + + ", courseId='" + courseId + '\'' + + ", chapterId='" + chapterId + '\'' + + ", uploadTime=" + uploadTime + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/net/micode/myapplication/repository/CourseRepository.java b/src/main/java/net/micode/myapplication/repository/CourseRepository.java new file mode 100644 index 0000000..b733d17 --- /dev/null +++ b/src/main/java/net/micode/myapplication/repository/CourseRepository.java @@ -0,0 +1,211 @@ +package net.micode.myapplication.repository; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import net.micode.myapplication.database.DatabaseHelper; +import net.micode.myapplication.model.Chapter; +import net.micode.myapplication.model.Course; + +import java.util.ArrayList; +import java.util.List; + +public class CourseRepository { + private DatabaseHelper dbHelper; + + public CourseRepository(Context context) { + this.dbHelper = new DatabaseHelper(context); + } + + // Course相关操作 + public long insertCourse(Course course) { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(DatabaseHelper.COLUMN_COURSE_ID, course.getCourseId()); + values.put(DatabaseHelper.COLUMN_COURSE_NAME, course.getCourseName()); + values.put(DatabaseHelper.COLUMN_SCHOOL, course.getSchool()); + + return db.insert(DatabaseHelper.TABLE_COURSES, null, values); + } + + public Course getCourseById(String courseId) { + SQLiteDatabase db = dbHelper.getReadableDatabase(); + Cursor cursor = db.query(DatabaseHelper.TABLE_COURSES, + null, + DatabaseHelper.COLUMN_COURSE_ID + " = ?", + new String[]{courseId}, + null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + Course course = new Course(); + course.setCourseId(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_COURSE_ID))); + course.setCourseName(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_COURSE_NAME))); + course.setSchool(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_SCHOOL))); + cursor.close(); + return course; + } + + if (cursor != null) { + cursor.close(); + } + return null; + } + + public List getAllCourses() { + List courses = new ArrayList<>(); + SQLiteDatabase db = dbHelper.getReadableDatabase(); + Cursor cursor = db.query(DatabaseHelper.TABLE_COURSES, null, null, null, null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + do { + Course course = new Course(); + course.setCourseId(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_COURSE_ID))); + course.setCourseName(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_COURSE_NAME))); + course.setSchool(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_SCHOOL))); + courses.add(course); + } while (cursor.moveToNext()); + cursor.close(); + } + + return courses; + } + + // Chapter相关操作 + public long insertChapter(Chapter chapter) { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(DatabaseHelper.COLUMN_CHAPTER_ID, chapter.getChapterId()); + values.put(DatabaseHelper.COLUMN_COURSE_ID, chapter.getCourseId()); + values.put(DatabaseHelper.COLUMN_CHAPTER_NUMBER, chapter.getChapterNumber()); + values.put(DatabaseHelper.COLUMN_CHAPTER_TITLE, chapter.getChapterTitle()); + values.put(DatabaseHelper.COLUMN_KEYWORDS, listToString(chapter.getKeywords())); + values.put(DatabaseHelper.COLUMN_ALIASES, listToString(chapter.getAliases())); + + return db.insert(DatabaseHelper.TABLE_CHAPTERS, null, values); + } + + public Chapter getChapterById(String chapterId) { + SQLiteDatabase db = dbHelper.getReadableDatabase(); + Cursor cursor = db.query(DatabaseHelper.TABLE_CHAPTERS, + null, + DatabaseHelper.COLUMN_CHAPTER_ID + " = ?", + new String[]{chapterId}, + null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + Chapter chapter = new Chapter(); + chapter.setChapterId(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_CHAPTER_ID))); + chapter.setCourseId(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_COURSE_ID))); + chapter.setChapterNumber(cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_CHAPTER_NUMBER))); + chapter.setChapterTitle(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_CHAPTER_TITLE))); + chapter.setKeywords(stringToList(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_KEYWORDS)))); + chapter.setAliases(stringToList(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_ALIASES)))); + cursor.close(); + return chapter; + } + + if (cursor != null) { + cursor.close(); + } + return null; + } + + public List getChaptersByCourseId(String courseId) { + List chapters = new ArrayList<>(); + SQLiteDatabase db = dbHelper.getReadableDatabase(); + Cursor cursor = db.query(DatabaseHelper.TABLE_CHAPTERS, + null, + DatabaseHelper.COLUMN_COURSE_ID + " = ?", + new String[]{courseId}, + null, null, DatabaseHelper.COLUMN_CHAPTER_NUMBER + " ASC"); + + if (cursor != null && cursor.moveToFirst()) { + do { + Chapter chapter = new Chapter(); + chapter.setChapterId(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_CHAPTER_ID))); + chapter.setCourseId(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_COURSE_ID))); + chapter.setChapterNumber(cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_CHAPTER_NUMBER))); + chapter.setChapterTitle(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_CHAPTER_TITLE))); + chapter.setKeywords(stringToList(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_KEYWORDS)))); + chapter.setAliases(stringToList(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_ALIASES)))); + chapters.add(chapter); + } while (cursor.moveToNext()); + cursor.close(); + } + + return chapters; + } + + public List getAllChapters() { + List chapters = new ArrayList<>(); + SQLiteDatabase db = dbHelper.getReadableDatabase(); + Cursor cursor = db.query(DatabaseHelper.TABLE_CHAPTERS, null, null, null, null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + do { + Chapter chapter = new Chapter(); + chapter.setChapterId(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_CHAPTER_ID))); + chapter.setCourseId(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_COURSE_ID))); + chapter.setChapterNumber(cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_CHAPTER_NUMBER))); + chapter.setChapterTitle(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_CHAPTER_TITLE))); + chapter.setKeywords(stringToList(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_KEYWORDS)))); + chapter.setAliases(stringToList(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_ALIASES)))); + chapters.add(chapter); + } while (cursor.moveToNext()); + cursor.close(); + } + + return chapters; + } + + // 批量插入课程和章节数据 + public void insertCourseWithChapters(Course course, List chapters) { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + db.beginTransaction(); + try { + insertCourse(course); + for (Chapter chapter : chapters) { + insertChapter(chapter); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + // 辅助方法:将List转换为String(用逗号分隔) + private String listToString(List list) { + if (list == null || list.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < list.size(); i++) { + if (i > 0) { + sb.append(","); + } + sb.append(list.get(i)); + } + return sb.toString(); + } + + // 辅助方法:将String转换为List + private List stringToList(String str) { + List list = new ArrayList<>(); + if (str != null && !str.trim().isEmpty()) { + String[] items = str.split(","); + for (String item : items) { + if (!item.trim().isEmpty()) { + list.add(item.trim()); + } + } + } + return list; + } + + // 关闭数据库连接 + public void close() { + dbHelper.close(); + } +} \ No newline at end of file diff --git a/src/main/java/net/micode/myapplication/repository/NoteRepository.java b/src/main/java/net/micode/myapplication/repository/NoteRepository.java new file mode 100644 index 0000000..66c6f17 --- /dev/null +++ b/src/main/java/net/micode/myapplication/repository/NoteRepository.java @@ -0,0 +1,183 @@ +package net.micode.myapplication.repository; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import net.micode.myapplication.database.DatabaseHelper; +import net.micode.myapplication.model.Note; + +import java.util.ArrayList; +import java.util.List; + +public class NoteRepository { + private DatabaseHelper dbHelper; + + public NoteRepository(Context context) { + this.dbHelper = new DatabaseHelper(context); + } + + public long insertNote(Note note) { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(DatabaseHelper.COLUMN_NOTE_ID, note.getNoteId()); + values.put(DatabaseHelper.COLUMN_TITLE, note.getTitle()); + values.put(DatabaseHelper.COLUMN_DESCRIPTION, note.getDescription()); + values.put(DatabaseHelper.COLUMN_OCR_TEXT, note.getOcrText()); + values.put(DatabaseHelper.COLUMN_COURSE_ID, note.getCourseId()); + values.put(DatabaseHelper.COLUMN_CHAPTER_ID, note.getChapterId()); + values.put(DatabaseHelper.COLUMN_FILE_PATH, note.getFilePath()); + values.put(DatabaseHelper.COLUMN_UPLOAD_TIME, note.getUploadTime()); + values.put(DatabaseHelper.COLUMN_UPDATE_TIME, note.getUpdateTime()); + + return db.insert(DatabaseHelper.TABLE_NOTES, null, values); + } + + public Note getNoteById(String noteId) { + SQLiteDatabase db = dbHelper.getReadableDatabase(); + Cursor cursor = db.query(DatabaseHelper.TABLE_NOTES, + null, + DatabaseHelper.COLUMN_NOTE_ID + " = ?", + new String[]{noteId}, + null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + Note note = cursorToNote(cursor); + cursor.close(); + return note; + } + + if (cursor != null) { + cursor.close(); + } + return null; + } + + public List getAllNotes() { + List notes = new ArrayList<>(); + SQLiteDatabase db = dbHelper.getReadableDatabase(); + Cursor cursor = db.query(DatabaseHelper.TABLE_NOTES, null, null, null, null, null, + DatabaseHelper.COLUMN_UPLOAD_TIME + " DESC"); + + if (cursor != null && cursor.moveToFirst()) { + do { + notes.add(cursorToNote(cursor)); + } while (cursor.moveToNext()); + cursor.close(); + } + + return notes; + } + + public List getNotesByCourse(String courseId) { + List notes = new ArrayList<>(); + SQLiteDatabase db = dbHelper.getReadableDatabase(); + Cursor cursor = db.query(DatabaseHelper.TABLE_NOTES, + null, + DatabaseHelper.COLUMN_COURSE_ID + " = ?", + new String[]{courseId}, + null, null, DatabaseHelper.COLUMN_UPLOAD_TIME + " DESC"); + + if (cursor != null && cursor.moveToFirst()) { + do { + notes.add(cursorToNote(cursor)); + } while (cursor.moveToNext()); + cursor.close(); + } + + return notes; + } + + public List getNotesByChapter(String chapterId) { + List notes = new ArrayList<>(); + SQLiteDatabase db = dbHelper.getReadableDatabase(); + Cursor cursor = db.query(DatabaseHelper.TABLE_NOTES, + null, + DatabaseHelper.COLUMN_CHAPTER_ID + " = ?", + new String[]{chapterId}, + null, null, DatabaseHelper.COLUMN_UPLOAD_TIME + " DESC"); + + if (cursor != null && cursor.moveToFirst()) { + do { + notes.add(cursorToNote(cursor)); + } while (cursor.moveToNext()); + cursor.close(); + } + + return notes; + } + + public int updateNote(Note note) { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(DatabaseHelper.COLUMN_TITLE, note.getTitle()); + values.put(DatabaseHelper.COLUMN_DESCRIPTION, note.getDescription()); + values.put(DatabaseHelper.COLUMN_OCR_TEXT, note.getOcrText()); + values.put(DatabaseHelper.COLUMN_COURSE_ID, note.getCourseId()); + values.put(DatabaseHelper.COLUMN_CHAPTER_ID, note.getChapterId()); + values.put(DatabaseHelper.COLUMN_FILE_PATH, note.getFilePath()); + values.put(DatabaseHelper.COLUMN_UPDATE_TIME, System.currentTimeMillis()); + + return db.update(DatabaseHelper.TABLE_NOTES, values, + DatabaseHelper.COLUMN_NOTE_ID + " = ?", + new String[]{note.getNoteId()}); + } + + public int deleteNote(String noteId) { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + return db.delete(DatabaseHelper.TABLE_NOTES, + DatabaseHelper.COLUMN_NOTE_ID + " = ?", + new String[]{noteId}); + } + + public int deleteNotesByCourse(String courseId) { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + return db.delete(DatabaseHelper.TABLE_NOTES, + DatabaseHelper.COLUMN_COURSE_ID + " = ?", + new String[]{courseId}); + } + + private Note cursorToNote(Cursor cursor) { + Note note = new Note(); + note.setNoteId(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_NOTE_ID))); + note.setTitle(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_TITLE))); + note.setDescription(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_DESCRIPTION))); + note.setOcrText(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_OCR_TEXT))); + note.setCourseId(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_COURSE_ID))); + note.setChapterId(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_CHAPTER_ID))); + note.setFilePath(cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_FILE_PATH))); + note.setUploadTime(cursor.getLong(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_UPLOAD_TIME))); + note.setUpdateTime(cursor.getLong(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_UPDATE_TIME))); + return note; + } + + // 搜索笔记 + public List searchNotes(String query) { + List notes = new ArrayList<>(); + SQLiteDatabase db = dbHelper.getReadableDatabase(); + + String selection = DatabaseHelper.COLUMN_TITLE + " LIKE ? OR " + + DatabaseHelper.COLUMN_DESCRIPTION + " LIKE ? OR " + + DatabaseHelper.COLUMN_OCR_TEXT + " LIKE ?"; + String[] selectionArgs = new String[]{"%" + query + "%", "%" + query + "%", "%" + query + "%"}; + + Cursor cursor = db.query(DatabaseHelper.TABLE_NOTES, + null, selection, selectionArgs, null, null, + DatabaseHelper.COLUMN_UPLOAD_TIME + " DESC"); + + if (cursor != null && cursor.moveToFirst()) { + do { + notes.add(cursorToNote(cursor)); + } while (cursor.moveToNext()); + cursor.close(); + } + + return notes; + } + + // 关闭数据库连接 + public void close() { + dbHelper.close(); + } +} \ No newline at end of file diff --git a/src/main/java/net/micode/myapplication/service/ClassificationService.java b/src/main/java/net/micode/myapplication/service/ClassificationService.java new file mode 100644 index 0000000..54f2dac --- /dev/null +++ b/src/main/java/net/micode/myapplication/service/ClassificationService.java @@ -0,0 +1,261 @@ +package net.micode.myapplication.service; + +import net.micode.myapplication.model.Chapter; +import net.micode.myapplication.model.ClassificationResult; +import net.micode.myapplication.model.Course; +import net.micode.myapplication.model.Note; +import net.micode.myapplication.repository.CourseRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Collections; + +public class ClassificationService { + private CourseRepository courseRepository; + private TextProcessor textProcessor; + + // 分类阈值 + private static final double MIN_CONFIDENCE_THRESHOLD = 0.3; // 最低置信度阈值 + private static final double HIGH_CONFIDENCE_THRESHOLD = 0.7; // 高置信度阈值 + + public ClassificationService(CourseRepository courseRepository) { + this.courseRepository = courseRepository; + this.textProcessor = new TextProcessor(); + } + + /** + * 对笔记进行自动分类 + */ + public ClassificationResult classifyNote(Note note) { + if (note == null) { + return createNoMatchResult("笔记为空"); + } + + // 获取所有课程和章节 + List allCourses = courseRepository.getAllCourses(); + if (allCourses.isEmpty()) { + return createNoMatchResult("没有可用的课程数据"); + } + + // 组合笔记的文本内容 + String noteText = combineNoteText(note); + if (noteText.isEmpty()) { + return createNoMatchResult("笔记内容为空"); + } + + // 预处理文本 + String processedText = textProcessor.preprocessText(noteText); + + // 计算每个章节的匹配分数 + List chapterMatches = new ArrayList<>(); + + for (Course course : allCourses) { + List chapters = courseRepository.getChaptersByCourseId(course.getCourseId()); + + for (Chapter chapter : chapters) { + double score = calculateMatchScore(processedText, chapter); + if (score > 0) { + chapterMatches.add(new ChapterMatch(course, chapter, score)); + } + } + } + + // 如果没有找到匹配的章节 + if (chapterMatches.isEmpty()) { + return createNoMatchResult("没有找到匹配的课程章节"); + } + + // 按分数排序,找到最佳匹配 + Collections.sort(chapterMatches, (a, b) -> Double.compare(b.score, a.score)); + ChapterMatch bestMatch = chapterMatches.get(0); + + // 检查置信度 + if (bestMatch.score < MIN_CONFIDENCE_THRESHOLD) { + return createLowConfidenceResult(bestMatch.course, bestMatch.chapter, bestMatch.score); + } + + // 返回分类结果 + return createClassificationResult(bestMatch.course, bestMatch.chapter, bestMatch.score); + } + + /** + * 计算笔记文本与章节的匹配分数 + */ + private double calculateMatchScore(String noteText, Chapter chapter) { + double totalScore = 0.0; + + // 1. 关键词匹配分数 (权重: 0.5) + double keywordScore = calculateKeywordScore(noteText, chapter); + totalScore += keywordScore * 0.5; + + // 2. 章节标题相似度分数 (权重: 0.3) + double titleScore = calculateTitleSimilarity(noteText, chapter); + totalScore += titleScore * 0.3; + + // 3. 别名匹配分数 (权重: 0.2) + double aliasScore = calculateAliasScore(noteText, chapter); + totalScore += aliasScore * 0.2; + + return totalScore; + } + + /** + * 计算关键词匹配分数 + */ + private double calculateKeywordScore(String noteText, Chapter chapter) { + if (chapter.getKeywords() == null || chapter.getKeywords().isEmpty()) { + return 0.0; + } + + return textProcessor.calculateKeywordMatchScore(noteText, chapter.getKeywords()); + } + + /** + * 计算章节标题相似度分数 + */ + private double calculateTitleSimilarity(String noteText, Chapter chapter) { + if (chapter.getChapterTitle() == null || chapter.getChapterTitle().isEmpty()) { + return 0.0; + } + + // 提取标题中的关键词 + List titleKeywords = textProcessor.extractKeywords(chapter.getChapterTitle(), 5); + + if (titleKeywords.isEmpty()) { + return 0.0; + } + + return textProcessor.calculateKeywordMatchScore(noteText, titleKeywords); + } + + /** + * 计算别名匹配分数 + */ + private double calculateAliasScore(String noteText, Chapter chapter) { + if (chapter.getAliases() == null || chapter.getAliases().isEmpty()) { + return 0.0; + } + + return textProcessor.calculateKeywordMatchScore(noteText, chapter.getAliases()); + } + + /** + * 组合笔记的所有文本内容 + */ + private String combineNoteText(Note note) { + StringBuilder text = new StringBuilder(); + + if (note.getTitle() != null && !note.getTitle().trim().isEmpty()) { + text.append(note.getTitle()).append(" "); + } + + if (note.getDescription() != null && !note.getDescription().trim().isEmpty()) { + text.append(note.getDescription()).append(" "); + } + + if (note.getOcrText() != null && !note.getOcrText().trim().isEmpty()) { + text.append(note.getOcrText()); + } + + return text.toString().trim(); + } + + /** + * 创建分类结果 + */ + private ClassificationResult createClassificationResult(Course course, Chapter chapter, double score) { + ClassificationResult result = new ClassificationResult(); + result.setCourse(course); + result.setChapter(chapter); + result.setConfidenceScore(score); + + String reason; + if (score >= HIGH_CONFIDENCE_THRESHOLD) { + reason = "高置信度匹配:关键词和标题高度相关"; + } else { + reason = "中等置信度匹配:发现相关关键词"; + } + result.setReason(reason); + + return result; + } + + /** + * 创建低置信度结果 + */ + private ClassificationResult createLowConfidenceResult(Course course, Chapter chapter, double score) { + ClassificationResult result = new ClassificationResult(); + result.setCourse(course); + result.setChapter(chapter); + result.setConfidenceScore(score); + result.setReason("低置信度匹配:建议用户确认或手动选择"); + return result; + } + + /** + * 创建无匹配结果 + */ + private ClassificationResult createNoMatchResult(String reason) { + ClassificationResult result = new ClassificationResult(); + result.setConfidenceScore(0.0); + result.setReason(reason); + return result; + } + + /** + * 获取推荐分类列表(前N个最佳匹配) + */ + public List getTopClassifications(Note note, int topN) { + List results = new ArrayList<>(); + + if (note == null) { + return results; + } + + List allCourses = courseRepository.getAllCourses(); + if (allCourses.isEmpty()) { + return results; + } + + String noteText = combineNoteText(note); + String processedText = textProcessor.preprocessText(noteText); + + List chapterMatches = new ArrayList<>(); + + for (Course course : allCourses) { + List chapters = courseRepository.getChaptersByCourseId(course.getCourseId()); + + for (Chapter chapter : chapters) { + double score = calculateMatchScore(processedText, chapter); + if (score > 0) { + chapterMatches.add(new ChapterMatch(course, chapter, score)); + } + } + } + + Collections.sort(chapterMatches, (a, b) -> Double.compare(b.score, a.score)); + + int count = Math.min(topN, chapterMatches.size()); + for (int i = 0; i < count; i++) { + ChapterMatch match = chapterMatches.get(i); + results.add(createClassificationResult(match.course, match.chapter, match.score)); + } + + return results; + } + + /** + * 内部类用于存储章节匹配结果 + */ + private static class ChapterMatch { + Course course; + Chapter chapter; + double score; + + ChapterMatch(Course course, Chapter chapter, double score) { + this.course = course; + this.chapter = chapter; + this.score = score; + } + } +} \ No newline at end of file diff --git a/src/main/java/net/micode/myapplication/service/CourseOutlineImporter.java b/src/main/java/net/micode/myapplication/service/CourseOutlineImporter.java new file mode 100644 index 0000000..25ec23c --- /dev/null +++ b/src/main/java/net/micode/myapplication/service/CourseOutlineImporter.java @@ -0,0 +1,314 @@ +package net.micode.myapplication.service; + +import android.content.Context; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import net.micode.myapplication.model.Chapter; +import net.micode.myapplication.model.Course; +import net.micode.myapplication.repository.CourseRepository; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +public class CourseOutlineImporter { + private static final String TAG = "CourseOutlineImporter"; + private CourseRepository courseRepository; + private Gson gson; + + public CourseOutlineImporter(CourseRepository courseRepository) { + this.courseRepository = courseRepository; + this.gson = new Gson(); + } + + /** + * 从assets文件夹导入课程大纲 + */ + public boolean importCourseOutlineFromAssets(Context context, String fileName) { + try { + InputStream inputStream = context.getAssets().open(fileName); + return importCourseOutline(inputStream); + } catch (IOException e) { + Log.e(TAG, "无法读取assets文件: " + fileName, e); + return false; + } + } + + /** + * 从输入流导入课程大纲 + */ + public boolean importCourseOutline(InputStream inputStream) { + try { + JsonObject jsonObject = JsonParser.parseReader(new InputStreamReader(inputStream)).getAsJsonObject(); + return parseAndImportCourseOutline(jsonObject); + } catch (Exception e) { + Log.e(TAG, "解析JSON失败", e); + return false; + } finally { + try { + inputStream.close(); + } catch (IOException e) { + Log.e(TAG, "关闭输入流失败", e); + } + } + } + + /** + * 解析并导入课程大纲 + */ + private boolean parseAndImportCourseOutline(JsonObject jsonObject) { + try { + // 解析课程基本信息 + Course course = parseCourseInfo(jsonObject); + if (course == null) { + Log.e(TAG, "解析课程信息失败"); + return false; + } + + // 解析章节信息 + List chapters = parseChapters(jsonObject, course.getCourseId()); + if (chapters.isEmpty()) { + Log.w(TAG, "没有找到章节信息"); + } + + // 导入到数据库 + return importToDatabase(course, chapters); + + } catch (Exception e) { + Log.e(TAG, "解析课程大纲失败", e); + return false; + } + } + + /** + * 解析课程基本信息 + */ + private Course parseCourseInfo(JsonObject jsonObject) { + try { + String courseId = getStringValue(jsonObject, "id"); + String title = getStringValue(jsonObject, "title"); + + if (courseId == null || title == null) { + Log.e(TAG, "课程ID或标题为空"); + return null; + } + + Course course = new Course(); + course.setCourseId(courseId); + course.setCourseName(title); + + // 可选字段 + String school = getStringValue(jsonObject, "school"); + if (school != null) { + course.setSchool(school); + } + + return course; + + } catch (Exception e) { + Log.e(TAG, "解析课程基本信息失败", e); + return null; + } + } + + /** + * 解析章节信息 + */ + private List parseChapters(JsonObject jsonObject, String courseId) { + List chapters = new ArrayList<>(); + + try { + JsonArray nodes = jsonObject.getAsJsonArray("nodes"); + if (nodes == null) { + Log.w(TAG, "没有找到nodes数组"); + return chapters; + } + + int chapterNumber = 1; + for (JsonElement element : nodes) { + if (element.isJsonObject()) { + JsonObject chapterObj = element.getAsJsonObject(); + Chapter chapter = parseChapter(chapterObj, courseId, chapterNumber); + if (chapter != null) { + chapters.add(chapter); + chapterNumber++; + } + } + } + + } catch (Exception e) { + Log.e(TAG, "解析章节信息失败", e); + } + + return chapters; + } + + /** + * 解析单个章节 + */ + private Chapter parseChapter(JsonObject chapterObj, String courseId, int chapterNumber) { + try { + String chapterId = getStringValue(chapterObj, "id"); + String chapterTitle = getStringValue(chapterObj, "title"); + + if (chapterId == null || chapterTitle == null) { + Log.w(TAG, "章节ID或标题为空"); + return null; + } + + Chapter chapter = new Chapter(); + chapter.setChapterId(chapterId); + chapter.setCourseId(courseId); + chapter.setChapterNumber(chapterNumber); + chapter.setChapterTitle(chapterTitle); + + // 解析关键词 + List keywords = parseKeywords(chapterObj); + for (String keyword : keywords) { + chapter.addKeyword(keyword); + } + + // 解析别名 + List aliases = parseAliases(chapterObj); + for (String alias : aliases) { + chapter.addAlias(alias); + } + + return chapter; + + } catch (Exception e) { + Log.e(TAG, "解析单个章节失败", e); + return null; + } + } + + /** + * 解析关键词 + */ + private List parseKeywords(JsonObject obj) { + List keywords = new ArrayList<>(); + + try { + JsonArray keywordsArray = obj.getAsJsonArray("keywords"); + if (keywordsArray != null) { + for (JsonElement element : keywordsArray) { + if (element.isJsonPrimitive()) { + keywords.add(element.getAsString()); + } + } + } + } catch (Exception e) { + Log.w(TAG, "解析关键词失败", e); + } + + return keywords; + } + + /** + * 解析别名 + */ + private List parseAliases(JsonObject obj) { + List aliases = new ArrayList<>(); + + try { + JsonArray aliasesArray = obj.getAsJsonArray("aliases"); + if (aliasesArray != null) { + for (JsonElement element : aliasesArray) { + if (element.isJsonPrimitive()) { + aliases.add(element.getAsString()); + } + } + } + } catch (Exception e) { + Log.w(TAG, "解析别名失败", e); + } + + return aliases; + } + + /** + * 获取字符串值 + */ + private String getStringValue(JsonObject obj, String key) { + try { + JsonElement element = obj.get(key); + if (element != null && element.isJsonPrimitive()) { + return element.getAsString(); + } + } catch (Exception e) { + Log.w(TAG, "获取字符串值失败: " + key, e); + } + return null; + } + + /** + * 导入到数据库 + */ + private boolean importToDatabase(Course course, List chapters) { + try { + // 检查课程是否已存在 + Course existingCourse = courseRepository.getCourseById(course.getCourseId()); + if (existingCourse != null) { + Log.i(TAG, "课程已存在,将更新数据: " + course.getCourseId()); + // 可以选择更新或跳过 + } + + // 批量插入课程和章节 + courseRepository.insertCourseWithChapters(course, chapters); + boolean success = true; // 方法没有返回值,假设成功 + + if (success) { + Log.i(TAG, String.format("成功导入课程: %s (%d 个章节)", + course.getCourseName(), chapters.size())); + } else { + Log.e(TAG, "导入课程失败"); + } + + return success; + + } catch (Exception e) { + Log.e(TAG, "导入到数据库失败", e); + return false; + } + } + + /** + * 批量导入多个课程大纲 + */ + public boolean importMultipleCourseOutlines(Context context, List fileNames) { + boolean allSuccess = true; + int successCount = 0; + + for (String fileName : fileNames) { + boolean success = importCourseOutlineFromAssets(context, fileName); + if (success) { + successCount++; + } else { + allSuccess = false; + Log.e(TAG, "导入失败: " + fileName); + } + } + + Log.i(TAG, String.format("批量导入完成: %d/%d 成功", successCount, fileNames.size())); + return allSuccess; + } + + /** + * 获取导入状态信息 + */ + public String getImportStatusMessage(boolean success, String fileName) { + if (success) { + return String.format("成功导入课程大纲: %s", fileName); + } else { + return String.format("导入失败: %s", fileName); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/micode/myapplication/service/OcrService.java b/src/main/java/net/micode/myapplication/service/OcrService.java new file mode 100644 index 0000000..eeea7e4 --- /dev/null +++ b/src/main/java/net/micode/myapplication/service/OcrService.java @@ -0,0 +1,320 @@ +package net.micode.myapplication.service; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.media.ExifInterface; +import android.net.Uri; +import android.util.Log; + +import com.google.mlkit.vision.common.InputImage; +import com.google.mlkit.vision.text.Text; +import com.google.mlkit.vision.text.TextRecognition; +import com.google.mlkit.vision.text.TextRecognizer; +import com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions; + +import com.tom_roush.pdfbox.pdmodel.PDDocument; +import com.tom_roush.pdfbox.rendering.PDFRenderer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +public class OcrService { + private static final String TAG = "OcrService"; + private static final int MAX_IMAGE_SIZE = 2048; // 最大图片尺寸 + private static final int PDF_DPI = 150; // PDF渲染DPI + + private Context context; + private TextRecognizer textRecognizer; + + public OcrService(Context context) { + this.context = context; + // 初始化中文文本识别器 + this.textRecognizer = TextRecognition.getClient(new ChineseTextRecognizerOptions.Builder().build()); + } + + /** + * 从文件提取文本(支持图片和PDF) + */ + public String extractTextFromFile(String filePath) throws Exception { + if (filePath == null || filePath.isEmpty()) { + throw new IllegalArgumentException("文件路径不能为空"); + } + + File file = new File(filePath); + if (!file.exists()) { + throw new IOException("文件不存在: " + filePath); + } + + String fileName = file.getName().toLowerCase(); + + try { + if (fileName.endsWith(".pdf")) { + return extractTextFromPdf(filePath); + } else if (isImageFile(fileName)) { + return extractTextFromImage(filePath); + } else { + throw new UnsupportedOperationException("不支持的文件类型: " + fileName); + } + } catch (Exception e) { + Log.e(TAG, "OCR提取文本失败: " + filePath, e); + throw e; + } + } + + /** + * 从图片文件提取文本 + */ + public String extractTextFromImage(String imagePath) throws Exception { + Bitmap bitmap = loadAndProcessImage(imagePath); + if (bitmap == null) { + throw new IOException("无法加载图片: " + imagePath); + } + + try { + InputImage image = InputImage.fromBitmap(bitmap, 0); + return performTextRecognition(image); + } finally { + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + } + } + + /** + * 从Bitmap提取文本 + */ + public String extractTextFromBitmap(Bitmap bitmap) throws Exception { + if (bitmap == null) { + throw new IllegalArgumentException("Bitmap不能为空"); + } + + try { + InputImage image = InputImage.fromBitmap(bitmap, 0); + return performTextRecognition(image); + } catch (Exception e) { + Log.e(TAG, "从Bitmap提取文本失败", e); + throw e; + } + } + + /** + * 从PDF文件提取文本 + */ + public String extractTextFromPdf(String pdfPath) throws Exception { + PDDocument document = null; + StringBuilder result = new StringBuilder(); + + try { + document = PDDocument.load(new File(pdfPath)); + PDFRenderer renderer = new PDFRenderer(document); + + int pageCount = document.getNumberOfPages(); + Log.i(TAG, "PDF总页数: " + pageCount); + + for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) { + Log.i(TAG, "处理PDF第 " + (pageIndex + 1) + " 页"); + + // 将PDF页面渲染为Bitmap + Bitmap bitmap = renderer.renderImageWithDPI(pageIndex, PDF_DPI); + if (bitmap != null) { + try { + // 从Bitmap提取文本 + String pageText = extractTextFromBitmap(bitmap); + if (pageText != null && !pageText.trim().isEmpty()) { + result.append(pageText); + if (pageIndex < pageCount - 1) { + result.append("\n\n"); // 页间分隔 + } + } + } finally { + if (!bitmap.isRecycled()) { + bitmap.recycle(); + } + } + } + } + + return result.toString().trim(); + + } catch (Exception e) { + Log.e(TAG, "PDF处理失败: " + pdfPath, e); + throw e; + } finally { + if (document != null) { + try { + document.close(); + } catch (IOException e) { + Log.e(TAG, "关闭PDF文档失败", e); + } + } + } + } + + /** + * 执行文本识别 + */ + private String performTextRecognition(InputImage image) throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference result = new AtomicReference<>(""); + final AtomicReference error = new AtomicReference<>(null); + + textRecognizer.process(image) + .addOnSuccessListener(visionText -> { + try { + String extractedText = processVisionText(visionText); + result.set(extractedText); + } catch (Exception e) { + error.set(e); + } finally { + latch.countDown(); + } + }) + .addOnFailureListener(e -> { + error.set(e); + latch.countDown(); + }); + + // 等待识别完成(最多30秒) + if (!latch.await(30, TimeUnit.SECONDS)) { + throw new Exception("文本识别超时"); + } + + if (error.get() != null) { + throw error.get(); + } + + return result.get(); + } + + /** + * 处理Vision Text结果 + */ + private String processVisionText(Text visionText) { + StringBuilder result = new StringBuilder(); + + for (Text.TextBlock block : visionText.getTextBlocks()) { + for (Text.Line line : block.getLines()) { + result.append(line.getText()).append("\n"); + } + result.append("\n"); // 块间分隔 + } + + return result.toString().trim(); + } + + /** + * 加载并处理图片 + */ + private Bitmap loadAndProcessImage(String imagePath) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(imagePath, options); + + // 计算缩放比例 + int scaleFactor = calculateInSampleSize(options, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE); + + options.inJustDecodeBounds = false; + options.inSampleSize = scaleFactor; + + Bitmap bitmap = BitmapFactory.decodeFile(imagePath, options); + if (bitmap == null) { + return null; + } + + // 处理图片方向 + bitmap = handleImageOrientation(bitmap, imagePath); + + return bitmap; + } + + /** + * 计算图片缩放比例 + */ + private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + final int halfHeight = height / 2; + final int halfWidth = width / 2; + + while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2; + } + } + + return inSampleSize; + } + + /** + * 处理图片方向 + */ + private Bitmap handleImageOrientation(Bitmap bitmap, String imagePath) { + try { + ExifInterface exif = new ExifInterface(imagePath); + int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL); + + Matrix matrix = new Matrix(); + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + matrix.postRotate(90); + break; + case ExifInterface.ORIENTATION_ROTATE_180: + matrix.postRotate(180); + break; + case ExifInterface.ORIENTATION_ROTATE_270: + matrix.postRotate(270); + break; + default: + return bitmap; + } + + Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), + bitmap.getHeight(), matrix, true); + + // 回收原始bitmap + if (rotatedBitmap != bitmap && !bitmap.isRecycled()) { + bitmap.recycle(); + } + + return rotatedBitmap; + + } catch (IOException e) { + Log.e(TAG, "处理图片方向失败", e); + return bitmap; + } + } + + /** + * 检查是否为图片文件 + */ + private boolean isImageFile(String fileName) { + String[] imageExtensions = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".gif"}; + String lowerFileName = fileName.toLowerCase(); + + for (String ext : imageExtensions) { + if (lowerFileName.endsWith(ext)) { + return true; + } + } + return false; + } + + /** + * 关闭OCR服务 + */ + public void close() { + if (textRecognizer != null) { + textRecognizer.close(); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/micode/myapplication/service/TextProcessor.java b/src/main/java/net/micode/myapplication/service/TextProcessor.java new file mode 100644 index 0000000..8518c8d --- /dev/null +++ b/src/main/java/net/micode/myapplication/service/TextProcessor.java @@ -0,0 +1,218 @@ +package net.micode.myapplication.service; + +import com.hankcs.hanlp.HanLP; +import com.hankcs.hanlp.seg.common.Term; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.regex.Pattern; + +public class TextProcessor { + + // 常见停用词列表 + private static final String[] STOP_WORDS = { + "的", "了", "在", "是", "我", "有", "和", "就", "不", "人", "都", "一", "一个", "上", "也", "很", "到", "说", "要", "去", "你", "会", "着", "没有", "看", "好", "自己", "这" + }; + + // 标点符号正则表达式 + private static final Pattern PUNCTUATION_PATTERN = Pattern.compile("[\\p{Punct}\\s]+"); + + /** + * 预处理文本:去除标点符号、转换为小写、去除多余空格 + */ + public String preprocessText(String text) { + if (text == null || text.trim().isEmpty()) { + return ""; + } + + // 去除标点符号和特殊字符 + text = PUNCTUATION_PATTERN.matcher(text).replaceAll(" "); + + // 去除多余空格 + text = text.replaceAll("\\s+", " ").trim(); + + return text.toLowerCase(); + } + + /** + * 提取关键词:使用HanLP进行分词并过滤停用词 + */ + public List extractKeywords(String text, int maxKeywords) { + List keywords = new ArrayList<>(); + + if (text == null || text.trim().isEmpty()) { + return keywords; + } + + try { + // 使用HanLP进行分词 + List terms = HanLP.segment(text); + + // 统计词频 + Map wordFrequency = new HashMap<>(); + for (Term term : terms) { + String word = term.word.trim(); + if (word.length() >= 2 && !isStopWord(word)) { // 只保留长度>=2的词 + Integer count = wordFrequency.get(word); + wordFrequency.put(word, count == null ? 1 : count + 1); + } + } + + // 按词频排序并返回前maxKeywords个词 + List> entries = new ArrayList<>(wordFrequency.entrySet()); + Collections.sort(entries, new Comparator>() { + @Override + public int compare(Map.Entry a, Map.Entry b) { + return b.getValue().compareTo(a.getValue()); + } + }); + for (int i = 0; i < Math.min(maxKeywords, entries.size()); i++) { + keywords.add(entries.get(i).getKey()); + } + + } catch (Exception e) { + e.printStackTrace(); + // 如果HanLP失败,使用简单的空格分割 + String[] words = text.split("\\s+"); + for (String word : words) { + if (word.length() >= 2 && !isStopWord(word)) { + keywords.add(word); + if (keywords.size() >= maxKeywords) { + break; + } + } + } + } + + return keywords; + } + + /** + * 提取文本特征:包括关键词、词频等 + */ + public Map extractTextFeatures(String text) { + Map features = new HashMap<>(); + + if (text == null || text.trim().isEmpty()) { + return features; + } + + String processedText = preprocessText(text); + List keywords = extractKeywords(processedText, 20); + + // 计算词频 + Map wordFrequency = new HashMap<>(); + String[] words = processedText.split("\\s+"); + for (String word : words) { + if (word.length() >= 2 && !isStopWord(word)) { + Integer count = wordFrequency.get(word); + wordFrequency.put(word, count == null ? 1 : count + 1); + } + } + + features.put("text", processedText); + features.put("keywords", keywords); + features.put("wordFrequency", wordFrequency); + features.put("wordCount", words.length); + features.put("uniqueWordCount", wordFrequency.size()); + + return features; + } + + /** + * 计算两个文本的相似度(基于关键词重叠) + */ + public double calculateTextSimilarity(String text1, String text2) { + if (text1 == null || text2 == null || text1.isEmpty() || text2.isEmpty()) { + return 0.0; + } + + List keywords1 = extractKeywords(preprocessText(text1), 10); + List keywords2 = extractKeywords(preprocessText(text2), 10); + + if (keywords1.isEmpty() || keywords2.isEmpty()) { + return 0.0; + } + + // 计算交集 + List intersection = new ArrayList<>(keywords1); + intersection.retainAll(keywords2); + + // 计算并集 + List union = new ArrayList<>(keywords1); + for (String keyword : keywords2) { + if (!union.contains(keyword)) { + union.add(keyword); + } + } + + // Jaccard相似度 + return (double) intersection.size() / union.size(); + } + + /** + * 计算关键词匹配分数 + */ + public double calculateKeywordMatchScore(String text, List targetKeywords) { + if (text == null || targetKeywords == null || targetKeywords.isEmpty()) { + return 0.0; + } + + String processedText = preprocessText(text); + List textKeywords = extractKeywords(processedText, 50); // 提取更多关键词用于匹配 + + int matchCount = 0; + for (String targetKeyword : targetKeywords) { + String processedTarget = preprocessText(targetKeyword); + if (textKeywords.contains(processedTarget) || processedText.contains(processedTarget)) { + matchCount++; + } + } + + return (double) matchCount / targetKeywords.size(); + } + + /** + * 判断是否为停用词 + */ + private boolean isStopWord(String word) { + if (word == null || word.trim().isEmpty()) { + return true; + } + + for (String stopWord : STOP_WORDS) { + if (stopWord.equals(word)) { + return true; + } + } + return false; + } + + /** + * 提取文本中的数字信息(章节号等) + */ + public List extractNumbers(String text) { + List numbers = new ArrayList<>(); + if (text == null || text.trim().isEmpty()) { + return numbers; + } + + Pattern pattern = Pattern.compile("\\d+"); + java.util.regex.Matcher matcher = pattern.matcher(text); + + while (matcher.find()) { + try { + numbers.add(Integer.parseInt(matcher.group())); + } catch (NumberFormatException e) { + // 忽略解析错误的数字 + } + } + + return numbers; + } +} \ No newline at end of file diff --git a/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2d74dda --- /dev/null +++ b/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/card_background.xml b/src/main/res/drawable/card_background.xml new file mode 100644 index 0000000..c070496 --- /dev/null +++ b/src/main/res/drawable/card_background.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_image.xml b/src/main/res/drawable/ic_image.xml new file mode 100644 index 0000000..478fa68 --- /dev/null +++ b/src/main/res/drawable/ic_image.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_launcher_background.xml b/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..11d33ce --- /dev/null +++ b/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/src/main/res/drawable/ic_lock.xml b/src/main/res/drawable/ic_lock.xml new file mode 100644 index 0000000..fe89e5b --- /dev/null +++ b/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_person.xml b/src/main/res/drawable/ic_person.xml new file mode 100644 index 0000000..66e42d4 --- /dev/null +++ b/src/main/res/drawable/ic_person.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_person_outline.xml b/src/main/res/drawable/ic_person_outline.xml new file mode 100644 index 0000000..79aee65 --- /dev/null +++ b/src/main/res/drawable/ic_person_outline.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_search.xml b/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..5deeb03 --- /dev/null +++ b/src/main/res/drawable/ic_search.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_upload.xml b/src/main/res/drawable/ic_upload.xml new file mode 100644 index 0000000..5de5ef7 --- /dev/null +++ b/src/main/res/drawable/ic_upload.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/input_background.xml b/src/main/res/drawable/input_background.xml new file mode 100644 index 0000000..50d3c96 --- /dev/null +++ b/src/main/res/drawable/input_background.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/spinner_background.xml b/src/main/res/drawable/spinner_background.xml new file mode 100644 index 0000000..9baf759 --- /dev/null +++ b/src/main/res/drawable/spinner_background.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/stat_card_accent.xml b/src/main/res/drawable/stat_card_accent.xml new file mode 100644 index 0000000..837c246 --- /dev/null +++ b/src/main/res/drawable/stat_card_accent.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/stat_card_primary.xml b/src/main/res/drawable/stat_card_primary.xml new file mode 100644 index 0000000..788a0b8 --- /dev/null +++ b/src/main/res/drawable/stat_card_primary.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/stat_card_success.xml b/src/main/res/drawable/stat_card_success.xml new file mode 100644 index 0000000..c25661b --- /dev/null +++ b/src/main/res/drawable/stat_card_success.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/layout/activity_login.xml b/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..481fe0e --- /dev/null +++ b/src/main/res/layout/activity_login.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + +