news 2026/10/5 11:41:37

Java+MySQL构建高可用学生成绩分析系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java+MySQL构建高可用学生成绩分析系统

简介:本资源是一个基于Java与MySQL开发的学生成绩管理分析系统,面向高校计算机专业学生、Java初学者及教育信息化实践者,解决传统成绩管理效率低、分析手段弱、家校协同难等实际问题。压缩包共18个文件,含7个XML配置与界面定义文件、4个IntelliJ项目配置(.iml)、2个Gradle构建脚本、1个Java主程序文件(EnterGui.java)及配套properties、jar、bat和gradlew等运行支持文件,整体仅62KB,轻量易部署,适合课程设计、毕业设计或小型教务场景快速上手。目前已有29人学习下载。读者可直接导入IDEA运行完整系统,获得包含学生信息管理、成绩录入查询、统计分析报表、权限控制、成绩预警通知在内的六大核心模块源码与工程结构,尤其适合理解MVC分层设计、JDBC连接MySQL、Swing/AWT桌面界面开发及Gradle项目构建全流程。

1. 为什么一个“学生成绩管理分析系统”值得用 Java + MySQL 重做一遍?

不是所有教务系统都叫“学生成绩管理分析系统”——它得能查成绩、能算均分、能按班级/课程/学期横向比、能导出 Excel、能发现异常波动(比如某班数学平均分突然比年级低 12 分)、还能支撑 500+ 学生并发录入不卡顿。我去年接手一个老系统,用 PHP + SQLite 搭的,跑三年后连“按专业统计挂科率”都要等 8 秒,导出 2000 条记录直接超时。后来用 Java(JDK 17)+ MySQL 8.0.33 重构,核心模块响应压到 200ms 内,支持实时滚动计算各维度统计值,且上线后三个月没动过 SQL 优化脚本。这不是炫技,而是当“分析”二字写进系统名里,就意味数据要可钻取、可回溯、可预警——Java 提供强类型校验和线程安全控制,MySQL 提供事务一致性与窗口函数能力,二者组合是当前中小教育信息化场景下最稳、最易维护、最能扛住真实业务压力的技术栈。适合高校教务处、职业院校信息中心、以及需要交付可审计成果的外包团队。


2. 从零建库:MySQL 设计必须绕开的三个反模式

2.1 表结构设计:为什么“一张大宽表”在分析场景里是定时炸弹?

常见错误是把所有字段塞进student_score表:id, student_id, name, class_id, major, course_name, term, score, teacher_name, exam_type, create_time...
表面省事,实际埋雷:

  • 更新学生姓名或班级时,需全表UPDATE,锁表时间长;
  • 按专业查挂科率时,WHERE major = '计算机应用技术'无法走索引(除非给major单独建索引,但宽表索引膨胀快);
  • 无法区分“平时成绩”“期中”“期末”,导致后续分析维度缺失。

正确做法:严格遵循第三范式,拆成 4 张主表 + 1 张关联表

-- 学生主表(只存身份与归属) CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(12) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(20) NOT NULL, gender ENUM('M','F') DEFAULT 'M', class_id BIGINT NOT NULL, enrollment_year YEAR NOT NULL, INDEX idx_class_year (class_id, enrollment_year) ); -- 课程主表(含课程属性,非仅名称) CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(10) NOT NULL UNIQUE COMMENT '课程代码', name VARCHAR(50) NOT NULL, credit TINYINT NOT NULL DEFAULT 2, category ENUM('必修','选修','实践') NOT NULL, dept_id BIGINT NOT NULL ); -- 成绩事实表(只存原子事实,无冗余) CREATE TABLE score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, term VARCHAR(10) NOT NULL COMMENT '格式:2023-2024-1', score DECIMAL(5,2) NOT NULL CHECK(score BETWEEN 0 AND 100), exam_type ENUM('平时','期中','期末','补考') NOT NULL, teacher_id BIGINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course_term (student_id, course_id, term, exam_type), INDEX idx_course_term (course_id, term), INDEX idx_student_term (student_id, term) ); -- 班级维度表(支持按年级/专业/班级多级下钻) CREATE TABLE class ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(10) NOT NULL UNIQUE COMMENT '如:2022JC01', name VARCHAR(30) NOT NULL, major VARCHAR(30) NOT NULL, grade YEAR NOT NULL, capacity TINYINT DEFAULT 50 ); -- 教师表(独立维护,避免成绩表冗余) CREATE TABLE teacher ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(10) NOT NULL UNIQUE, name VARCHAR(20) NOT NULL, dept_id BIGINT NOT NULL );

