当前位置: 首页 > news >正文

Python实战:从零构建学生管理系统,掌握CRUD与数据持久化

1. 项目概述:为什么从学生管理系统开始你的Python实战?

如果你刚学完Python基础语法,面对着一堆变量、循环和函数,心里可能正犯嘀咕:这些知识到底能用来做什么?我学的东西是不是太“玩具”了?别急,这种感觉每个开发者都经历过。从“知道”到“会用”,中间隔着的就是“项目实战”。而“学生管理系统”,几乎就是为这个过渡阶段量身定做的练手项目。

它听起来简单,不就是增删改查吗?但恰恰是这份“简单”,让它成为了检验你Python基础是否扎实的绝佳试金石。你需要综合运用变量、数据类型、条件判断、循环、函数,甚至文件操作或数据库连接。更重要的是,它能帮你建立起一个完整的、有逻辑的编程思维:如何设计数据结构来存储学生信息?如何组织代码让功能清晰可维护?如何处理用户的异常输入?这些思考,远比单纯敲出几行“Hello World”要重要得多。

我见过太多新手卡在理论到实践的这一步。有人觉得管理系统太“low”,想直接去搞爬虫、做AI,结果基础不牢,遇到问题连调试都无从下手。其实,把学生管理系统这个“麻雀”解剖明白,五脏六腑的功能和关联都理清了,你再去接触任何复杂的系统,心里都会有底。这就是我建议你从这里开始的原因——它不炫酷,但足够让你脚踏实地地入门项目开发。

2. 系统核心需求与功能模块拆解

在动手写代码之前,我们必须先想清楚这个系统要干什么。盲目开干,最后代码肯定会变成一团乱麻。一个好的学生管理系统,核心是围绕“学生”这个实体,提供一套完整的数据管理能力。

2.1 核心数据模型定义

首先,我们需要抽象出一个“学生”应该包含哪些信息。这决定了我们程序内部如何表示一个学生对象。一个基础的学生模型通常包括:

  • 学号:唯一标识符,通常是字符串,用于精确查找。
  • 姓名:字符串类型。
  • 性别:可以用字符串(‘男‘/’女‘)或整数(0/1)表示。
  • 年龄:整数类型。
  • 班级:字符串类型,如“计算机科学与技术2023级1班”。
  • 成绩:这可能是一个单科成绩(如数学成绩),也可能是一个字典,包含多门课程的成绩,例如{‘数学‘: 90, ‘英语‘: 85}

在Python中,我们可以用多种方式来表示这个模型。最基础的是使用字典,例如student = {‘id‘: ‘1001‘, ‘name‘: ‘张三‘, ‘age‘: 20}。但更推荐使用面向对象的思想,定义一个Student类。这样做的好处是结构清晰,可以封装相关的方法(如计算平均分),也是后续学习更高级框架的必经之路。

2.2 四大基础功能模块

围绕这个数据模型,我们可以拆解出四个最核心的功能模块,也就是常说的CRUD

  1. 新增学生信息:提供界面让用户输入学生的各项信息,并将这个新的学生对象保存到我们的存储系统中(可能是内存列表、文件或数据库)。
  2. 查询学生信息:这是使用频率最高的功能。需要支持多种查询方式:
    • 按学号精确查询:快速定位特定学生。
    • 按姓名模糊查询:可能返回多个结果。
    • 列出所有学生:以表格形式清晰展示。
  3. 修改学生信息:首先通过学号或姓名找到目标学生,然后允许用户修改其一项或多项信息(如修改成绩、调整班级)。
  4. 删除学生信息:根据学号等唯一标识,将学生从系统中移除。

这四大功能构成了管理系统的骨架。一个可用的系统,至少要实现它们。

2.3 进阶功能扩展思路

当基础CRUD实现后,你可以考虑添加一些进阶功能,让项目更丰满,也更能锻炼你的能力:

  • 数据统计:计算全班平均分、最高分、最低分,统计各分数段人数。这会用到循环遍历和条件判断。
  • 数据持久化:让程序关闭后数据不丢失。可以从简单的文本文件(如CSV、JSON格式)读写开始,再尝试连接SQLite或MySQL数据库。这是从“玩具程序”迈向“实用工具”的关键一步。
  • 用户界面:告别黑白的命令行。可以尝试用tkinter库做一个简单的图形界面,或者用PyQt做更美观的界面。这能极大提升项目的完整度和你的成就感。
  • 数据验证:确保用户输入的数据是合理的,例如年龄不能为负数,学号不能重复,成绩应在0-100之间。这体现了程序的健壮性。

