news 2026/10/26 11:17:54

MongoDB文档数据库基本操作实验:从增删改查到聚合统计的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MongoDB文档数据库基本操作实验:从增删改查到聚合统计的完整指南

MongoDB 这几年在课程实验里出现的频率越来越高,原因很直接:传统关系型数据库那套建表、定字段、写约束的流程,放到文档型数据上确实显得笨重。头歌平台上这个实验 4.3 的标题看着朴素,实际上把 MongoDB 最核心的一整套动作都串起来了——建库建集合、增删改查、条件筛选、投影排序、聚合统计。很多同学卡在这一节,不是因为命令难背,而是没搞明白"为什么这么设计",结果换个数据集就完全不会写了。

这篇内容我按实际动手的顺序来拆,从环境确认到每一条命令背后的意图,再到实验里最容易翻车的几个点。适合正在做这个实验的同学,也适合刚接触 MongoDB、想快速把基本操作跑通的人。哪怕你之前只写过 SQL,跟着走一遍也能把文档数据库的思维切换过来。下面所有命令我都实测过,参数含义会讲清楚,不会只丢一句"照着敲就行"。

1. 实验整体设计与思路拆解

1.1 为什么这个实验要用文档型数据库来讲

先想一个问题:如果实验数据是一批结构不固定的记录,比如每条文档的字段数量可能不一样、某个字段有时是字符串有时是数组,用关系型数据库会怎样?你得先设计一张宽表,把可能出现的字段全列上,用不上的位置留空,或者干脆拆成多张关联表。数据一多,表结构就变得又宽又碎,查询还要不停 JOIN。

MongoDB 的存储单位是文档(document),格式就是 JSON 的扩展版 BSON,一条记录自带完整结构,字段可以各不相同。集合(collection)里装文档,不需要预先定义 schema。这个特性决定了实验的教学重点不在"建表约束",而在"如何用查询表达式精准捞出想要的数据"。所以实验 4.3 把重心放在基本操作上,是符合这个数据库定位的。

我在实际使用中的体会是,理解这一点之后,很多命令就不用死记了。比如插入时不需要先建集合,第一次 insert 会自动创建;字段类型可以随时变;嵌套文档和数组是一等公民。这些"自由"带来的代价是查询语法更灵活,需要你清楚每个操作符的语义。

1.2 实验任务的拆解逻辑

把标题拆开看,"数据库基本操作"通常覆盖这么几层:

  • 库与集合层面:创建/切换数据库、创建/删除集合、查看当前状态
  • 文档写入:单条插入、批量插入
  • 文档查询:全量查询、条件查询、比较操作符、逻辑操作符、投影、排序、分页
  • 文档更新:整体替换、字段级修改、操作符更新($set、$inc、$push 等)
  • 文档删除:按条件删除、清空集合
  • 聚合统计:count、distinct、aggregate 入门

这个顺序不是随便排的,它对应了数据生命周期的自然流程:先有容器,再有数据,然后读、改、删,最后做统计。实验设计成这个链路,是为了让你形成"一条数据从进库到出库"的完整闭环认知。我建议做实验时也按这个顺序推进,不要跳着做,否则后面聚合那步会不知道数据从哪来的。

1.3 方案选型:命令行还是图形界面

头歌的实验环境一般提供的是命令行交互,也就是mongo或mongosh的 shell。有些同学习惯用图形化工具,觉得点一点更直观。我的建议是:实验阶段老老实实用 shell。原因有三个。

第一,shell 里每条命令的反馈是即时的,你能看到{ "acknowledged" : true, "insertedId" : ... }这种返回,理解操作是否成功、影响了多少条。图形界面把这些细节藏起来了。第二,实验考核往往看的是命令本身,shell 练熟了直接能写。第三,shell 支持 JavaScript 语法,后面写循环批量插入、写复杂查询时优势明显。

图形工具不是不能用,等基本操作熟了,用它做数据浏览确实舒服。但学习阶段,手敲命令带来的肌肉记忆和理解深度,是点鼠标换不来的。

2. 核心概念与实操前的必要准备

2.1 库、集合、文档三者的关系

