读者看此博客前,需要做以下几件事情:

  1. 看博客《https://blog.csdn.net/qq_60955261/article/details/127308701?》
  2. 学习JAVA GUI SWing,第一点博客中有参考学习链接
  3. 完成第一二点之后,要自己上手先实践一下

请勿抄袭,仅供学习参考

一、课程设计题目

考勤管理系统

二、课程设计目的

通过一学期对数据库系统这门学科的学习,我明白数据库系统这门课程对于计算机学子的重要性,数据库系统作为一门基础学科,我们单单学习理论是远远不够的,将其应用到实践中才是最重要的。

本次数据库系统课程设计我选择的是考勤管理系统,课程设计目的在于加深对关系数据库理论知识的理解,通过使用具体的 DBMS,掌握一种实际的数据库管理系统并掌握其操作技术,熟练掌握使用数据库前端开发工具,进一步提高同学们运用数据库技术解决实际问题的能力。

在此过程中,我们将理论转为实践,通过JDBC将数据库和JAVA联系起来,通过JAVA中的GUI完成界面设计,通过对界面按钮添加监听事件来完成增删查改等功能的具体实现,所涉及的知识面广,需要认真思考和自我摸索,对我们来说是一次很好的锻炼机会,对提升我们的思维能力和代码能力有很大的帮助。

三、总体设计

考勤制度是每个企事业单位所必需的,计算机的出现使员工出勤情况的记录和统计变得十分简单,此系统的主要目的是通过对员工上下班、出差、请假和加班记录的统计实现对员工的月度考勤统计,便于管理者掌握数据。

3.1 数据库概念结构设计

  • 考勤管理系统的 E-R 图

3.2 数据库逻辑结构设计

  • SQL实现


3.3 系统的功能模块设计

系统主要分为九个模块:【详细实现见4.6节】

  1. 上下班时间设定

  2. 添加员工和信息展示

  3. 上下班考勤信息展示和打卡

  4. 添加请假信息和查询

  5. 添加出差信息和查询

  6. 添加加班信息和查询

  7. 添加部门信息和查询

  8. 添加职务信息和增删改查

  9. 月度考勤信息表统计和查询

3.4 系统设计的核心步骤

核心步骤简述如下:

  1. 数据表的思考和构建【考勤管理系统数据表 – 见3.1节和3.2节】

  2. 实体创建,为每个表设置一个entity对象,设置属性、构造方法、getter 和setter方法

  3. resource 包的构造

    • 存放font:字体样式文件

    • 存放icon:图标照片文件

  4. 工具类(util包)的实现

    • 数据库连接:JDBCUtil

    • 图标工具类:IconUtil

    • 字体工具类:FontUtil

  5. 设置一个AMSMain 主类

  6. gui包的构建

    核心思想:对于每一个界面主类都要设置一个panel,然后设置一个对应的layout进行位置适配

    其他:自定义了一个dialog类和InformDialog

    核心模块

    • AMSLoginGui登录界面:

      • 里面内嵌了一个LoadingPanel进度条界面,同时加了一个Key实体,用于对账号密码的加密操作

      • 进度条里面嵌入了数据库的实现操作

    • AMSMainGui主界面设置:在主界面上设置了九个功能按钮键,并每个都进行了不同的事件监听,对应不同的功能

    • 功能按钮键的页面设置:【核心:GUI-Panel-Layout

      • 对于每一个子类界面,都设置一个独立的JFrame界面,并设计独立的PanelLayout,具体功能实现见4.6节功能说明

      • 对于每个子界面,我们又设置了功能按键,然后对每一个功能按键加监听事件,实现”增删查改”操作

  7. dao包的构建

    核心思想:对于每一个实体,都设置一个对应的dao类,建立SQL语句,通过数据库连接对其进行”增删查改”等操作,这是最核心的模块!!!

四、详细设计