3. 开发环境搭建与工具选型

工欲善其事,必先利其器。一个顺手的开发环境能让你事半功倍,减少很多不必要的麻烦。

3.1 Python解释器安装与配置

这是第一步。我强烈建议你直接去Python官网下载最新稳定版本。安装时,务必勾选“Add Python to PATH”这个选项。这是很多新手遇到的第一个坑:不勾选,在命令行里就无法直接使用python命令。安装完成后,打开命令行(CMD或PowerShell),输入python --version,如果能正确显示版本号,说明安装成功。

注意:如果你的电脑上之前安装过其他版本的Python,可能会出现版本冲突。一个彻底的解决办法是卸载旧版本,或者使用虚拟环境进行隔离。

3.2 代码编辑器:VSCode是首选

对于初学者和大多数场景,我首推Visual Studio Code。它免费、轻量、插件生态极其丰富。安装后,你需要配置Python开发环境:

  1. 在VSCode中安装官方Python扩展。
  2. 打开你的项目文件夹,VSCode通常能自动识别Python解释器。如果没有,你可以按Ctrl+Shift+P,输入“Python: Select Interpreter”来选择你刚安装的解释器。
  3. 建议再安装Pylance扩展,它能提供更好的代码补全和类型检查功能。

为什么不用PyCharm?PyCharm功能更强大,特别是对于大型Django项目,但它也更重、启动更慢。对于学生管理系统这类轻量级项目,VSCode的灵活和快速更胜一筹。当然,如果你之后主要做数据分析(用Pandas, NumPy),Spyder或Jupyter Notebook可能更合适;但就通用Python开发而言,VSCode是目前社区最活跃、最平衡的选择。

3.3 版本管理:早用Git早受益

即使你现在是一个人开发,我也强烈建议你初始化一个Git仓库。git init一下,然后定期git commit。这不仅仅是为了“备份”,更是为了培养一种良好的开发习惯:将大的改动分解为一系列小的、有记录的提交。当你某次改代码改出问题,想回到之前能正常运行的状态时,你就会感谢Git。去注册一个GitHub或Gitee账号,把代码推送到远程仓库,这也是你未来简历上的一个亮点。

3.4 虚拟环境:项目的独立沙箱

这是一个非常重要但常被新手忽略的环节。虚拟环境可以为每个项目创建独立的Python包安装空间,避免不同项目依赖的包版本冲突。

# 在项目根目录下,创建虚拟环境 python -m venv venv # 激活虚拟环境 (Windows) venv\Scripts\activate # 激活虚拟环境 (MacOS/Linux) source venv/bin/activate # 激活后,命令行提示符前会出现 (venv) 标识

激活后,你用pip install安装的任何包(比如后面可能用到的pandas做数据分析),都只会安装在这个项目的venv文件夹内,不会影响系统全局或其他项目。项目完成后,你可以通过pip freeze > requirements.txt命令生成依赖列表,别人拿到你的代码后,一键pip install -r requirements.txt就能复原环境。

4. 核心代码实现与分步解析

现在,我们进入实战环节。我们将采用“迭代开发”的思路,先实现一个基于命令行和内存列表的版本,再逐步增强。

4.1 版本一:内存存储版(基础CRUD)

这个版本的所有数据都保存在程序运行时的一个列表里,程序退出,数据就没了。目标是快速实现核心逻辑。

第一步:定义学生类这是我们的数据蓝图。

class Student: """学生类,定义学生属性和方法""" def __init__(self, stu_id, name, gender, age, class_name, score): """ 初始化学生对象 :param stu_id: 学号 (str) :param name: 姓名 (str) :param gender: 性别 (str) :param age: 年龄 (int) :param class_name: 班级 (str) :param score: 成绩 (int 或 float) """ self.stu_id = stu_id self.name = name self.gender = gender self.age = age self.class_name = class_name self.score = score def __str__(self): """定义打印学生对象时的格式""" return f"学号:{self.stu_id}, 姓名:{self.name}, 性别:{self.gender}, 年龄:{self.age}, 班级:{self.class_name}, 成绩:{self.score}"