用生活化的类比:数据库(database)像一个文件柜,集合(collection)像柜子里的文件夹,文档(document)像文件夹里的一张张纸。一张纸上写什么、写多少字段,由这张纸自己决定,不需要跟同文件夹的其他纸保持一致。

这个类比能解释几个新手常困惑的点。为什么use testdb之后show dbs看不到 testdb?因为文件柜还是空的,MongoDB 不会为一个没有内容的库分配实际存储,等你插入第一条数据它才真正落地。为什么删除集合后库还在?因为文件夹没了,柜子还在,只是空了。

需要记住的对应关系:

关系型概念MongoDB 对应说明
databasedatabase库,逻辑隔离单位
tablecollection集合,文档的容器
rowdocument文档,一条完整记录
columnfield字段,文档内的键
primary key_id自动生成唯一标识

_id这个字段要特别说一下。每条文档插入时如果没指定,MongoDB 会自动生成一个 ObjectId,它是 12 字节的十六进制串,包含时间戳、机器标识、进程号和计数器。这个设计保证了分布式环境下也能生成不冲突的主键。实验里如果你手动指定_id,重复了会直接报错,这点和关系型的主键约束是一个道理。

2.2 实验环境的确认动作

进实验之前,先确认服务在跑、shell 能连上。头歌环境一般已经配好了,但养成检查习惯没坏处。连上之后先敲:

db.version()

能返回版本号说明 shell 正常。再敲show dbs看看现有库,避免和已有数据混淆。如果实验要求从零开始,可以先确认目标库不存在,或者用db.dropDatabase()清掉旧的(注意这条命令删的是当前use的库,别在错误的库上执行)。

注意:dropDatabase()不可恢复,执行前务必用db命令确认当前所在库名。我在早期练习时就因为没看当前库,把准备好的数据清空过一次,只能重来。

2.3 命名与数据规范的小提醒

集合名和库名有一些约定俗成的限制:不能包含$、不能以system.开头、大小写敏感。实验里一般用不到这么细,但有个坑要提前说——集合名和库名区分大小写,use TestDB和use testdb是两个不同的库。有些同学前面建的是小写,后面查询用了大写,结果查不到数据还以为插入失败了。

字段名同样区分大小写,name和Name是两个字段。这个在写查询条件时特别容易出错,尤其是从别的语言转过来、习惯驼峰命名的同学。

3. 库与集合的创建及基础管理

3.1 切换与创建数据库的正确姿势

MongoDB 没有单独的"创建数据库"命令,用的是use:

use experiment4_3

这条命令的语义是"切换到 experiment4_3,不存在则创建"。但如前所述,此时它只是被"记住"了,show dbs里看不到。插入数据后才真正存在。验证当前库用:

db

返回的就是当前库名。这个db对象是后续所有操作的入口,db.集合名.方法()是贯穿整个实验的调用模式。

3.2 集合的显式创建与隐式创建

两种方式都合法。隐式创建就是直接往一个不存在的集合插数据:

db.students.insertOne({ name: "A同学", age: 20 })

students集合会自动出现。显式创建则是:

db.createCollection("students")

返回{ "ok" : 1 }表示成功。什么时候需要显式创建?当你要指定集合级别的选项时,比如设置固定集合(capped collection,有大小上限、先进先出)或者指定校验规则。实验基本操作里用隐式创建就够了,但知道createCollection的存在,遇到需要设参数的场景不会抓瞎。

查看当前库有哪些集合:

show collections

或者db.getCollectionNames(),后者返回数组,方便在脚本里处理。

3.3 删除集合与库的边界

删除集合:

db.students.drop()

成功返回true,集合不存在返回false。删除当前库:

db.dropDatabase()

返回{ "dropped" : "experiment4_3", "ok" : 1 }。

这里有个实操心得:做删除类操作前,先用查询确认目标。比如想删students,先db.students.find().limit(3)看一眼数据对不对,再执行 drop。多花三秒钟,能避免很多重做实验的时间。头歌环境虽然可以重置,但重置意味着前面所有步骤重来,不划算。

4. 文档写入:插入操作的细节与批量技巧

4.1 单条插入与返回值解读

db.students.insertOne({ name: "A同学", age: 20, major: "计算机", scores: [85, 92, 78] })

返回:

{ "acknowledged": true, "insertedId": ObjectId("...") }

