
本文介绍如何在go中结合mongodb驱动(mgo或官方driver)构建条件聚合查询,通过$match、$unwind、$group和$project等阶段,按学生rollno和name统计指定时间范围内各科的出勤百分比。
本文介绍如何在go中结合mongodb驱动(mgo或官方driver)构建条件聚合查询,通过$match、$unwind、$group和$project等阶段,按学生rollno和name统计指定时间范围内各科的出勤百分比。
在实际教务系统开发中,常需基于多维条件(如学院、专业、学期、班级、课程及日期范围)动态生成学生出勤率报表。MongoDB 的聚合管道(Aggregation Pipeline)是解决此类“条件性分组统计”问题的理想方案。本文以计算每位学生的出勤百分比为例,完整演示从需求分析到Go代码落地的全过程。
核心思路解析
目标是将嵌套在 atndnc 数组中的每位学生多次打卡记录(跨多天),按 rollno + name 分组,统计其总课时数(即匹配条件的总记录数)与出勤次数(attend: true 的数量),最终计算百分比:
prcntg = (出勤次数 / 总课时数) × 100
这需要四步聚合操作:
- $match:筛选符合条件的文档(college_id、stream、semester、section、subject 及 date 范围);
- $unwind:将 atndnc 数组展开为独立文档流,使每个学生单次记录可被单独处理;
- $group:按 rollno 和 name 分组,同时用 $sum 和 $cond 实现条件计数(true 计1,false 计0);
- $project:整理输出字段,计算百分比并保留整数或保留一位小数。
完整Go代码示例(适配官方mongo-go-driver)
以下代码使用 MongoDB 官方 Go Driver(v1.12+),结构清晰、类型安全,并支持日期范围查询:
package main
import (
"context"
"fmt"
"log"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type AttendanceReport struct {
Rollno string `bson:"rollno"`
Name string `bson:"name"`
Prcntg float64 `bson:"prcntg"`
}
func getStudentAttendancePercentage(
client *mongo.Client,
dbName, collectionName string,
filter map[string]interface{},
startDate, endDate time.Time,
) ([]AttendanceReport, error) {
coll := client.Database(dbName).Collection(collectionName)
// 构建聚合管道
pipeline := []bson.M{
// Step 1: 匹配基础条件 + 日期范围(注意:date 字段需在索引中优化)
{"$match": bson.M{
"college_id": filter["college_id"],
"stream": filter["stream"],
"semester": filter["semester"],
"section": filter["section"],
"subject": filter["subject"],
"date": bson.M{
"$gte": startDate,
"$lte": endDate,
},
}},
// Step 2: 展开 atndnc 数组
{"$unwind": "$atndnc"},
// Step 3: 按学生分组,条件统计出席次数 & 总次数
{"$group": bson.M{
"_id": bson.M{
"rollno": "$atndnc.rollno",
"name": "$atndnc.name",
},
"total": bson.M{"$sum": 1},
"present": bson.M{
"$sum": bson.M{
"$cond": bson.M{
"if": "$atndnc.attend",
"then": 1,
"else": 0,
},
},
},
}},
// Step 4: 计算百分比,投影所需字段
{"$project": bson.M{
"_id": 0,
"rollno": "$_id.rollno",
"name": "$_id.name",
"prcntg": bson.M{
"$round": []interface{}{bson.M{"$multiply": []interface{}{bson.M{"$divide": []interface{}{"$present", "$total"}}, 100}}, 1},
},
}},
}
cursor, err := coll.Aggregate(context.TODO(), pipeline)
if err != nil {
return nil, fmt.Errorf("aggregation failed: %w", err)
}
defer cursor.Close(context.TODO())
var results []AttendanceReport
if err = cursor.All(context.TODO(), &results); err != nil {
return nil, fmt.Errorf("decode results failed: %w", err)
}
return results, nil
}
// 使用示例
func main() {
client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
log.Fatal(err)
}
defer client.Disconnect(context.TODO())
filter := map[string]interface{}{
"college_id": "tisl",
"stream": "CS",
"semester": "sem3",
"section": "A",
"subject": "PH301",
}
start := time.Date(2016, 4, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2016, 4, 30, 23, 59, 59, 0, time.UTC)
reports, err := getStudentAttendancePercentage(client, "school_db", "attendance", filter, start, end)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Attendance Report (%d students):\n", len(reports))
for _, r := range reports {
fmt.Printf(`{"rollno":"%s","name":"%s","prcntg":%.1f},`, r.Rollno, r.Name, r.Prcntg)
}
}
关键注意事项
- ✅ 日期范围必须使用 $gte/$lte:确保 date 字段为 BSON Date 类型(非字符串),否则范围查询无效;
- ✅ $unwind 前务必 $match:先缩小文档集再展开数组,极大提升性能(避免冗余展开);
- ✅ $cond 是条件计数的核心:替代低效的 $sum 配合 $filter,直接在 $group 中完成逻辑判断;
- ⚠️ 避免 _id 冗余嵌套:原答案中 bson.M{"_id": bson.M{"rollno":"$atndnc.rollno"}} 易引发歧义;应明确分组键结构,便于后续 $project 提取;
- ? 生产环境建议添加索引:对高频查询字段组合(如 {"college_id":1,"stream":1,"semester":1,"section":1,"subject":1,"date":1})建立复合索引。
通过以上实现,你不仅能获得结构化的出勤率数组,还能灵活扩展——例如增加缺勤明细、导出Excel、对接前端图表等。聚合管道是MongoDB数据价值挖掘的基石,掌握其在Go中的工程化写法,将显著提升后端数据服务能力。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