这里使用了__str__魔法方法,这样当我们用print(student)时,就能看到格式化的信息,而不是晦涩的内存地址。

第二步:主程序框架与全局数据

# 用一个全局列表来存储所有学生对象,模拟数据库 students_list = [] def main(): """主函数,程序入口""" while True: print("\n" + "="*30) print("欢迎使用学生管理系统") print("1. 新增学生") print("2. 查询所有学生") print("3. 按学号查询学生") print("4. 修改学生信息") print("5. 删除学生") print("0. 退出系统") print("="*30) choice = input("请输入您的选择:") if choice == "1": add_student() elif choice == "2": show_all_students() elif choice == "3": query_student_by_id() elif choice == "4": update_student() elif choice == "5": delete_student() elif choice == "0": print("感谢使用,再见!") break else: print("输入有误,请重新输入!") if __name__ == "__main__": main()

这是一个典型的事件循环模式。while True让菜单持续显示,根据用户输入的数字,调用不同的功能函数。

第三步:实现新增功能

def add_student(): """新增学生信息""" print("\n--- 新增学生 ---") stu_id = input("请输入学号:").strip() # 简单的学号重复检查 for stu in students_list: if stu.stu_id == stu_id: print(f"错误:学号 {stu_id} 已存在!") return name = input("请输入姓名:").strip() gender = input("请输入性别:").strip() # 年龄和成绩需要转换类型,并加入异常处理 try: age = int(input("请输入年龄:").strip()) score = float(input("请输入成绩:").strip()) except ValueError: print("错误:年龄必须是整数,成绩必须是数字!") return # 创建学生对象并加入列表 new_student = Student(stu_id, name, gender, age, class_name, score) students_list.append(new_student) print(f"学生 {name} 添加成功!")

这里有几个关键点:1. 学号重复检查。2. 使用try...except捕获输入转换可能引发的ValueError异常,防止程序因非法输入而崩溃。这是编写健壮程序的基本素养。

第四步:实现查询功能

def show_all_students(): """显示所有学生信息""" print("\n--- 所有学生信息 ---") if not students_list: # 列表为空 print("当前系统中暂无学生信息。") return # 打印表头 print(f"{'学号':<10}{'姓名':<8}{'性别':<4}{'年龄':<6}{'班级':<15}{'成绩':<6}") print("-"*50) for stu in students_list: # 使用格式化输出,让信息对齐 print(f"{stu.stu_id:<10}{stu.name:<8}{stu.gender:<4}{stu.age:<6}{stu.class_name:<15}{stu.score:<6.1f}") # .1f 表示保留一位小数 def query_student_by_id(): """按学号查询学生""" stu_id = input("请输入要查询的学生学号:").strip() for stu in students_list: if stu.stu_id == stu_id: print("\n查询结果:") print(stu) # 这里会调用 Student 类的 __str__ 方法 return print(f"未找到学号为 {stu_id} 的学生。")

show_all_students函数中使用了字符串的格式化方法,让输出看起来像表格一样整齐,用户体验更好。<10表示左对齐并占用10个字符宽度。

第五步:实现修改和删除功能

def update_student(): """修改学生信息""" stu_id = input("请输入要修改的学生学号:").strip() for stu in students_list: if stu.stu_id == stu_id: print(f"当前信息:{stu}") print("请输入新的信息(若直接回车则保持原值):") new_name = input(f"姓名 [{stu.name}]:").strip() if new_name: stu.name = new_name # 同理,可以继续询问并更新 gender, age, score 等字段 new_score = input(f"成绩 [{stu.score}]:").strip() if new_score: try: stu.score = float(new_score) except ValueError: print("成绩输入无效,保持原值。") print("信息修改成功!") return print(f"未找到学号为 {stu_id} 的学生。") def delete_student(): """删除学生信息""" stu_id = input("请输入要删除的学生学号:").strip() for i, stu in enumerate(students_list): if stu.stu_id == stu_id: confirm = input(f"确定要删除学生 {stu.name} 吗?(y/n): ").strip().lower() if confirm == 'y': # 使用 pop 方法根据索引删除,效率比 remove 高 deleted_stu = students_list.pop(i) print(f"学生 {deleted_stu.name} 已被删除。") else: print("操作已取消。") return print(f"未找到学号为 {stu_id} 的学生。")