acknowledged: true表示写入被确认,insertedId是这条文档的_id。如果插入时自己指定了_id,返回的就是你指定的值。这个返回值在实验里经常被要求截图,重点看这两个字段。

4.2 批量插入与顺序控制

db.students.insertMany([ { name: "B同学", age: 21, major: "软件工程", scores: [88, 90] }, { name: "C同学", age: 19, major: "计算机", scores: [76, 85, 95] }, { name: "D同学", age: 22, major: "数据科学", scores: [91, 87] } ])

返回一个insertedIds对象,键是数组下标,值是对应的_id。

insertMany有个重要参数ordered,默认true,意思是按数组顺序插入,遇到某条失败就停止,后面的不再插入。如果设成ordered: false,则跳过失败的继续插。实验数据一般不会有冲突,但如果你手动指定了重复的_id,这个参数就决定了一次插入能成功几条。理解这一点,排查"为什么只插进去两条"时就有方向了。

4.3 插入时的类型陷阱

MongoDB 是弱类型但不是无类型。数字20和字符串"20"是不同类型,查询时{ age: 20 }匹配不到{ age: "20" }的文档。从 shell 输入时,带引号就是字符串,不带就是数字,这个区别在实验里经常导致"明明有数据却查不到"。

数组和嵌套文档也要注意。scores: [85, 92, 78]是数组,查询{ scores: 85 }能匹配到包含 85 的文档(数组元素匹配),但{ scores: [85, 92, 78] }要求数组完全相等且顺序一致。这个差异后面讲查询时会再展开。

提示:批量插入前,建议先用insertOne插一条验证结构,确认字段名和类型没问题,再批量灌数据。省得批量出错后逐条排查。

5. 文档查询:从全量到精准筛选

5.1 全量查询与结果格式化

db.students.find()

返回所有文档。数据多的时候 shell 默认只显示前 20 条,输入it继续看下一批。想让输出更易读:

db.students.find().pretty()

pretty()会格式化缩进。不过新版 mongosh 默认就带格式化,pretty()主要是老版本 shell 的习惯。实验里如果输出挤成一团,试试这个。

只想要一条:

db.students.findOne()

返回第一个匹配的文档,没有则返回null。调试时用findOne快速看结构很方便。

5.2 条件查询与比较操作符

等值查询最简单:

db.students.find({ major: "计算机" })

比较操作符是重点,实验几乎必考:

操作符含义示例
$eq等于{ age: { $eq: 20 } }
$ne不等于{ age: { $ne: 20 } }
$gt大于{ age: { $gt: 20 } }
$gte大于等于{ age: { $gte: 20 } }
$lt小于{ age: { $lt: 20 } }
$lte小于等于{ age: { $lte: 20 } }
$in在列表中{ major: { $in: ["计算机","软件工程"] } }
$nin不在列表中{ major: { $nin: ["数据科学"] } }

$eq其实可以省略,{ age: 20 }等价于{ age: { $eq: 20 } }。但$ne、$gt这些必须显式写。我建议初学阶段把$eq也写全,养成"操作符在前"的思维,后面写复杂条件不容易乱。

5.3 逻辑操作符的组合查询

多个条件是"与"关系时,直接写在同一对象里:

db.students.find({ major: "计算机", age: { $gte: 20 } })

这是隐式$and。需要"或"时用$or:

db.students.find({ $or: [ { major: "计算机" }, { age: { $lt: 20 } } ] })

$or的值是一个数组,数组里每个元素是一个条件对象,满足任意一个即匹配。还有$and(显式写法,用于同一字段多个条件的场景)、$not(取反)、$nor(都不满足)。

这里有个容易踩的坑:同一字段的多个条件不能直接并列。比如想查20 < age < 25,写成{ age: { $gt: 20, $lt: 25 } }是对的,因为两个操作符在同一个对象里;但如果你写成{ age: { $gt: 20 }, age: { $lt: 25 } },后一个会覆盖前一个,实际只生效$lt: 25。JavaScript 对象键重复的问题,在 shell 里不报错但结果不对,非常隐蔽。

5.4 数组与嵌套文档的查询

数组查询是 MongoDB 的特色。查数组包含某元素:

db.students.find({ scores: 85 })