4.1 数据库的建立

  • 首先是建立考勤管理系统(下面简称AMS)的数据库表,我是先做成一个EXCEL文件,然后进行SQL语句的撰写,一条条进行测试,保证数据表的正确建立,减少后面工作的麻烦,我用的是可视化工具Navicat,比较方便,没有用命令行操作,也是类似的,先创建一个名为attendancemanagementsystem数据库,然后是建立基本表,基本操作如下:如PERSON表的创建⬇️

  • Navicat中的attendancemanagementsystem数据库下创建查询语句,建立基本表:

  • 对于AMS, 我们需要建立八个基本表(ATTENDANCE 出勤记录表、ATTENDANCE_STATIC 月度考勤表、LEAVE 请假记录表、OVERTIME 加班记录表、ERRAND 出差记录表、PERSON 员工个人信息表、DEPARTMENT 部门信息表、JOB 职务信息表),同样的操作建完另外的表,同时把基本表建立的语句存放在”表的建立”查询中,方便之后对表进行更改时的操作,成功界面如下:

  • 成功完成后,数据库基本表的创建就完成啦,然后把所有的基本表删除,因为之后我们将在代码中实现数据库基本表的创建,以上操作只是为了测试SQL语句的正确性。

4.2 entity的创建

注意:在此工作之前,需要创建一个Java项目,我把他命名为AttendanceMnagementSystem,并建好基础的项目结构,会随着进展还会更改

  • 对应我们所创建的基本表,我们需要创建九个实体类,以Attendance类为例:属性设置要与基本表一致,然后建立构造函数和getter、setter函数,如下图:

下一步:同理建立AttendanceStatic、Department、Errand、Job、Leave、Overtime、Person实体


4.3 util工具类

4.3.1 JDBCUtil 数据库工具类

主要功能:该类的主要作用是实现数据库的连接,首先需要学习JDBC,在做课设之前,我已学习并总结(网页地址),在这个项目中,新增了一个YAMl的配置文件config.yaml,将其数据库的地址、账号和密码存入其中,再将这个文件导入到类中,并构建了getConnection()函数,提高代码的复用度,使项目逻辑性更强,核心代码如下:

LinkedHashMap<String,String> configs = new Yaml().load(new FileInputStream("config.yml"));db_url = configs.get("db_url");root = configs.get("user");password = configs.get("password");// 数据库连接public Connection getConnection() throws SQLException{return DriverManager.getConnection(db_url,root,password);}

注意:在此需要导入俩个jar包(驱动程序),在这里把俩个包放在lib的目录下,选择 FILE-Project Structure-Modules-Dependencies-选择“+”-选择第一个-添加下载好的jar包,再Apply, OK,即可。

  • 接着构建了free()函数,作用:释放数据库
  • config.yaml文件的copy实现,用到了流的实现,由于这里的局限设置,在我下面第一次跑主类的时候,会报错,运行第二次的时候会正常运行
  • 同时这里设置了一个单例模式,减少内存的销毁,提高效率,使其能获得JDBCUtil工具类对象,这里展示一下该方法实现的核心代码:
private static JDBCUtil instance = null;public static JDBCUtil getInstance(){if(instance == null){synchronized (JDBCUtil.class){if(instance == null){instance = new JDBCUtil();}}}return instance;}

4.3.2 IconUtil 图片工具类

主要功能:获取图片,结构化代码,将所有的图片都放在resource/icon文件下,将所有图片的定义都丢在IconUtil类里,这里以登录界面的更改窗口左上角图标为例:

// 定义public static final ClassLoader CLASS_LOADER = IconUtil.class.getClassLoader();public static final ImageIcon MAIN_LOGIN = new ImageIcon(CLASS_LOADER.getResource("resource/icon/登录.png"));// SIMSLoginGui.java 文件下 -->调用this.setIconImage(IconUtil.MAIN_LOGIN.getImage());
  • 之后所有的图片处理都与上面处理一致,用到的时候再往里面丢就好了

4.3.3 FontUtil 字体工具类

NODE:设置一个字体工具类,主要用于控制字体的类型、大小和颜色等效果,达到适配的效果,当然这个语句很简单,也可以直接设置,以下为代码中的一个实例:

components[0].setFont(new Font("",Font.PLAIN, size)); // 默认字体components[0].setForeground(Color.YELLOW); // 设置字体颜色

字体工具类:

 public static Font FZJZ_FONT;try {FZJZ_FONT = Font.createFont(Font.PLAIN, new File(CLASS_LOADER.getResource("resource/font/方正剪纸简体.ttf").toURI()));} catch (URISyntaxException | FontFormatException | IOException e) {e.printStackTrace();}public static Font getFont(Font font, int style, float size) {return font.deriveFont(style, size);}

4.4 AMSLoginGui 登录界面