在修改功能中,我们提供了“回车保持原值”的友好交互。删除功能中,加入了确认环节,防止误操作。enumerate函数在遍历列表时同时获取索引和元素,便于后续的删除操作。

至此,一个具备基础CRUD功能的命令行学生管理系统就完成了。你可以运行它,体验完整的操作流程。

4.2 版本二:文件持久化版(数据不丢失)

内存版的数据无法保存。接下来,我们使用JSON文件来持久化数据。JSON是一种轻量级的数据交换格式,Python标准库中的json模块可以很方便地读写。

第一步:定义文件操作函数

import json import os DATA_FILE = "students.json" def load_students(): """从文件加载学生数据到内存列表""" global students_list # 声明要修改全局变量 students_list = [] # 先清空 if not os.path.exists(DATA_FILE): # 如果文件不存在,则返回空列表 return try: with open(DATA_FILE, 'r', encoding='utf-8') as f: # 从JSON文件读取数据,它是一个字典列表 data_list = json.load(f) for data in data_list: # 将字典数据重新构造成Student对象 stu = Student( data['stu_id'], data['name'], data['gender'], data['age'], data['class_name'], data['score'] ) students_list.append(stu) print("数据加载成功。") except (json.JSONDecodeError, IOError) as e: print(f"加载数据文件时出错:{e},将使用空列表。") students_list = [] def save_students(): """将内存中的学生列表保存到文件""" # 先将Student对象列表转换为字典列表 data_to_save = [] for stu in students_list: data_to_save.append({ 'stu_id': stu.stu_id, 'name': stu.name, 'gender': stu.gender, 'age': stu.age, 'class_name': stu.class_name, 'score': stu.score }) try: with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(data_to_save, f, ensure_ascii=False, indent=4) # indent使文件格式美观 print("数据保存成功。") except IOError as e: print(f"保存数据到文件时出错:{e}")

第二步:修改主函数和增删改函数在主函数main()的开头,调用load_students()。 在add_student(),update_student(),delete_student()这三个会改变数据的函数末尾,调用save_students()。 这样,每次程序启动都会自动加载上次保存的数据,每次数据变更都会立即写入文件。

实操心得:使用JSON文件做持久化简单快捷,非常适合小数据量(比如几百上千条记录)和初学者。但它也有缺点:每次保存都要写入整个文件,如果数据量巨大,效率会很低;并且不支持复杂的查询(比如“找所有数学成绩大于90的学生”)。当你的数据量和复杂度增长后,就该考虑使用真正的数据库了,比如SQLite(Python内置支持)或MySQL。

4.3 版本三:引入SQLite数据库版

当数据量和业务逻辑变得复杂时,文件存储的弊端就显现了。SQLite是一个轻量级的、无需单独服务器进程的数据库,它的整个数据库就是一个文件,但支持完整的SQL查询,是桌面和小型Web应用的绝佳选择。

第一步:设计数据库表结构我们先设计一张students表:

CREATE TABLE IF NOT EXISTS students ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增主键,方便管理 stu_id TEXT UNIQUE NOT NULL, -- 学号,唯一且非空 name TEXT NOT NULL, gender TEXT, age INTEGER, class_name TEXT, score REAL );

第二步:编写数据库连接与操作类

import sqlite3 class StudentDB: def __init__(self, db_name='students.db'): self.conn = sqlite3.connect(db_name) self.cursor = self.conn.cursor() self.create_table() def create_table(self): """创建学生表""" sql = """ CREATE TABLE IF NOT EXISTS students ( id INTEGER PRIMARY KEY AUTOINCREMENT, stu_id TEXT UNIQUE NOT NULL, name TEXT NOT NULL, gender TEXT, age INTEGER, class_name TEXT, score REAL ) """ self.cursor.execute(sql) self.conn.commit() def add_student(self, student): """添加一个学生""" sql = "INSERT INTO students (stu_id, name, gender, age, class_name, score) VALUES (?, ?, ?, ?, ?, ?)" try: self.cursor.execute(sql, (student.stu_id, student.name, student.gender, student.age, student.class_name, student.score)) self.conn.commit() return True except sqlite3.IntegrityError: print(f"错误:学号 {student.stu_id} 已存在!") return False def get_all_students(self): """获取所有学生""" self.cursor.execute("SELECT * FROM students") rows = self.cursor.fetchall() # 将查询结果转换为Student对象列表 students = [] for row in rows: # row[0]是自增id,我们构造Student时不需要它 stu = Student(row[1], row[2], row[3], row[4], row[5], row[6]) students.append(stu) return students # 同理,实现按学号查询、更新、删除的方法... # get_student_by_id(self, stu_id) # update_student(self, stu_id, **kwargs) # 使用字典传参更新特定字段 # delete_student(self, stu_id) def __del__(self): """析构函数,关闭数据库连接""" self.conn.close() # 在主程序中,实例化StudentDB,并用它的方法替代之前对students_list的直接操作 db = StudentDB()