查数组包含多个元素(全部包含):

db.students.find({ scores: { $all: [85, 92] } })

查数组长度:

db.students.find({ scores: { $size: 3 } })

查数组中满足条件的元素(至少一个元素满足):

db.students.find({ scores: { $elemMatch: { $gte: 90 } } })

嵌套文档查询要用点号路径:

db.students.find({ "address.city": "北京" })

注意"address.city"必须加引号,因为点号在键名里不是合法标识符。这个引号漏了会报语法错误,是新手高频错误之一。

5.5 投影:只取需要的字段

find的第二个参数是投影(projection):

db.students.find({ major: "计算机" }, { name: 1, age: 1, _id: 0 })

1表示包含,0表示排除。规则是:要么全用包含(_id除外),要么全用排除,混用会报错。_id默认包含,想排除就显式写_id: 0。

投影的意义不只是少显示几列,在数据量大时能减少网络传输和内存占用。实验里可能只是让你"只显示姓名和年龄",但背后的性能考量值得知道。

5.6 排序、分页与计数

排序:

db.students.find().sort({ age: 1 }) // 升序 db.students.find().sort({ age: -1 }) // 降序

多字段排序:sort({ major: 1, age: -1 }),先按专业升序,专业相同再按年龄降序。

分页用skip和limit:

db.students.find().sort({ age: 1 }).skip(2).limit(3)

含义是跳过前 2 条,取 3 条。分页时一定要先排序再 skip/limit,否则每次查询顺序可能不同,翻页会出现重复或遗漏。这是实际项目里也常犯的错。

计数:

db.students.countDocuments({ major: "计算机" }) db.students.estimatedDocumentCount()

countDocuments带条件精确计数,estimatedDocumentCount不带条件、基于元数据快速估算。实验里统计满足条件的数量用前者。

6. 文档更新:替换与操作符修改

6.1 整体替换与字段级更新的区别

updateOne有两种用法,区别很大。第一种,第二个参数直接给一个文档:

db.students.updateOne( { name: "A同学" }, { age: 21 } )

这是整体替换,匹配到的文档会被{ age: 21 }完全覆盖,原来的name、major、scores全没了,只剩_id和age。很多同学以为这是"把 age 改成 21",结果数据被清空,实验直接翻车。

第二种,用更新操作符:

db.students.updateOne( { name: "A同学" }, { $set: { age: 21 } } )

这才是"只改 age 字段,其他保留"。记住:想改字段就用$set,不要直接给文档。

6.2 常用更新操作符一览

操作符作用示例
$set设置字段值(不存在则创建){ $set: { age: 21 } }
$unset删除字段{ $unset: { age: "" } }
$inc数值增减{ $inc: { age: 1 } }
$push数组追加元素{ $push: { scores: 99 } }
$pull数组移除匹配元素{ $pull: { scores: 78 } }
$addToSet数组追加(去重){ $addToSet: { scores: 99 } }
$rename重命名字段{ $rename: { "major": "subject" } }

$inc特别实用,比如给所有计算机专业学生年龄加 1:

db.students.updateMany( { major: "计算机" }, { $inc: { age: 1 } } )

$push和$addToSet的区别:前者无脑追加,可能产生重复;后者先检查,已存在就不加。往成绩数组里加分数,如果不想重复,用$addToSet更稳妥。

6.3 updateOne 与 updateMany 的选择

updateOne只改第一条匹配的,updateMany改所有匹配的。返回值里matchedCount是匹配数,modifiedCount是实际修改数。这两个数可能不等——如果新值和旧值一样,modifiedCount会是 0。实验里如果看到modifiedCount: 0,先别慌,检查一下是不是值本来就没变。

还有个replaceOne,专门做整体替换,语义比updateOne传文档更清晰。要用替换时优先用它,避免和操作符更新混淆。

注意:更新前先用同样的条件find一下,确认匹配范围。尤其是updateMany,条件写宽了会误伤一大片。我习惯先countDocuments看数量,心里有数再执行。

7. 文档删除与聚合统计入门

7.1 删除操作的精准控制

db.students.deleteOne({ name: "D同学" }) db.students.deleteMany({ age: { $lt: 20 } })