4.4.1 表的创建

  • 此处的实现基于JDBCUtil类的实现和SQL语句的实现,直接用最简单的方法调用即可,核心代码如下:
Connection connection = JDBCUtil.getInstance().getConnection();PreparedStatement ps = connection.prepareStatement("写入SQL语句即可");ps.execute();ps.execute("下一条SQL语句")
  • 检查:该类要继承JFrame类,在无参构造函数中设置窗口大小和其他样式,设置窗口可见,执行代码,正常出现主窗口界面并且可以在Navicat中看到我们所创建的基本表,证明到此操作为止,代码和逻辑都可行,每一步一定要进行检查!!不然出现了Bug的时候无从下手!!

4.4.2 加载条

  • 此界面嵌套在AMSLoginGui.java文件中,构建了LoadingPanel类,并对其进行了界面适配,同时实现了数据表的创建

核心思想

  • LoadingPanel.java,继承了JPanel类,设置了一个进度条
public class LoadingPanel extends JPanel {public JProgressBar pb = new JProgressBar(0, 100);public LoadingPanel() {this.setLayout(new LoadingPanelLayout());pb.setStringPainted(true);this.add(pb);}}
  • 进行适配操作
  • AMSLoginGui.java中,边设置进度条界面,边创建了数据表,部分核心代码如下:
LoadingPanel lp = new LoadingPanel();Dialog dialog = new Dialog(SCREEN_DIMENSION.width * 3 / 8, SCREEN_DIMENSION.height / 10, "加载中", IconUtil.LOADING_ICON.getImage(), lp, false);lp.pb.setValue(11);

4.4.3 界面设计

  • 这里采用的是在登录界面添加一个JPanel,此处构建LoginPanel类,然后进行界面预设计

    • 1️⃣界面组件的定义【这里详细对其进行说明,之后不再进行详细解释】
      • 设置了四个JLabel,作用分别是:标题,账号描述,密码描述,背景设计
      • 设置了一个JTextField,用于账号的输入;一个JPasswordField,用于密码的输入
      • 设置了一个JButton,登录▶️按钮,并对其增加监听事件

    核心代码块:

    private static final JLabel LABEL_ACCOUNT = new JLabel("账号:");private static final JPasswordField TEXT_PASSWORD = new JPasswordField();private static final JButton BUTTON_LOGIN = new JButton();
    • 2️⃣添加组件,此处需要与注意添加的顺序,下面做适配的时候需要一一对应

    • 3️⃣监听事件,此处只对登录按钮进行了监听,获取账号和密码的输入并与我们预设置的进行对比,如果正确,就跳转到主界面,如果错误就弹出警告页面(一个是输入错误,一个是还未输入就登录)

    效果图

    • 4️⃣进行界面的适配,构建了LoginLayout类,详细见4.4.4节↙️

4.4.4 界面适配

核心步骤

对于所有的界面适配类,核心思想都是首先要实现接口LayoutManager,接着最重要的是layoutContainer函数的重写,最基本的操作都是获取父界面的长宽和组件,然后对设置每个组件的位置的大小,此处有一个技巧,我们保证等比例放大,先输出父类的长宽(设长为 a),然后根据当前界面设置位置,乘一个width / a ,那么当我们放大窗口后,width改变,组件大小也会跟着变化,不会使布局被破坏

LoginLayout为例,重写函数layoutContainer

​ 1️⃣基础代码

int width = parent.getWidth();int height = parent.getHeight();Component[] components = parent.getComponents();int size = width / 50 + height / 30;// 根据界面大小进行设置

​ 2️⃣设置组件位置

  • 我这里的width=708,height=415,然后进行组件定位,注意组件元素与添加的先后关系一致
// titlecomponents[0].setBounds(163 * width / 708 , (-100) * height / 415 , width , height);components[0].setFont(new Font("",Font.PLAIN, size));components[0].setForeground(Color.YELLOW);
  • 此处对登录按钮和背景设置做了特殊处理,把按钮设成了背景透明,无框线,添加了自己自定义的图片,使界面更加美观,将背景设置成覆盖整个界面,此处给出部分核心代码:
// backgroundcomponents[6].setBounds(0,0,width,height);ImageIcon icon2 = new ImageIcon(IconUtil.LOGIN_BACKGROUND_ICON.getImage());icon2.setImage(icon2.getImage().getScaledInstance(width, height, Image.SCALE_DEFAULT));((JLabel)components[6]).setIcon(icon2);
  • 所有组件都类似上面的操作设置即可。