使用数据库后,我们的业务逻辑函数(如add_student)将不再直接操作全局列表,而是调用db.add_student(new_student)。查询所有学生则调用students = db.get_all_students()

这个版本的改变是巨大的,它将数据存储和业务逻辑分离了。你的程序核心(菜单、输入输出、业务规则)不再关心数据到底存在哪里、怎么存的,它只和StudentDB这个“数据访问对象”打交道。这种思想,就是软件工程中常说的“数据访问层”分离,是构建更复杂、更可维护应用的基础。

5. 常见问题、调试技巧与性能优化

在实际开发中,你一定会遇到各种错误和意想不到的情况。下面是一些典型问题的排查思路和优化建议。

5.1 编码与中文乱码问题

这是一个非常常见的问题。当你从文件读取或向文件写入中文,或者在控制台打印中文时,可能会看到乱码。

  • 文件读写:在open()函数中,始终指定encoding='utf-8'参数。UTF-8是一种兼容性极好的编码。
  • 控制台乱码:Windows系统的CMD或PowerShell默认编码可能是GBK。可以尝试在代码开头添加:
    import io import sys sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    或者更简单的方法是,使用VSCode等现代编辑器的内置终端,它们通常能很好地支持UTF-8。

5.2 输入验证与异常处理

用户的输入是不可靠的。你必须假设用户会输入任何奇怪的东西。

  • 类型转换:在将input()得到的字符串转为intfloat时,务必使用try...except ValueError进行包裹。
  • 业务逻辑验证:学号是否重复?年龄是否在合理范围(比如1-100)?成绩是否在0-100之间?这些检查应该在数据存入列表或数据库之前完成。
  • 使用循环确保输入有效:对于关键信息,如果用户输入无效,应该提示重新输入,而不是直接返回。
    def get_valid_input(prompt, value_type, validation_func=None): while True: try: value = value_type(input(prompt).strip()) if validation_func and not validation_func(value): print("输入值不符合要求,请重新输入。") continue return value except ValueError: print(f"输入无效,请输入一个{value_type.__name__}类型的值。") # 调用示例:age = get_valid_input(“请输入年龄:”, int, lambda x: 1 <= x <= 100)

5.3 程序调试基础

当程序不按预期运行时,不要慌,系统地排查。

  1. 打印大法好:在怀疑出问题的代码行前后,打印关键变量的值 (print(f“变量a的值是:{a}”))。这是最直接有效的调试方法。
  2. 使用断点调试:在VSCode中,点击行号左侧设置断点(红点),然后按F5启动调试。程序运行到断点会暂停,你可以查看当前所有变量的值,并逐行执行(F10),步入函数(F11),这是定位复杂逻辑错误的利器。
  3. 阅读错误信息:Python的错误信息(Traceback)非常详细。从下往上看,最后一行是错误类型(如KeyError,TypeError),上面几行指出了错误发生在哪个文件的哪一行。学会阅读错误信息,能解决80%的问题。

5.4 从命令行到图形界面

如果你想让程序看起来更“像样”,可以尝试图形界面。tkinter是Python自带的库,无需安装。

