简介这份资源面向Android初学者与移动开发课程学习者提供一套可直接运行的「学生成绩管理系统」完整工程用于练习SQLite本地存储与Java业务逻辑的配合。系统围绕学生信息与科目成绩的增删改查展开涵盖SQLiteOpenHelper建表与版本管理、RecyclerView列表展示、按学号姓名条件查询及按总分排序等典型功能并涉及异常处理与CursorLoader异步加载等性能优化思路适合作为课程设计或毕业设计的参考原型。压缩包共1641个文件约18.93MB以class与dex编译产物、json与xml配置、java源码及jar依赖为主另含apk安装包、gradle构建脚本与图片资源完整保留了工程结构与构建痕迹。目前已有4243人学习。读者可从中获得数据模型设计、UI布局与数据库交互的完整实现路径对照源码理解CRUD落地方式并借鉴查询排序与数据安全的处理思路。1. 从一份能跑通的 Android 成绩管理源码说起期末周前一周某高校实验室的 A 同学抱着一台旧笔记本找我说他课程设计选了「学生成绩管理系统」导师要求必须能装到真机上演示还要能说清楚数据是怎么存的。他手里有一份从网上扒下来的 Android 工程打开一看Activity 里直接拼 SQL 字符串成绩表连主键都没设删一条记录能把整个班的成绩带走。这不是个例——Android SQLite Java 这套组合看起来门槛低真正落地时翻车的地方全在细节里。这份资源就是围绕「学生成绩管理系统」这个经典课程设计场景用 Java SQLite 原生 API 搭一套能编译、能演示、能讲清原理的完整工程适合正在做课程设计的学生、需要快速交付一个本地数据管理 Demo 的开发者以及想搞明白 SQLiteOpenHelper 到底该怎么用的人。它解决的不是「能不能跑」而是「跑起来之后数据对不对、删改会不会误伤、换台机器还能不能复现」。2. 拆开工程看骨架Activity、SQLiteOpenHelper 与数据模型怎么分工拿到一份 Android 成绩管理源码第一件事不是急着点运行而是先看目录结构判断它是不是「能改得动」的工程。很多网上流传的版本把所有逻辑塞进一个 MainActivity几千行代码里既画界面又写 SQL这种结构演示能过但导师一问「你的数据层在哪」就露馅。合理的分层应该是实体类负责字段定义SQLiteOpenHelper 子类负责建库建表和版本迁移DAO 层封装增删改查Activity 只做界面绑定和事件响应。这套分工不是学院派洁癖而是因为 SQLite 在 Android 上是进程内嵌数据库没有独立服务进程任何一处 SQL 写错都会直接抛异常到 UI 线程分层能让你在 DAO 层就把错误拦住。2.1 实体类与表结构的字段映射先看实体类。学生成绩这个场景核心就两张表学生表和成绩表。学生表存学号、姓名、班级成绩表存学号、课程名、分数。这里有个新手常犯的错——把学生姓名直接冗余进成绩表觉得查询方便结果改个名字要同步更新几十条成绩记录一旦漏改就出现「同一个人两个名字」。正确做法是成绩表只存学号作为外键关联姓名通过 JOIN 或二次查询拿。实体类字段和表字段一一对应类型也要对齐SQLite 没有严格的类型约束你声明 INTEGER 它也能塞字符串进去所以 Java 侧的类型转换必须在 DAO 层做掉不能指望数据库帮你兜底。// Student.java —— 实体类只做字段承载不掺业务逻辑 public class Student { private String studentId; // 学号业务主键用 TEXT 存避免前导零丢失 private String name; // 姓名 private String className; // 班级 public Student(String studentId, String name, String className) { this.studentId studentId; this.name name; this.className className; } // getter/setter 省略实际工程里建议用 IDE 生成 }这段代码没什么花哨的但学号用 String 而不是 int 是关键决策。很多学校的学号带前导零或者字母前缀用 int 存会在插入时被截断查询时对不上号这种坑在演示现场被导师随手输一个「001」就能触发。字段命名用驼峰建表时转下划线这是 Java 侧和 SQL 侧的约定后面写 DAO 时能少一层心智负担。2.2 SQLiteOpenHelper 的建表与版本管理SQLiteOpenHelper 是整个工程的基石它管两件事第一次打开时建库建表以及数据库版本升级时做迁移。onCreate 只在数据库文件不存在时调用一次onUpgrade 在版本号变大时调用。我见过太多工程把建表语句写在 Activity 的 onCreate 里每次启动都执行一遍 CREATE TABLE IF NOT EXISTS看似无害实则把版本管理彻底废掉了——将来加字段时你没有任何钩子可用。public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME grade.db; private static final int DB_VERSION 1; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { // 学生表学号为主键TEXT 类型保留前导零 db.execSQL(CREATE TABLE student ( student_id TEXT PRIMARY KEY, name TEXT NOT NULL, class_name TEXT)); // 成绩表自增主键 学号外键分数限制 0-100 db.execSQL(CREATE TABLE score ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, course TEXT NOT NULL, score REAL CHECK(score 0 AND score 100), FOREIGN KEY(student_id) REFERENCES student(student_id))); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 版本 1 到 2 的迁移示例给成绩表加备注字段 if (oldVersion 2) { db.execSQL(ALTER TABLE score ADD COLUMN remark TEXT); } } }建表语句里有两个值得说的点。第一成绩表用了自增主键 id而不是拿「学号课程」做联合主键原因是同一学生同一课程可能存在补考记录联合主键会挡住第二次插入自增 id 留出了余地。第二score 字段加了 CHECK 约束把 0 到 100 的范围卡在数据库层这样即使 UI 层校验漏了脏数据也进不来。注意 SQLite 的外键约束默认是关闭的需要在 onOpen 里执行db.execSQL(PRAGMA foreign_keys ON)才生效这一点后面避坑章节会展开。onUpgrade 里用 oldVersion 判断而不是直接 DROP 重建是因为真实场景下用户数据不能丢课程设计演示时导师很可能问你「加个字段怎么办」这段代码就是答案。2.3 DAO 层封装把 SQL 关进笼子DAO 层的价值在于让 Activity 永远不碰 SQL 字符串。每个方法对应一个业务动作参数用实体类或基本类型传返回值用 List 或单个对象。这样做的直接好处是SQL 注入风险被收敛到一处Cursor 的关闭有统一出口将来换 Room 或换网络存储时只改这一层。public class ScoreDao { private final DBHelper helper; public ScoreDao(Context context) { this.helper new DBHelper(context); } // 插入成绩返回新行 id-1 表示失败 public long insertScore(String studentId, String course, double score) { SQLiteDatabase db helper.getWritableDatabase(); ContentValues cv new ContentValues(); cv.put(student_id, studentId); cv.put(course, course); cv.put(score, score); // 用 insert 而非 execSQL 拼串参数由框架转义 return db.insert(score, null, cv); } // 按学号查所有成绩游标用完必须关 public ListString queryByStudent(String studentId) { ListString result new ArrayList(); SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.query(score, new String[]{course, score}, student_id ?, new String[]{studentId}, null, null, course ASC); try { while (cursor.moveToNext()) { result.add(cursor.getString(0) : cursor.getDouble(1)); } } finally { cursor.close(); // 不关游标会泄漏长时间运行后查询变慢 } return result; } }insertScore 用 ContentValues 而不是字符串拼接这是防注入的基本功也是 SQLite 官方推荐的写法。queryByStudent 里 Cursor 放在 try-finally 中关闭是因为 Cursor 持有底层文件句柄忘记关在低端机上跑几十次查询就会触发「Too many open files」。查询条件用占位符?配合 selectionArgs而不是把 studentId 直接拼进 where 子句同样是为了安全和转义。排序用course ASC让结果稳定演示时列表顺序不会每次刷新都变这种细节导师不一定夸但乱序一定会被问。3. 从建库到界面联动一套可复现的落地步骤骨架看清之后真正动手要把工程跑起来还得走一遍环境配置、数据初始化和界面绑定的完整链路。这一章按「新建工程 → 建库 → 灌数据 → 列表展示 → 增删改」的顺序推进每一步都给出可抄的配置和代码重点讲清楚参数为什么这么设、失败时先看哪里。Android 的版本兼容是个绕不开的话题minSdk 设太低用不了新 API设太高旧设备装不上课程设计场景下建议 minSdk 21、targetSdk 按当前主流走这样模拟器和真机都能覆盖。3.1 工程配置与依赖的最小集用 Android Studio 新建 Empty Activity 工程语言选 Java包名自己定。build.gradle 里不需要引入任何第三方库SQLite 是系统内置的这也是这套方案在课程设计里吃香的原因——不用配仓库、不用等下载、断网也能编译。唯一要确认的是 compileSdk 和 targetSdk 保持一致避免运行时权限和行为变更带来的意外。// app/build.gradle 关键片段 android { compileSdk 34 defaultConfig { applicationId com.example.grademanager minSdk 21 // 覆盖绝大多数在用设备 targetSdk 34 versionCode 1 versionName 1.0 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }minSdk 21 是个务实的选择再低会碰到一些 API 缺失再高会挡掉一部分旧模拟器镜像。Java 8 的语法支持在 AGP 新版本里默认开启但显式写出来能避免换机器后编译报错。这里没有加任何依赖意味着工程体积小、构建快答辩现场用别人的电脑 clone 下来也能直接跑这种「零依赖可复现」在演示场景里比任何花哨功能都值钱。3.2 首次启动的数据初始化策略空数据库打开界面是一片空白演示效果很差。常见做法是在 DBHelper 的 onCreate 里插入几条种子数据但要注意 onCreate 只在建库时触发一次如果你调试时反复卸载重装没问题可一旦只清数据不删库种子就不会再灌。更稳妥的方式是单独写一个初始化方法在 Application 或首个 Activity 启动时判断表是否为空为空才插入。// 在 MainActivity.onCreate 中调用 private void initSeedDataIfEmpty() { ScoreDao dao new ScoreDao(this); if (dao.countAll() 0) { // 表空才灌避免重复插入 dao.insertScore(2023001, 高等数学, 88.5); dao.insertScore(2023001, 大学英语, 76.0); dao.insertScore(2023002, 高等数学, 92.0); } }countAll 的实现就是一条SELECT COUNT(*) FROM score返回 int。判断为 0 才插入保证多次启动不会累积重复记录。种子数据选两三个学生、两三门课覆盖「同一学生多门课」和「同一课程多人」两种查询场景演示时切换学号就能看到不同结果比只放一条数据有说服力。分数故意用 88.5 这种带小数的值是为了验证 REAL 类型和显示格式整数分数容易掩盖精度问题。3.3 ListView 绑定与增删改的界面联动界面层用 ListView SimpleAdapter 就够不必上 RecyclerView课程设计的重点是数据链路不是 UI 框架。关键在于每次数据变更后要重新查询并 notifyDataSetChanged否则界面和数据库会脱节——这是新手最常见的「改了没反应」问题。private void refreshList() { ListMapString, String items new ArrayList(); for (String line : scoreDao.queryAllFormatted()) { MapString, String row new HashMap(); row.put(line, line); items.add(row); } SimpleAdapter adapter new SimpleAdapter(this, items, android.R.layout.simple_list_item_1, new String[]{line}, new int[]{android.R.id.text1}); listView.setAdapter(adapter); } // 删除按钮回调里 int rows scoreDao.deleteById(selectedId); if (rows 0) { refreshList(); // 删除成功后必须刷新否则列表还显示旧数据 }refreshList 每次都重建 adapter数据量小的时候没问题数据上千条时会有卡顿进阶做法是复用 adapter 只更新数据源这一点在最后一章会提。deleteById 返回受影响行数大于 0 才刷新这样删除失败时界面不会误报成功。selectedId 来自 ListView 的 onItemClick 回调里记录的 position 对应的数据库 id注意是数据库主键不是列表下标两者在排序变化后会错位这个坑后面单独说。4. 避坑与排查五个让演示当场翻车的细节代码能编译不代表能演示SQLite 在 Android 上的很多行为跟桌面版不一样下面这几条都是我在帮人看工程时反复遇到的真实翻车点按「现象 → 原因 → 解决」拆开写照着排查能省掉大量试错时间。4.1 外键约束写了却没生效现象成绩表里插入了不存在的学号数据库照单全收查询时出现「孤儿成绩」。原因SQLite 的外键约束默认关闭建表时写了 FOREIGN KEY 只是声明不执行PRAGMA foreign_keys ON就不会真正校验。解决在 DBHelper 的 onOpen 方法里执行该 PRAGMA注意 onOpen 每次打开数据库都会调用适合放这种会话级设置。Override public void onOpen(SQLiteDatabase db) { super.onOpen(db); if (!db.isReadOnly()) { db.execSQL(PRAGMA foreign_keys ON); } }4.2 主线程查询导致界面卡顿甚至 ANR现象数据量到几千条后点查询按钮界面冻结一两秒严重时弹「应用无响应」。原因SQLite 查询是同步 IO放在主线程会阻塞 UI 渲染。解决小数据量课程设计可以接受但要心里有数数据量上去后用 ExecutorService 或 AsyncTask已废弃但课程设计仍常见把查询挪到子线程查完再回主线程更新 UI。演示前先用几千条数据压一遍能提前暴露这个问题。4.3 用列表下标当数据库 id 删除现象按课程名排序后删第二条结果删掉的是另一条记录。原因ListView 的 position 是当前显示顺序的下标排序或过滤后与数据库主键不再对应。解决adapter 的数据项里带上数据库 idonItemClick 时取 id 而不是 position删除用 id 定位。这个错误在演示时特别隐蔽因为第一次点往往是对的换个排序就错位。4.4 数据库版本号改了但没写迁移逻辑现象给实体加了字段、改了 DB_VERSION装上新版后一查询就崩报「no such column」。原因版本号变大触发 onUpgrade但方法体是空的或者只写了 DROP TABLE旧数据被清空或表结构没更新。解决onUpgrade 里按 oldVersion 逐版本写 ALTER TABLE绝不 DROP 重建调试阶段可以卸载重装绕过但交付版本必须写对迁移。4.5 忘记关闭 Cursor 和数据库连接现象反复查询几十次后应用变慢Logcat 出现资源相关警告。原因Cursor 和 SQLiteDatabase 都持有底层资源不关闭会累积泄漏。解决Cursor 用 try-finally 关闭SQLiteDatabase 一般由 SQLiteOpenHelper 统一管理不要每次手动 close否则下次 getWritableDatabase 又要重开反而更慢。记住「Cursor 必关、Database 交给 Helper」这条原则即可。5. 进阶验证用事务和批量操作把数据一致性钉死基础功能跑通之后真正区分「能演示」和「敢交付」的是数据一致性。成绩管理里最典型的场景是批量导入一个班几十条成绩逐条 insert 中途失败会留下半截数据查也不是删也不是。SQLite 的事务机制就是为这种场景准备的把一批操作包在 beginTransaction 和 setTransactionSuccessful 之间任何一条抛异常就整体回滚要么全进要么全不进。下面这段是批量导入的写法注意 endTransaction 必须放在 finally 里否则异常路径下事务不会结束后续操作会一直卡在未提交状态。public int batchInsert(Listdouble[] records, String studentId) { SQLiteDatabase db helper.getWritableDatabase(); db.beginTransaction(); int success 0; try { for (double[] r : records) { ContentValues cv new ContentValues(); cv.put(student_id, studentId); cv.put(course, 课程 (int) r[0]); cv.put(score, r[1]); if (db.insert(score, null, cv) ! -1) success; } db.setTransactionSuccessful(); // 只有走到这里才真正提交 } finally { db.endTransaction(); // 无论成败都要结束事务 } return success; }批量插入用事务的另一个好处是速度。SQLite 默认每次 insert 都走一次磁盘同步几十条数据能明显感觉到卡包进事务后只在提交时同步一次实测能快一个数量级。验证事务是否生效有个土办法故意在循环中间抛个异常看数据库里是不是一条都没进去进了就说明事务没包对。这个验证动作我每次交付前都会做一遍比看代码可靠。再往上一层是查询性能。成绩管理常见的「按班级查平均分」「按课程排名」这类聚合查询用 SQL 的 GROUP BY 和 AVG 在数据库层算完再返回比把全表捞到 Java 里循环快得多也省内存。索引方面student_id 和 course 上各建一个普通索引查询计划会从全表扫描变成索引查找几千条数据下差异肉眼可见。建索引的语句放在 onCreate 里跟建表一起执行db.execSQL(CREATE INDEX idx_score_student ON score(student_id)); db.execSQL(CREATE INDEX idx_score_course ON score(course));索引不是越多越好每个索引都会拖慢写入并占空间成绩表这种读多写少的场景加这两个足够。验证索引是否被用上可以在查询前执行EXPLAIN QUERY PLAN看输出里有没有USING INDEX没有就说明条件写法让索引失效了比如对字段做了函数运算或者用了 LIKE %xx 前缀通配。最后说一个演示技巧答辩前把数据库文件从设备里 pull 出来用任意 SQLite 客户端打开当着导师的面查一遍表结构和数据比在手机上点来点去更有说服力。命令是adb shell run-as 包名 cat databases/grade.db local.db前提是应用是 debuggable 的。这一步能证明数据真的落盘了不是内存里造的假象。从那以后我每次交付带本地存储的工程都会强制走一遍「pull 库 → 外部工具打开 → 核对表结构」的流程翻车次数直线下降。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
C++算法学习路线与核心知识体系详解 1. 为什么C算法学习需要方向性规划 作为从大学ACM竞赛一路走来的老选手,我见过太多初学者在C算法学习上踩坑。最常见的就是盲目刷题,看到什么学什么,最后知识点零散不成体系。这种学习方式效率极低,往往投入上百小时却收效甚微。 … · 2026/11/5 8:18:03
金融行业智能运维(AIOps)架构设计与实战 1. 智能运维在金融数字化转型中的核心价值 金融行业作为数字化转型的先行者,面临着前所未有的系统稳定性与效能挑战。我曾在某头部券商负责智能运维体系建设,深刻体会到传统运维模式在应对高频交易、实时清算等场景时的力不从心。当系统日交易量突破百亿… · 2026/11/5 8:17:03
服务器安全防护:核心威胁分析与加固实战指南 1. 服务器安全现状与核心威胁分析 服务器作为现代互联网基础设施的核心组件,每天面临着数以万计的安全威胁。根据我过去十年运维经验,即使是最基础的企业官网服务器,平均每天也会遭遇50-100次自动化扫描和试探性攻击。这些攻击行为呈现出明显… · 2026/11/5 8:17:03
MySQL批量修改表字段的实用技巧与最佳实践 1. 为什么需要批量修改MySQL表字段 在日常数据库维护中,我们经常会遇到需要批量修改表字段的情况。比如最近我在处理一个电商系统的数据库升级时,就遇到了需要统一修改二十多张表中相同前缀的字段名和数据类型的问题。如果手动一个个修改,不仅… · 2026/11/5 8:25:07
Visio画布三层次模型:绘图页、页面与视图的工程化控制 1. 这不是“改尺寸”,而是重新定义绘图空间的底层逻辑很多人搜“visio怎么调整画布大小”,点开教程就照着点“设计→大小→其他页面大小”一顿操作,结果发现:文字缩了、形状变形了、打印出来一半内容被裁掉——问题没解决… · 2026/11/5 8:25:07
SVDD多分类算法改造与工业质检应用实践 1. 项目背景与核心价值 SVDD(Support Vector Data Description)作为一类经典的单分类算法,在异常检测领域已有成熟应用。但将其扩展为多类分类器,面临着核心算法逻辑与多分类需求间的本质矛盾。这个项目尝试突破传统"一对一&… · 2026/11/5 8:25:07
锂电池裁切喷码产线PLC控制与工艺优化 1. 锂电池裁切喷码产线概述在锂电池制造过程中,裁切和喷码是两道关键工序。裁切工序负责将大卷的锂电池极片材料按照设计要求切割成特定尺寸,而喷码工序则需要在极片或电池表面标记产品信息、生产批次等关键数据。这两道工序的精度和稳定性直接影响到锂电… · 2026/11/5 8:25:07
Matplotlib 散点图逐点标注文本的实现方式 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 8:25:07
Go安全测试工具工程化:并发超时、字节解析与限速审计 1. 先把“黑帽子”这三个字摆到台面上:Go 在安全测试里真正吃香的三件事前阵子帮一个做内部资产盘点的团队看代码,他们用某个脚本语言写的探测工具,扫完一个 /16 的网段要跑将近四个小时,中间还因为网络抖动卡死在某个地址上&… · 2026/11/5 8:23:06
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38