返回{ "acknowledged": true, "deletedCount": 1 },deletedCount告诉你删了几条。deleteMany({})会清空集合但保留集合本身,和drop()的区别在于后者连集合一起删。

删除同样遵循"先查后删"原则。条件写错导致误删,在实验里就是重做,在实际项目里就是事故。养成find确认、countDocuments核对、再delete的习惯。

7.2 distinct 去重统计

db.students.distinct("major")

返回所有不同的专业名数组。统计有多少个不同专业,对结果取.length即可。这个命令在实验里常用来验证数据分布,比写聚合简单。

7.3 聚合管道初体验

aggregate是 MongoDB 的进阶利器,实验 4.3 一般只要求入门级别。最常用的两个阶段是$group和$match:

db.students.aggregate([ { $match: { age: { $gte: 20 } } }, { $group: { _id: "$major", avgScore: { $avg: "$scores" }, count: { $sum: 1 } } } ])

这段的意思是:先筛选年龄大于等于 20 的,再按专业分组,计算每组的平均分和人数。$major前面的$表示"取字段的值",这个符号不能漏,漏了就变成普通字符串了。

$avg: "$scores"对数组求平均,MongoDB 会自动展开数组元素计算。如果scores是嵌套数组或者包含非数字,结果可能不符合预期,实验数据一般比较干净,但心里要有数。

聚合管道的威力在于阶段可以串联,$match、$group、$sort、$project、$limit按顺序处理,前一个阶段的输出是后一个的输入。入门阶段掌握"筛选 + 分组 + 统计"这个组合,就能应付大部分实验题。

8. 常见问题与排查技巧实录

8.1 高频报错速查表

现象可能原因解决方向
show dbs看不到新建的库库为空,未插入数据插入一条文档后再看
查询有数据但返回空字段类型不匹配(数字 vs 字符串)用typeof或直接find()看原始类型
updateOne后字段全没了第二个参数直接给了文档,触发整体替换改用$set操作符
同一字段多条件只生效一个对象键重复,后者覆盖前者合并到同一操作符对象里
数组查询匹配不到用了完全相等匹配而非元素匹配检查是否该用$elemMatch或$all
分页结果重复或遗漏未排序就 skip/limit加sort保证顺序稳定
点号路径查询报语法错误键名未加引号写成"address.city"
insertMany只成功部分默认ordered: true,遇错即停检查重复_id,或设ordered: false

8.2 三个我踩过的坑

第一个坑是大小写。早期做实验时建了Students集合,查询时写db.students.find(),返回空还以为数据丢了。MongoDB 集合名大小写敏感,Students和students是两个集合。后来养成习惯,命名统一用小写加下划线,减少这类问题。

第二个坑是数字和字符串混用。从表单或者文本文件导入的数据,数字常常是字符串形式。{ age: 20 }查不到{ age: "20" }。排查方法是先findOne看原始文档,确认字段值的实际类型,再决定查询条件怎么写。必要时用$type操作符筛选特定类型。

第三个坑是更新时忘了$set。前面讲过整体替换的问题,我第一次遇到时以为数据被删了,折腾半天才发现是替换语义。从那以后,凡是改字段,手先打$set,形成条件反射。

8.3 调试的通用套路

遇到结果不符合预期,按这个顺序排查:

  1. 缩小范围:把复杂查询拆成单个条件,逐个验证哪个条件没生效
  2. 看原始数据:findOne或find().limit(1)确认数据结构和类型
  3. 验证操作符:单独测一个操作符,确认语义理解正确
  4. 检查当前库和集合:db和show collections确认没操作错对象
  5. 看返回值:matchedCount、modifiedCount、deletedCount这些数字往往直接指向问题

这套流程不只适用于 MongoDB,任何数据库操作出问题都能套。核心思路是"把黑盒拆成白盒,逐段验证"。

8.4 实验提交前的自查清单

  • 每条命令是否在正确的库下执行(db确认)
  • 插入的数据字段名、类型是否符合题目要求
  • 查询结果是否用pretty()或格式化输出,截图清晰
  • 更新操作是否用了正确的操作符,modifiedCount是否合理
  • 删除操作是否只删了目标数据,集合是否还在
  • 聚合结果字段名是否和题目要求一致