适配是比较麻烦的,相当于是界面好不好看的核心,在这个地方,需要一点点的去调整,这也是JFrame框架的一个小劣势吧,需要人为的去进行适配操作,需要耐心和时间完成!

4.5 主界面

设置了九个按钮键,并每个按钮键插入一张对应的图片和加上文字说明,并对每一个按钮键加了一个监听事件,每一个按键点开都会出来对应的JFrame界面,此处有九个子界面,部分核心代码如下:

JButton[] jButtons = new JButton[9];for (int i = 0; i < 9; i++) {jButtons[i] = new JButton();jButtons[i].setContentAreaFilled(false);jButtons[i].add(new JLabel());jButtons[i].add(new JLabel(contents.get(i)));jButtons[i].setLayout(new MainButtonLayout(icons.get(i)));this.add(jButtons[i]);}// 考勤jButtons[2].addActionListener(e -> {if (ATTENDANCE_GUI == null) {AttendanceGui attendanceGui = new AttendanceGui();}else{if(!ATTENDANCE_GUI.isVisible()) {ATTENDANCE_GUI.setVisible(true);}}});

效果图如下:

  • TableModel,以考勤表为例:
package pers.cwisdomj.attendancemanagementsystem.gui.table.model;import pers.cwisdomj.attendancemanagementsystem.dao.AttendanceDao;import pers.cwisdomj.attendancemanagementsystem.entity.Attendance;import pers.cwisdomj.attendancemanagementsystem.entity.Person;import pers.cwisdomj.attendancemanagementsystem.gui.panel.son.AttendancePanel;import pers.cwisdomj.attendancemanagementsystem.util.JDBCUtil;import javax.swing.table.AbstractTableModel;import java.util.ArrayList;import static pers.cwisdomj.attendancemanagementsystem.util.JDBCUtil.DATETIME_FORMAT;/** * @author CWisdomJ * @date 2022/12/25 14:55 **/public class AttendanceTableModel extends AbstractTableModel {private static AttendanceTableModel instance;private final String[] columnNames = {"记录编号", "员工姓名","出入情况", "出入时间"};private ArrayList<Attendance> viewAttendances;private ArrayList<Attendance> attendances;private final Attendance condition = new Attendance();public AttendanceTableModel(){update();reload();}public void update(){attendances = AttendanceDao.searchAllAttendances();viewAttendances = new ArrayList<>();}public void reload(){viewAttendances = (ArrayList<Attendance>) attendances.clone();viewAttendances.removeIf( e -> {boolean flag = condition.getPerson() == null || e.getPerson().getName().contains(condition.getPerson().getName());return !flag;});}public void addAttendance(Attendance attendance){attendances.add(attendance);}public void updateUI(){reload();AttendancePanel.getTableDepartment().updateUI();}@Overridepublic int getRowCount() {return viewAttendances.size();}@Overridepublic int getColumnCount() {return columnNames.length;}@Overridepublic String getColumnName(int columnIndex) {return columnNames[columnIndex];}@Overridepublic Object getValueAt(int rowIndex, int columnIndex) {Attendance attendance = viewAttendances.get(rowIndex);switch (columnIndex){case 0:return attendance.getId();case 1:return attendance.getPerson().getName();case 2:return attendance.getIo() == 1 " />"上班" : "下班";case 3:return DATETIME_FORMAT.format(attendance.getToTime());default:return null;}}public static AttendanceTableModel getInstance() {if (instance == null) {synchronized(JDBCUtil.class) {if (instance == null) {instance = new AttendanceTableModel();}}}return instance;}public void setCondition(Attendance attendance){condition.setPerson(new Person(1,attendance.getPerson().getName()));}}

4.6 功能实现

4.6.1 上下班时间的设定

此处是在数据库里面建立了一个set表,先在数据库里面与设置了上下班时间,同时当管理员更改上下班的时候,会将set表更新,实现更改。

部分核心代码:

 try { DATE_START.getInnerTextField().setValue(TIME_FORMAT.parse(StartTime)); DATE_END.getInnerTextField().setValue(TIME_FORMAT.parse(EndTime));} catch (ParseException ex) { ex.printStackTrace();}

4.6.2 员工信息

此处展示的是员工信息表并增设了增加和查询功能,同时所在部门和职务连接了部门表和职务表的信息,会随着对应部门和职务信息的更改及时跟新员工信息表,每一个功能按键会出现一个JPanel和对应的layout,当点击确定时,会执行对应的dao,并更新数据库。

此处的查询功能是模糊匹配,例如我输入”张”,那么员工信息表将会展示姓名里面有”张”的员工信息,此处展示dao的具体实现代码:

 private static final String SQL_SEARCH_PERSONS = "SELECT P.ID, PASSWORD, AUTHORITY, P.NAME, SEX, BIRTHDAY, D.ID, D.NAME, J.ID, J.NAME, EDULEVEL, SPECIALTY, ADDRESS, TEL, EMAIL, STATE, REMAKEFROM `PERSON` P, `DEPARTMENT` D, `JOB` J WHERE P.DEPARTMENT = D.ID AND P.JOB = J.ID"; public static ArrayList<Person> searchAllPersons() {ArrayList<Person> list = null;Connection connection = null;PreparedStatement ps = null;ResultSet rs = null;try {list = new ArrayList<>();connection = JDBCUtil.getInstance().getConnection();ps = connection.prepareStatement(SQL_SEARCH_PERSONS);ps.execute();rs = ps.getResultSet();while (rs.next()) {Person person = new Person(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getString(4), rs.getInt(5), new Date(JDBCUtil.DATE_FORMAT.parse(rs.getString(6)).getTime()), new Department(rs.getInt(7), rs.getString(8)), new Job(rs.getInt(9), rs.getString(10)), rs.getInt(11), rs.getString(12), rs.getString(13), rs.getString(14), rs.getString(15), rs.getInt(16), rs.getString(17));person.setPassword(person.getKey().decrypt(person.getPassword()));list.add(person);}} catch (SQLException | ParseException e) {new InformDialog(400, 150, "警告", true, "数据查询错误", false);e.printStackTrace();}JDBCUtil.getInstance().free(connection, ps, rs);return list;}

4.6.3 上下班信息

考勤记录,此处当插入一条上下班信息后,会在月度考勤统计表里面增加一条对应的年月信息,同时如果员工迟到,那么对应迟到次数就会加一,如果员工提前下班,那么对应的早退次数会加一,此处实现都写在dao里面,当增加对应的信息时就会执行操作,部分核心如下:

 if (attendance.getIo() == 0) { //如果是下班,更新早退次数AttendanceStatic attendanceStatic = StatisticsDao.getAttendanceStaticFromSQL(attendance.getPerson(), yearMonth);//累计工作天数先加+1attendanceStatic.setWorkHour(attendanceStatic.getWorkHour()+1);if (end.getTime() > signIn.getTime()) { //早退了attendanceStatic.setEarlyTimes(attendanceStatic.getEarlyTimes()+1);}StatisticsDao.updateAttendanceStatic(attendanceStatic);} else {//如果是上班,更新迟到次数if (start.getTime() < signIn.getTime()) { //迟到了AttendanceStatic attendanceStatic = StatisticsDao.getAttendanceStaticFromSQL(attendance.getPerson(), yearMonth);attendanceStatic.setLateTimes(attendanceStatic.getLateTimes()+1);StatisticsDao.updateAttendanceStatic(attendanceStatic);}}

以上部分效果图如下:

4.6.4 出差、请假和加班信息

主要是插入出差、请假和加班信息,三者大体上是一致的,一个增加信息的按钮和一个查询信息的按钮键,对于请假信息,当增加一条员工信息时,会更新考勤统计表的累计请假时数,对于出差信息,当增加一条出差信息时,会更新考勤统计表的累计出差时数,对于加班信息,当增加一条加班信息时,会更新考勤统计表的累计加班时数,此处以请假信息为例,部分核心代码如下:

long spaceMS = errand.getEndTime().getTime() - errand.getStartTime().getTime();long hourM = 1000 * 60 * 60;int spaceHours =(int) (spaceMS / hourM);int yearMonth = Integer.parseInt(new SimpleDateFormat("yyyyMM").format(new Date(errand.getStartTime().getTime())));AttendanceStatic attendanceStatic = StatisticsDao.getAttendanceStaticFromSQL(errand.getPerson(), yearMonth);attendanceStatic.setErrandHDay(attendanceStatic.getErrandHDay() + spaceHours);StatisticsDao.updateAttendanceStatic(attendanceStatic);
public static boolean updateAttendanceStatic(AttendanceStatic attendanceStatic) {Connection connection = null;PreparedStatement ps = null;boolean flag = true;try{connection = JDBCUtil.getInstance().getConnection();ps = connection.prepareStatement(SQL_UPDATE_STATIC);//更新内容ps.setInt(1,attendanceStatic.getWorkHour());ps.setInt(2,attendanceStatic.getOverHour());ps.setInt(3,attendanceStatic.getLeaveHDay());ps.setInt(4,attendanceStatic.getErrandHDay());ps.setInt(5,attendanceStatic.getLateTimes());ps.setInt(6,attendanceStatic.getEarlyTimes());ps.setInt(7,attendanceStatic.getAbsentTimes());//条件ps.setInt(8,attendanceStatic.getYearMonth());ps.setInt(9,attendanceStatic.getPerson().getId());ps.execute();}catch (SQLException e){e.printStackTrace();flag = false;}JDBCUtil.getInstance().free(connection,ps);return flag;}

效果图如下:

4.6.5 部门和职务信息

此处展示的部门和职务信息,对部门界面,设置了增加部门和查询部门按钮,对于职务界面,设置了增删查改的按钮,此处不是本系统的关键地方,主要是对员工信息表的影响,在设计的时候为了使得程序更结构化,所以把该块内容单独了出来,代码就不再给出,效果图如下:

4.6.6 月度考勤统计

此处是在AttendanceInputPanelLeaveInputPanelOvertimeInputPanelErrandInputPanel的基础上的实现,我采用的分模块进行统计,在插入对应的数据时,就进行数据的更新,当然,我们还可以采取最后的时候查询数据库里面的所有数据再进行统计,显然,这种方式不是最优的,所以在这个功能模块下,我设置了一个月度考勤统计表和查询按钮,此处月份采用的是年月,这样才能具体分别清楚,这是此系统里面最核心的部分。

五、结果与分析

结果:

实现了考勤管理系统,通过验证,统计数据完全正确,功能实现,界面设计内容完整,代码结构化和清晰化程序高,本次课设完成度相对较高。

此处展示项目目录结构图:

分析

所用的JFrame用起来较复杂,代码量大,但是界面只能说清晰化程度高,美观度并不高,同时定位麻烦,对每一个界面都写了一个适配的layout文件,大大增加了工作量,但是自己感到比较满意的一点是自己的项目结构非常清晰,界面虽然没那么精致,但是内容丰满,界面干净清晰,重点都展示了出来,待提升的地方:

  1. 界面美观度

  2. 结构化整体代码,多写接口,减少代码量

  3. 功能这一块还能进行增加

  4. 尝试换一种方式去写,实现更好的优化

  5. 提升自己的思维能力和代码能力,锻炼结构化和模块化思维

六、小结与心得体会

  • 总结和体会

​ 在课设中,遇到的最大的问题是考勤数据的统计,月度考勤统计表涉及了几个数据表,让我在实现的时候头疼不已,不知道该如何实时更新统计表,后进行了进一步的深入学习和探索,最后完美解决。

​ 本次课设从总体上来说还是有一定的难度的,很锻炼和培养我们的综合能力,自己最开心的是没有被困难所吓倒,通过自己的努力顺利地完成了本次课设,同时在课设过程中,将理论运用到了实践中,巩固了数据库以及Java课程方面的相关知识,对 GUI有了更深入的了解,也加深了对JDBC 的了解,同时培养了好的代码习惯,学习优秀的项目结构,总之,此次课设对于自己来说是一种突破的进展,通过此次学习,受益匪浅,也希望自己再之后的学习中能以更加饱满的热情去迎接一次次的挑战,同时在一次次挑战中不断增强自己的专业能力,成为一名合格的计算机学子!

  • 致谢

感谢OneWanAlantRonglin在此次课设中对我的帮助,感谢!和朋友们一起学习的感觉真的很好,一起学习,一起交流,一起进步,一起成长~✨✨

希望以上博客能给处于迷惘中的学子一点帮助,同时也不希望我的博客给大家提供了偷懒的途径,祝大家学业顺利~

完结于20221228,更新于20231113