import tkinter as tk from tkinter import ttk, messagebox class StudentManagerGUI: def __init__(self, root): self.root = root self.root.title("学生管理系统") self.db = StudentDB() # 复用之前的数据库类 # 创建界面组件:标签、输入框、按钮、表格(Treeview) self.create_widgets() self.load_data_to_table() def create_widgets(self): # 这里创建各种输入框和按钮,并绑定事件(如command=self.add_student) pass def add_student(self): # 从输入框获取数据,调用db.add_student() # 操作成功后,刷新表格显示 pass def load_data_to_table(self): # 从数据库获取所有学生,显示在Treeview表格中 pass if __name__ == "__main__": root = tk.Tk() app = StudentManagerGUI(root) root.mainloop()

图形界面编程是另一个广阔的领域,核心是“事件驱动”。你创建按钮,并告诉它“当你被点击时,就执行add_student这个函数”。虽然tkinter做出来的界面比较复古,但用于学习事件驱动模型和构建小型工具完全足够。

5.5 性能与扩展思考

当你的系统里的学生数量达到上万甚至更多时,一些简单的操作可能会变慢。

  • 查询优化:在数据库版本中,确保在经常用于查询的字段(如stu_id)上创建索引(CREATE INDEX idx_stu_id ON students(stu_id)),这能极大提升按学号查询的速度。
  • 批量操作:如果需要导入大量数据,不要用循环一次次执行INSERT,而应该使用SQLite的executemany()方法进行批量插入。
  • 连接管理:数据库连接是宝贵资源。在我们的简单示例中,程序启动时建立连接,退出时关闭。在更复杂的(如Web)应用中,通常使用连接池来管理。
  • 下一步学什么:完成这个项目后,你可以尝试把它改造成一个Web应用(使用Flask或Django框架),这样你就可以通过浏览器来访问了。或者,你可以为它添加更复杂的数据分析功能,用pandas库来计算平均分、绘制成绩分布图等。这个项目就像一个种子,可以朝着多个方向生长。
http://www.cnnetsun.cn/news/4239879.html

相关文章:

  • Qwen3.8实战:从API接入到本地部署与推理加速
  • 北岳恒山与悬空寺:绝壁之上的道化山河
  • MATLAB数学建模实战:从数据预处理到算法优化的核心技巧
  • NOIP2008 ISBN校验题精讲:从规则落地到工程化思维
  • AI生物技术情报简报实战:用LLM分析EGFR耐药文献全流程
  • 大模型本质是上下文预测引擎:AI应用开发与部署实践
  • Ansible控制节点配置与云服务自动化实战指南
  • 172张工业车间人员检测数据集:YOLOv8微调与部署实战
  • 数模竞赛多元线性回归实战:从数据诊断到模型检验全流程解析
  • 动态规划去重技巧:从蓝桥杯真题解析本质不同上升子序列计数
  • 半导体制冷杯DIY全解析:TEC选型、散热设计与PID温控实战
  • 保姆级教程:茉莉花 Zotero 插件 30 分钟搞定知网元数据抓取与 PDF 大纲
  • 网盘下载速度慢到 KB 级?这款免费油猴脚本本地解析直链,9 大网盘通吃,四步十分钟上手
  • Mac版Navicat试用到期怎么办?免费脚本快速重置恢复14天
  • 玻璃脏污目标检测数据集:工业视觉质检实战指南
  • 电力高空作业安全带检测数据集:VOC/YOLO双格式与YOLOv8实战
  • Coze记忆功能全解析:让智能体真正记住用户
  • 微盘源码K线修复与余额宝会员等级系统部署全攻略
  • Grok无字幕看懂数学视频?拆解多模态与推理融合的技术链路
  • 架构与设计演化:大型系统不停机现代化改造路径
  • 中医药知识图谱问答系统项目实战:Neo4j建模与Python问答实现
  • MATLAB仿真报童问题:从理论到实战的库存优化指南
  • 坑洼检测不是图像分类:道路语义理解与轻量化部署实战
  • 数模竞赛相关性分析实战:MATLAB与SPSS核心操作与结果解读
  • YOLOv8遥感小目标检测实战:NWPU VHR-10与DOTA数据集改进与训练全解析
  • ROS 2四足机器人单腿逆运动学实战:从关节坐标到运动控制
  • AI模型罗盘:从ReAct到Agent的工程化选型与评测方法
  • 基于DETR的智能冰箱物品识别:训练、部署与zip解压避坑全攻略
  • IEEE39节点模型深度解析:从文件结构到电力系统仿真落地
  • 自制Arduino Uno兼容单板:从硬件设计到grbl固件烧录全攻略