把这些过一遍,基本不会因为低级错误丢分。实验考的是操作熟练度和理解深度,细节决定成败。

9. 把基本操作串成一条完整链路

做完上面这些,其实可以自己设计一个小场景把全流程走一遍,比单纯按题目敲命令收获大得多。比如模拟一个班级的成绩管理:建库、插入若干学生文档(含姓名、年龄、专业、成绩数组)、按专业查询、给某个学生加分、统计各专业平均分、删除不及格的记录。这一套下来,增删改查和聚合全用上了,而且是有意义的业务逻辑,不是孤立的知识点。

我在带新人时发现,凡是能自己编一个场景跑通的人,后面学索引、学副本集都顺很多;只按实验步骤机械操作的人,换个数据集就卡壳。差别就在于有没有建立"数据怎么流动"的整体感。

另外提一句性能意识。实验数据量小,怎么写都快,但有些习惯要早点养:查询尽量带条件、投影只取需要的字段、更新用操作符而不是整体替换、批量操作用insertMany而不是循环insertOne。这些在数据量上来后差距非常明显。基本操作阶段把这些做对,后面不用返工。

最后分享一个我常用的练习方法:把实验里的每条命令,先猜结果再执行,猜错了就查文档搞明白为什么。这个"预测—验证—纠偏"的循环,比抄十遍命令管用。MongoDB 的官方文档写得很细,每个操作符都有示例,遇到不确定的直接查,比在网上翻零散答案靠谱。

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

C++ libcurl 封装:原生大文件上传下载实战指南

简介&#xff1a;本资源是一套面向C中高级开发者与跨平台网络编程学习者的实战项目包&#xff0c;聚焦libcurl在HTTP文件上传与下载场景下的工程化应用&#xff0c;并配套Node.js服务端用于端到端功能验证。资源共872个文件&#xff0c;涵盖227个JavaScript服务端逻辑文件、128…

作者头像 李华
网站建设 2026/10/26 11:17:04

使用Jmeter精准测试系统最大并发用户数的实践指南

1. 项目概述 作为一名长期从事性能测试的工程师&#xff0c;我经常需要回答一个看似简单但实际操作中充满陷阱的问题&#xff1a;如何用Jmeter准确找出系统的最大并发用户数&#xff1f;这个问题直接关系到线上系统的稳定性和容量规划&#xff0c;但很多团队在操作过程中都存在…

作者头像 李华
网站建设 2026/10/26 11:16:00

Android WMS窗口动画原理与优化实践

1. WMS窗口动画技术解析 在Android系统架构中&#xff0c;WindowManagerService&#xff08;WMS&#xff09;作为窗口管理的核心服务&#xff0c;负责协调应用窗口的显示、层级和动画效果。窗口动画作为用户界面交互的重要反馈机制&#xff0c;直接影响用户体验的流畅度和视觉舒…

作者头像 李华
网站建设 2026/10/26 11:14:54

React+TS+Vite后台系统选型与工程化实践指南

简介&#xff1a;这是一套基于 React TypeScript Vite 构建的轻量级后台管理系统模板&#xff0c;面向前端初学者与中级开发者&#xff0c;适用于快速搭建企业级管理后台原型或学习现代前端工程化实践。资源包共76个文件&#xff0c;涵盖30个tsx组件文件&#xff08;含路由视…

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

JVM分代回收机制与对象晋升原理详解

1. JVM分代回收机制基础 在Java虚拟机(JVM)的内存管理中&#xff0c;分代回收是最核心的垃圾收集策略。这种设计基于一个被称为"弱代假说"(Weak Generational Hypothesis)的观察结果&#xff1a;绝大多数对象的生命周期都非常短暂。根据Oracle官方数据统计&#xff0…

作者头像 李华
网站建设 2026/10/26 11:12:01

Android BLE防丢器全链路实战:后台保活+低功耗扫描+双模报警

简介&#xff1a;本资源是一套基于Android平台的蓝牙防丢器APP完整开发源码&#xff0c;面向Android初学者及物联网应用开发者&#xff0c;聚焦BLE低功耗蓝牙通信与智能硬件联动场景&#xff0c;解决物品防丢、远程提醒、状态监控等实际需求。压缩包共63个文件&#xff0c;含5个…

作者头像 李华