关键说明:

  • score表中exam_type用 ENUM 而非 VARCHAR,节省存储且防非法值;
  • UNIQUE KEY uk_stu_course_term防止同一学生同一课程同一学期重复录入(补考除外,靠exam_type区分);
  • term字段用VARCHAR(10)存'2023-2024-1'格式,比 DATE 类型更灵活(支持跨年学期),且便于按字符串前缀查询(如LIKE '2023%');
  • 所有外键未显式声明FOREIGN KEY,因生产环境常关闭外键检查(SET FOREIGN_KEY_CHECKS=0),靠应用层逻辑+唯一索引兜底,避免高并发下锁等待。

2.2 索引策略:别只建在WHERE字段上,更要覆盖ORDER BY + LIMIT场景

成绩分析高频操作:

  • 查某班所有学生成绩并按分数倒序(用于排名);
  • 查某课程近三学期平均分趋势(需按term排序);
  • 导出某专业所有学生各科成绩(需关联student-class-course三表)。

若只对student_id和course_id建单列索引,执行计划会显示Using filesort或Using temporary,性能断崖下跌。

实测有效复合索引方案(MySQL 8.0.33):

查询场景必须存在的索引创建语句为什么有效
按班级查学生成绩(含排序)(class_id, create_time)ALTER TABLE student ADD INDEX idx_class_time (class_id, create_time);class_id过滤后,create_time天然有序,避免 filesort
按课程查成绩趋势(按 term 排序)(course_id, term)ALTER TABLE score ADD INDEX idx_course_term (course_id, term);term是字符串,但按'2023-2024-1'格式存储时字典序 = 时间序
多表关联导出(student→class→score→course)score表上(student_id, course_id)ALTER TABLE score ADD INDEX idx_stu_course (student_id, course_id);覆盖关联路径中最常被驱动的表(student 先查,再用 student_id 扫 score)

血泪经验:曾为score表加INDEX(student_id, score)后,执行SELECT * FROM score WHERE student_id=123 ORDER BY score DESC LIMIT 10仍慢——因为score是 DECIMAL,B+Tree 中范围扫描效率低于等值匹配。最终改用(student_id, id)(id主键自动递增,天然有序),配合ORDER BY id DESC实现快速分页,再用 Java 层补足业务排序逻辑。索引不是越多越好,而是要匹配最重的那 3 个查询路径。

2.3 字符集与排序规则:UTF8MB4 不是万能解药,utf8mb4_0900_as_cs才是分析刚需

老系统用utf8mb4_general_ci,结果“张伟”和“张玮”在GROUP BY name时被合并——因为_ci(case insensitive)同时忽略大小写和 Unicode 变体。成绩分析要求精确匹配:学生姓名、课程代码、教师工号都必须严格区分。

必须设置:

-- 创建库时指定 CREATE DATABASE school_analytics DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs; -- 注意:是 _as_cs(accent sensitive + case sensitive) -- 建表时显式声明(即使库已设,也防迁移遗漏) CREATE TABLE student ( ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_as_cs;

验证方法:

SELECT '张伟' = '张玮' COLLATE utf8mb4_0900_as_cs; -- 返回 0(false) SELECT '张伟' = '张玮' COLLATE utf8mb4_general_ci; -- 返回 1(true)

若漏设,SELECT COUNT(*) FROM student GROUP BY name会少统计人数,且无法通过DISTINCT修复——这是静默数据错误,上线后极难追溯。


3. Java 层落地:用 Spring Boot + MyBatis-Plus 构建可分析的业务骨架

3.1 依赖选型:为什么放弃 JPA,坚持 MyBatis-Plus?

JPA 的@Entity映射在成绩系统里是负担:

  • score表需频繁GROUP BY多字段、WINDOW FUNCTION计算排名、UNION ALL合并不同考试类型,JPQL 写起来像翻译古文;
  • student和class表存在历史数据修正需求(如转专业),JPA 的脏检查机制易引发意外UPDATE;
  • 分析接口需动态拼接WHERE条件(如“挂科率 > 30% 且平均分 < 65”),MyBatis-Plus 的QueryWrapper天然支持。

pom.xml 关键依赖(Spring Boot 3.2.0 + JDK 17):

<dependencies> <!-- Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 数据库 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>4.3.0</version> </dependency> <!-- 分页与导出 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot3-starter</artifactId> <version>1.4.9</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.4</version> </dependency> </dependencies>

注意:mybatis-plus-spring-boot3-starter是适配 Spring Boot 3.x 的专用包,若误用mybatis-plus-boot-starter(旧版),启动时会报java.lang.NoClassDefFoundError: org/springframework/boot/bind/RelaxedPropertyResolver——这是 Spring Boot 2.x 与 3.x 的包路径断裂导致,新手踩坑率超 70%。

3.2 实体类与 Mapper:让 Java 代码“说人话”

Score实体不直接映射数据库字段,而是封装业务语义:

// src/main/java/com/example/school/entity/Score.java @Data @TableName("score") public class Score { @TableId(type = IdType.ASSIGN_ID) private Long id; private Long studentId; private Long courseId; private String term; // "2023-2024-1" private BigDecimal score; private String examType; // "期末", "补考" // 业务字段:不存库,但查询时注入 @TableField(exist = false) private String studentName; @TableField(exist = false) private String courseName; @TableField(exist = false) private String className; @TableField(exist = false) private Integer rankInClass; // 班级内排名(用窗口函数计算) }

Mapper 接口定义(含复杂分析查询):

// src/main/java/com/example/school/mapper/ScoreMapper.java @Mapper public interface ScoreMapper extends BaseMapper<Score> { // 查询某班某学期各科平均分、及格率、最高分(核心分析接口) @Select(""" SELECT c.name AS courseName, AVG(s.score) AS avgScore, COUNT(CASE WHEN s.score >= 60 THEN 1 END) * 100.0 / COUNT(*) AS passRate, MAX(s.score) AS maxScore, MIN(s.score) AS minScore FROM score s JOIN student st ON s.student_id = st.id JOIN class cl ON st.class_id = cl.id JOIN course c ON s.course_id = c.id WHERE cl.code = #{classCode} AND s.term = #{term} GROUP BY c.id, c.name ORDER BY avgScore DESC """) List<CourseStat> selectCourseStatsByClass(@Param("classCode") String classCode, @Param("term") String term); // 计算学生个人成绩趋势(近三学期) @Select(""" SELECT s.term, s.score, c.name AS courseName, ROW_NUMBER() OVER (PARTITION BY s.student_id ORDER BY s.term DESC) AS rn FROM score s JOIN course c ON s.course_id = c.id WHERE s.student_id = #{studentId} ORDER BY s.term DESC LIMIT 3 """) List<StudentTermTrend> selectLatestThreeTerms(@Param("studentId") Long studentId); }

参数说明:

  • @Param("classCode")显式命名参数,避免 XML 中#{}取值失败;
  • ROW_NUMBER() OVER (...)是 MySQL 8.0+ 窗口函数,替代传统自连接排名,性能提升 5 倍以上;
  • LIMIT 3放在子查询外层,确保只取最新 3 条,而非先取全部再排序——这是防止大数据量时 OOM 的关键。

3.3 Service 层:把“分析”变成可复用的方法链

成绩分析不是单次查询,而是指标组合。例如“专业预警”需同时满足:

  1. 某专业下所有班级的平均分 < 65;
  2. 该专业挂科率 > 25%;
  3. 近两学期趋势下降。

封装为可组合的服务方法:

@Service public class AnalysisService { @Autowired private ScoreMapper scoreMapper; @Autowired private ClassMapper classMapper; // 步骤1:获取专业下所有班级代码 public List<String> getClassCodesByMajor(String major) { return classMapper.selectList( new QueryWrapper<Class>().eq("major", major)) .stream().map(Class::getCode).collect(Collectors.toList()); } // 步骤2:批量查各班统计(避免 N+1) public Map<String, ClassStat> batchClassStats(List<String> classCodes, String term) { // MyBatis-Plus 不支持 in-list 参数直接传入 @Select,改用 XML 或构建动态 SQL // 此处用 XML 方式(更可控) return scoreMapper.selectClassStatsBatch(classCodes, term); } // 步骤3:聚合判断预警条件 public boolean isMajorAtRisk(String major, String term) { List<String> classes = getClassCodesByMajor(major); Map<String, ClassStat> stats = batchClassStats(classes, term); double avgOfAvgs = stats.values().stream() .mapToDouble(ClassStat::getAvgScore).average().orElse(0.0); double passRate = stats.values().stream() .mapToDouble(ClassStat::getPassRate).average().orElse(0.0); return avgOfAvgs < 65.0 && (100.0 - passRate) > 25.0; } }

玄学提示:batchClassStats方法若用IN语句传入 50+ 班级代码,MySQL 可能触发max_allowed_packet限制(默认 4MB)。实测解决方案:

  • 将classCodes拆成每 20 个一组,循环调用;
  • 或改用临时表:CREATE TEMPORARY TABLE tmp_classes(code VARCHAR(10)); INSERT INTO tmp_classes VALUES (...); JOIN tmp_classes——后者在 200+ 班级时快 3 倍。

4. 避坑:上线前必须验证的 4 个致命陷阱

4.1 现象:导出 Excel 时中文乱码,日志显示java.io.IOException: Invalid byte 1 of 1-byte UTF-8 sequence

原因:POI 读取模板时默认用ISO-8859-1解码,而模板文件实际是 UTF-8 编码。
解决:

  • 模板文件用记事本另存为 → 编码选“UTF-8 无 BOM”;
  • Java 代码中显式指定编码:
    try (InputStream is = new FileInputStream("template.xlsx")) { Workbook workbook = new XSSFWorkbook(is); // XSSF for .xlsx // 不要用 WorkbookFactory.create(is),它会自动探测编码并出错 }

4.2 现象:SELECT ... GROUP BY course_id返回结果顺序随机,前端表格每刷新一次列顺序就变

原因:MySQL 5.7 默认开启ONLY_FULL_GROUP_BY,但未指定ORDER BY时,GROUP BY结果无序;MySQL 8.0+ 默认仍无序。
解决:

  • 强制排序:所有GROUP BY查询末尾加ORDER BY,哪怕只是ORDER BY course_id;
  • 禁止依赖隐式排序:在 MyBatis XML 中写死<select> ... GROUP BY ... ORDER BY c.id</select>,不靠 Java 层Collections.sort()补救。

4.3 现象:并发录入成绩时,出现同一学生同一课程同一学期两条记录

原因:前端未禁用提交按钮,用户手抖双击;或网络延迟导致两次请求都到达服务端。
解决:

  • 数据库层:UNIQUE KEY uk_stu_course_term (student_id, course_id, term, exam_type)已建,但需捕获异常;
  • Service 层:
    try { scoreMapper.insert(score); } catch (DuplicateKeyException e) { throw new BusinessException("成绩已存在:" + score.getStudentId() + "-" + score.getCourseId() + "-" + score.getTerm()); }

4.4 现象:score表score字段为DECIMAL(5,2),但 Java 实体用double接收,导致99.95存成99.94999999999999

原因:double是二进制浮点数,无法精确表示十进制小数。
解决:

  • 实体字段必须用BigDecimal:
    private BigDecimal score; // ✅ // private double score; // ❌
  • MyBatis TypeHandler 保证精度:MyBatis-Plus 默认用BigDecimalTypeHandler,无需额外配置,但需确认application.yml中未覆盖:
    mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler # 删掉任何关于 type-handlers 的自定义配置,让 BigDecimal 自动生效

5. 进阶技巧:用 MySQL 窗口函数实现“免代码”动态排名与同比分析

5.1 班级内排名:一行 SQL 替代 Java 循环排序

传统做法:查出全班成绩列表 → Java 用Collections.sort()→ 遍历赋值rank字段。
问题:1000 人班级排序耗时 15ms,且无法利用数据库索引。

MySQL 8.0+ 窗口函数方案:

SELECT s.id, st.name AS studentName, c.name AS courseName, s.score, ROW_NUMBER() OVER (PARTITION BY s.course_id ORDER BY s.score DESC) AS rankInCourse, RANK() OVER (PARTITION BY st.class_id ORDER BY s.score DESC) AS rankInClass, DENSE_RANK() OVER (ORDER BY s.score DESC) AS globalRank FROM score s JOIN student st ON s.student_id = st.id JOIN course c ON s.course_id = c.id WHERE s.term = '2023-2024-1' AND c.code = 'CS101';

参数说明:

  • ROW_NUMBER():严格序号,相同分数不同名次(95,95,90 → 1,2,3);
  • RANK():跳跃序号,相同分数同名次(95,95,90 → 1,1,3);
  • DENSE_RANK():连续序号,相同分数同名次(95,95,90 → 1,1,2);
  • PARTITION BY定义分组边界,ORDER BY定义组内排序依据。

5.2 同比分析:不用 Java 计算,SQL 直出“较上学期提升/下降”百分比

需求:展示某课程本学期平均分 vs 上学期平均分的变化率。

用LAG()窗口函数一步到位:

WITH term_avg AS ( SELECT course_id, term, AVG(score) AS avg_score, ROW_NUMBER() OVER (PARTITION BY course_id ORDER BY term DESC) AS rn FROM score WHERE term IN ('2023-2024-1', '2022-2023-2') -- 锁定两个学期 GROUP BY course_id, term ) SELECT c.name AS courseName, t1.term AS currentTerm, t1.avg_score AS currentAvg, t2.avg_score AS lastAvg, ROUND((t1.avg_score - t2.avg_score) / NULLIF(t2.avg_score, 0) * 100, 2) AS changePercent FROM term_avg t1 JOIN term_avg t2 ON t1.course_id = t2.course_id AND t1.rn = 1 AND t2.rn = 2 JOIN course c ON t1.course_id = c.id;

关键点:

  • NULLIF(t2.avg_score, 0)防止除零错误,返回NULL;
  • ROUND(..., 2)控制小数位,避免12.333333333333334;
  • WITH子句提高可读性,实际执行计划与嵌套子查询无差异。

5.3 异常检测:用标准差识别“离群成绩”

教务老师常问:“这个班有没有异常高分或低分?”
手动看太慢,用STDDEV_POP()一键定位:

SELECT st.name AS studentName, c.name AS courseName, s.score, ROUND(AVG(s.score) OVER (PARTITION BY s.course_id), 2) AS courseAvg, ROUND(STDDEV_POP(s.score) OVER (PARTITION BY s.course_id), 2) AS courseStddev, CASE WHEN s.score > AVG(s.score) OVER (PARTITION BY s.course_id) + 2 * STDDEV_POP(s.score) OVER (PARTITION BY s.course_id) THEN '超高分' WHEN s.score < AVG(s.score) OVER (PARTITION BY s.course_id) - 2 * STDDEV_POP(s.score) OVER (PARTITION BY s.course_id) THEN '超低分' ELSE '正常' END AS anomalyFlag FROM score s JOIN student st ON s.student_id = st.id JOIN course c ON s.course_id = c.id WHERE s.term = '2023-2024-1';

为什么用 2 倍标准差?
统计学中,正态分布下约 95% 数据落在μ±2σ内。成绩虽非严格正态,但此阈值在实践中误报率 < 3%,且能覆盖绝大多数作弊、阅卷失误、录入错误场景。比固定阈值(如 <40)更适应不同课程难度。

我带团队落地这个系统时,最初用 Java 计算标准差,10 万条成绩要 800ms;改用窗口函数后压到 120ms,且结果更稳定——因为数据库引擎对聚合运算做了深度优化,而 JVM 的DoubleStream.average()在大数据量下 GC 压力明显。现在每次教务会议前,运维同事直接跑一条 SQL 就能输出异常名单,没人再翻原始表。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 11:40:47

Open-Shell深度配置指南:在Win11上找回经典开始菜单与右键菜单

1. 开始菜单被改得面目全非之后&#xff0c;Open-Shell为什么还有市场如果你是从Windows 7时代一路用过来的老用户&#xff0c;第一次打开Windows 11的开始菜单&#xff0c;大概率会愣一下&#xff1a;图标居中、磁贴没了、右键菜单折叠、“关机”按钮藏得严严实实&#xff0c;…

作者头像 李华
网站建设 2026/10/5 11:40:21

东软防火墙配置实战:从安全区域到NAT策略与验收

简介&#xff1a;一份面向网络管理员与防火墙运维人员的东软防火墙配置实操文档&#xff0c;系统梳理从设备上电到策略下发的完整过程。内容按步骤展开&#xff0c;涵盖初始化设备、修改主机名与系统时间、设定系统语言、更改 root 默认口令、添加 Web/Telnet/SSH/SCM 等登录方…

作者头像 李华
网站建设 2026/10/5 11:39:25

FTP服务系统设计与实现:从被动模式到断点续传的工程实践

简介&#xff1a;这是一份面向计算机专业毕业设计者的完整论文文档&#xff0c;主题为FTP服务系统的设计与实现。论文基于软件工程方法&#xff0c;从FTP协议基础讲起&#xff0c;分析了文件传输原理、客户端/服务器架构、主动与被动两种传输模式&#xff0c;并围绕用户身份验证…

作者头像 李华
网站建设 2026/10/5 11:38:06

AI编程重塑程序员能力模型:从写代码到指挥AI的工作流转型

1. 先别急着下结论&#xff1a;AI写的代码到底处在什么水平最近半年我一直在密集使用各种 AI 编程工具&#xff0c;从 Cursor 到 Copilot&#xff0c;再到字节的 Trae、阿里的通义灵码&#xff0c;基本主流的都轮了一遍。实测下来的结论可能会出乎很多人的意料&#xff1a;在不…

作者头像 李华
网站建设 2026/10/5 11:37:36

C语言atoi函数详解:从原理到安全替代方案

如果你写过哪怕三天的C语言&#xff0c;一定绕不开一个名字很怪的函数&#xff1a;atoi。每次在标准库里看到它&#xff0c;我都会想&#xff1a;这名字到底是缩写还是拼写错误。其实它就是在告诉你“ASCII to Integer”&#xff0c;把字符串变成整型数。它解决的问题很直接&am…

作者头像 李华
网站建设 2026/10/5 11:35:15

C#中typeof()与GetType()的区别:编译期与运行时类型获取详解

写这篇文章的念头&#xff0c;源于我前阵子在一个内部权限框架里排查审计日志时踩的一个坑。当时要打印某个实体对象的完整类型名&#xff0c;我顺手在日志模板里写死了typeof(UserModel).Name&#xff0c;结果所有继承自UserModel的扩展用户类型全部打成了基类名字&#xff0c…

作者头像 李华