sunsheng 1 anno fa
parent
commit
4a55d20cd3

+ 165 - 0
src/api/exeamLog.go

@@ -5,11 +5,15 @@ import (
 	"copter-train/db/repo"
 	"copter-train/log"
 	"errors"
+	"fmt"
+	"net/url"
 	"time"
 
 	"github.com/gin-gonic/gin"
+	"github.com/xuri/excelize/v2"
 	"go.mongodb.org/mongo-driver/bson"
 	"go.mongodb.org/mongo-driver/bson/primitive"
+	"go.mongodb.org/mongo-driver/mongo"
 )
 
 // 提交考核试题
@@ -51,3 +55,164 @@ func ExeamLogList(c *gin.Context, apictx *ApiSession) (interface{}, error) {
 		Sort:        bson.M{"createTime": 1},
 	})
 }
+
+// 考试日期 用户名 编码 类型 分数 完成度 学习总时长
+type HandleLatestExamLog struct {
+	ExamDate     string `json:"examDate"`
+	UserName     string `json:"userName"`
+	Nid          string `json:"nid"`
+	Type         string `json:"type"`
+	Score        int    `json:"score"`
+	CompleteRate int    `json:"completeRate"`
+	LearnTime    int    `json:"learnTime"`
+}
+
+// 导出学生最后一场考核成绩
+func LatestExamExportExcel(c *gin.Context, apictx *ApiSession) (interface{}, error) {
+	isAdmin, err := IsAdmin(c, apictx)
+	if err != nil {
+		return nil, err
+	}
+	if !isAdmin {
+		return nil, errors.New("没有权限")
+	}
+	db := c.Param("scope")
+	if len(db) == 0 {
+		return nil, errors.New("scope不能为空")
+	}
+	f := excelize.NewFile()
+	f.SetDefaultFont("宋体")
+	flagIndex := -1
+
+	examTypes := []string{"理论", "实操"}
+
+	for _, examType := range examTypes {
+
+		index, _ := f.NewSheet(examType)
+		if flagIndex < 0 {
+			flagIndex = index
+		}
+		latestExamExcel := NewLatestExamExcel(f)
+		latestExamExcel.SheetName = examType
+
+		latestExamExcel.Title = "[定检维护] 尾传动轴机上维护测量"
+		latestExamLogs, err := getHandleLatestExamLog(apictx, db, examType)
+		if err != nil {
+			return nil, err
+		}
+
+		latestExamExcel.Content = latestExamLogs
+		latestExamExcel.Draws()
+
+	}
+
+	f.SetActiveSheet(flagIndex)
+	f.DeleteSheet("Sheet1")
+
+	fileName := url.PathEscape("[定检维护] 尾传动轴机上维护测量.xlsx")
+	c.Header("Content-Type", "application/octet-stream")
+	c.Header("Content-Disposition", "attachment; filename="+fileName)
+	c.Header("Content-Transfer-Encoding", "binary")
+
+	err = f.Write(c.Writer)
+	if err != nil {
+		return nil, err
+	}
+
+	return nil, nil
+}
+
+func getTotalLearnTime(apictx *ApiSession, db string, uid string) (int32, error) {
+	colls := apictx.CreateRepoCtx().Client.GetDbCollection(db, repo.CollectionLearnLog)
+
+	// 定义聚合管道
+	pipeline := mongo.Pipeline{
+		{{Key: "$match", Value: bson.D{{Key: "uid", Value: uid}}}},
+		{{Key: "$sort", Value: bson.D{{Key: "createTime", Value: -1}, {Key: "uid", Value: -1}}}},
+		{{Key: "$group", Value: bson.D{
+			{Key: "_id", Value: "$uid"},
+			{Key: "totalLearnTime", Value: bson.D{{Key: "$sum", Value: "$learnTime"}}},
+		}}},
+	}
+
+	// 执行聚合管道
+	cursor, err := colls.Aggregate(apictx.CreateRepoCtx().Ctx, pipeline)
+	if err != nil {
+		log.Error(err)
+		return 0, err
+	}
+
+	// 遍历结果
+	var results []bson.M
+	if err = cursor.All(apictx.CreateRepoCtx().Ctx, &results); err != nil {
+		log.Error(err)
+		return 0, err
+	}
+	fmt.Println(results)
+	if len(results) > 0 {
+		return results[0]["totalLearnTime"].(int32), err
+	}
+
+	return 0, nil
+}
+
+func getHandleLatestExamLog(apictx *ApiSession, db string, examType string) ([]*HandleLatestExamLog, error) {
+	colls := apictx.CreateRepoCtx().Client.GetDbCollection(db, repo.CollectionExeamLog)
+
+	pipeline := mongo.Pipeline{
+		{{Key: "$match", Value: bson.D{{Key: "type", Value: examType}}}},
+		{{Key: "$sort", Value: bson.D{{Key: "uid", Value: -1}}}},
+		{{Key: "$group", Value: bson.D{
+			{Key: "_id", Value: "$uid"},
+			{Key: "latestExeamLog", Value: bson.D{{Key: "$first", Value: "$$ROOT"}}},
+		}}},
+	}
+
+	// 执行聚合管道
+	cursor, err := colls.Aggregate(apictx.CreateRepoCtx().Ctx, pipeline)
+	if err != nil {
+		return nil, err
+	}
+
+	// 遍历结果
+	var results []*model.LatestExamLog
+	if err = cursor.All(apictx.CreateRepoCtx().Ctx, &results); err != nil {
+		return nil, err
+	}
+
+	if len(results) == 0 {
+		return nil, errors.New("没有找到数据")
+	}
+
+	latestExamLogs := make([]*HandleLatestExamLog, 0)
+	// 变量查询数据
+	// 考试日期 用户名 编码 类型 分数 完成度 学习总时长
+	for _, result := range results {
+		examLog := result.ExeamLog
+		// 查询用户信息
+		user, err := GetUserById(apictx, examLog.Uid)
+		if err != nil {
+			fmt.Println(err)
+		}
+		// 查询用户对应总时长
+		totalLearnTime, _ := getTotalLearnTime(apictx, db, examLog.Uid)
+		_completeRate, _ := getLearnLogStatistics(apictx, db, examLog.Uid)
+		completeRate := int(_completeRate * 100)
+		score := 0
+		if examLog.Score != nil {
+			score = *examLog.Score
+		}
+		latestExamLogs = append(latestExamLogs, &HandleLatestExamLog{
+			ExamDate:     examLog.CreateTime.Format("2006-01-02"),
+			UserName:     user.Name,
+			Nid:          user.Nid,
+			Type:         examLog.Type,
+			Score:        score,
+			CompleteRate: completeRate,
+			LearnTime:    int(totalLearnTime),
+		})
+
+	}
+	return latestExamLogs, nil
+
+}

+ 197 - 0
src/api/latestExamExcel.go

@@ -0,0 +1,197 @@
+package api
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/xuri/excelize/v2"
+)
+
+type LatestExamExcel struct {
+	Offset           int
+	Row              int
+	Title            string //标题
+	Excel            *excelize.File
+	SheetName        string
+	AlignCenterStyle int
+	Content          []*HandleLatestExamLog
+	RowMap           map[string]int
+	RowWidthArray    []float64
+	RowsHeightArray  []map[int]float64
+}
+
+// 批量设置行高
+func (b *LatestExamExcel) setRowsHeight() {
+	for _, rowHeight := range b.RowsHeightArray {
+		for row, height := range rowHeight {
+			b.Excel.SetRowHeight(b.SheetName, row, height)
+		}
+	}
+
+}
+
+// 获取范围内单元格的宽度 A:F
+func (b *LatestExamExcel) getRangeWidth(r string) float64 {
+	rg := strings.Split(r, ":")
+
+	if len(rg) == 1 {
+		start := b.RowMap[rg[0]]
+		return b.RowWidthArray[start]
+	} else if len(rg) == 2 {
+		start := b.RowMap[rg[0]]
+		end := b.RowMap[rg[1]]
+		rowr := b.RowWidthArray[start : end+1]
+		width := 0.0
+		for _, v := range rowr {
+			width += v
+		}
+		return width
+	}
+	return 0.0
+}
+
+func (b *LatestExamExcel) drawTitle() error {
+	b.Row++
+	startCell := fmt.Sprintf("A%d", b.Row)
+	endCell := fmt.Sprintf("G%d", b.Row)
+
+	b.RowMap = map[string]int{"A": 0, "B": 1, "C": 2, "D": 3, "E": 4, "F": 5, "G": 6}
+
+	b.RowWidthArray = []float64{16, 12, 12, 10, 10, 12, 16}
+	b.Excel.SetColWidth(b.SheetName, "A", "A", 16)
+	b.Excel.SetColWidth(b.SheetName, "B", "B", 12)
+	b.Excel.SetColWidth(b.SheetName, "C", "C", 12)
+	b.Excel.SetColWidth(b.SheetName, "D", "D", 10)
+	b.Excel.SetColWidth(b.SheetName, "E", "E", 10)
+	b.Excel.SetColWidth(b.SheetName, "F", "F", 12)
+	b.Excel.SetColWidth(b.SheetName, "G", "G", 16)
+
+	err := b.Excel.MergeCell(b.SheetName, startCell, endCell)
+	if err != nil {
+		return err
+	}
+
+	style, err := b.Excel.NewStyle(&excelize.Style{
+		Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
+		Font:      &excelize.Font{Bold: true, Size: 18}})
+	if err != nil {
+		return err
+	}
+	err = b.Excel.SetCellStyle(b.SheetName, startCell, startCell, style)
+	if err != nil {
+		return err
+	}
+	b.Excel.SetRowHeight(b.SheetName, b.Row, 26)
+
+	b.Excel.SetCellValue(b.SheetName, startCell, b.Title)
+	return nil
+}
+
+func (b *LatestExamExcel) drawTableTitle() error {
+	b.Row++
+	var drawCol = func(prefix string, value string) error {
+		cell := fmt.Sprintf("%s%d", prefix, b.Row)
+		err := b.Excel.SetCellStyle(b.SheetName, cell, cell, b.AlignCenterStyle)
+		if err != nil {
+			return err
+		}
+
+		return b.Excel.SetCellValue(b.SheetName, cell, value)
+	}
+
+	drawCol("A", "考试日期")
+	drawCol("B", "用户名")
+	drawCol("C", "编码")
+	drawCol("D", "类型")
+	drawCol("E", "分数")
+	drawCol("F", "学习进度(%)")
+	drawCol("G", "学习总时长(分钟)")
+	b.Excel.SetRowHeight(b.SheetName, b.Row, 22)
+
+	return nil
+}
+
+func (b *LatestExamExcel) drawTableContent() error {
+	b.Row++
+	var DrawRow = func(rowIndex int, values ...string) float64 {
+		charas := []string{"A", "B", "C", "D", "E", "F", "G"}
+		// 获取该行最大行高
+		max := getRowHeight(values[0], b.getRangeWidth(charas[0]))
+		for i, c := range charas {
+			v := ""
+			if i < len(values) {
+				v = values[i]
+			}
+			b.Excel.SetCellValue(b.SheetName, fmt.Sprintf("%s%d", c, rowIndex), v)
+			val2Cel := fmt.Sprintf("%s%d", c, rowIndex)
+			b.Excel.SetCellStyle(b.SheetName, val2Cel, val2Cel, b.AlignCenterStyle)
+
+			if getRowHeight(v, b.getRangeWidth(c)) > max {
+				max = getRowHeight(v, b.getRangeWidth(c))
+			}
+		}
+		return max
+	}
+
+	latestExamLogs := b.Content
+	if len(latestExamLogs) > 0 {
+		for _, examLog := range latestExamLogs {
+			score := strconv.Itoa(examLog.Score)
+			completeRate := fmt.Sprintf("%d%%", examLog.CompleteRate)
+			learnTime := strconv.Itoa(examLog.LearnTime)
+			rowMaxHeight := DrawRow(b.Row, examLog.ExamDate, examLog.UserName, examLog.Nid, examLog.Type, score,
+				completeRate, learnTime)
+			b.RowsHeightArray = append(b.RowsHeightArray, map[int]float64{b.Row: rowMaxHeight})
+			b.Row++
+		}
+	}
+
+	return nil
+}
+
+func (b *LatestExamExcel) Draws() {
+	b.drawTitle()
+
+	b.drawTableTitle()
+	b.drawTableContent()
+	// 设置行高
+	b.setRowsHeight()
+}
+
+func NewLatestExamExcel(f *excelize.File) *LatestExamExcel {
+
+	border := []excelize.Border{
+		{Type: "top", Style: 1, Color: "000000"},
+		{Type: "left", Style: 1, Color: "000000"},
+		{Type: "right", Style: 1, Color: "000000"},
+		{Type: "bottom", Style: 1, Color: "000000"},
+	}
+
+	styleLeft, _ := f.NewStyle(&excelize.Style{
+		Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
+		Border:    border,
+	})
+
+	b := &LatestExamExcel{
+		Title:            "用户信息",
+		SheetName:        "Sheet1",
+		Excel:            f,
+		Offset:           0,
+		AlignCenterStyle: styleLeft,
+		RowMap:           map[string]int{"A": 0, "B": 1, "C": 2, "D": 3, "E": 4, "F": 5, "G": 6},
+		RowWidthArray:    []float64{16, 12, 12, 10, 10, 12, 16},
+		RowsHeightArray:  make([]map[int]float64, 0),
+	}
+
+	// f.SetPageMargins(b.SheetName, excelize.PageMarginTop(1), excelize.PageMarginLeft(0), excelize.PageMarginRight(0))
+
+	return b
+}
+
+func (b *LatestExamExcel) FormatToEmpty(str *string) {
+	if *str == "0" || *str == "0.000" {
+		*str = "-"
+	}
+
+}

+ 80 - 0
src/api/learnLog.go

@@ -5,6 +5,7 @@ import (
 	"copter-train/db/repo"
 	"copter-train/log"
 	"errors"
+	"fmt"
 	"time"
 
 	"github.com/gin-gonic/gin"
@@ -47,6 +48,85 @@ func SyncLearnTime(c *gin.Context, apictx *ApiSession) (interface{}, error) {
 	if objId.IsZero() {
 		return nil, errors.New("id错误")
 	}
+
+	// 写入统计数据,每个人每个模块的总学习时长
+	learnLog := &model.LearnLog{}
+	found, err := repo.RepoSeachDoc(apictx.CreateRepoCtx(), &repo.DocSearchOptions{
+		Db:          db,
+		CollectName: repo.CollectionLearnLog,
+		Query:       repo.Map{"_id": objId},
+		Project:     []string{"cid", "uid"},
+	}, learnLog)
+	if err != nil {
+		log.Error(err)
+		return nil, err
+	}
+	if !found {
+		return nil, errors.New("未找到记录")
+	}
+
 	update := bson.M{"$inc": bson.M{"learnTime": 1}, "$set": bson.M{"updateTime": time.Now()}}
+
+	_, err = repo.RepoUpdateSetDbDocs(apictx.CreateRepoCtx(), db, repo.CollectionLearnLogStatistics, bson.M{"cid": learnLog.Cid, "uid": learnLog.Uid}, update)
+	if err != nil {
+		log.Error(err)
+		fmt.Println(err)
+	}
 	return repo.RepoUpdateSetDbDocProps(apictx.CreateRepoCtx(), db, repo.CollectionLearnLog, id, update)
 }
+
+func LearnLogStatistics(c *gin.Context, apictx *ApiSession) (interface{}, error) {
+	db := c.Param("scope")
+	if len(db) == 0 {
+		return nil, errors.New("scope不能为空")
+	}
+	// 学习模块进度
+	uid := c.Query("uid")
+	if len(uid) == 0 {
+		uid = apictx.User.ID
+	}
+	return getLearnLogStatistics(apictx, db, uid)
+}
+
+func getLearnLogStatistics(apictx *ApiSession, db string, uid string) (float64, error) {
+	learnLogStatistics := make([]*model.LearnLogStatistics, 0)
+	err := repo.RepoSeachDocs(apictx.CreateRepoCtx(), &repo.DocSearchOptions{
+		Db:          db,
+		CollectName: repo.CollectionLearnLogStatistics,
+		Query:       repo.Map{"uid": uid},
+		Project:     []string{"cid", "learnTime"},
+	}, &learnLogStatistics)
+	if err != nil {
+		log.Error(err)
+		return 0.00, err
+	}
+	completeRate := 0.00
+	total := len(LearnLogSettings)
+	if len(learnLogStatistics) > 0 {
+		for _, v := range learnLogStatistics {
+			limit, ok := LearnLogSettings[v.Cid]
+			if !ok {
+				limit = 10
+			}
+			rate := float64(v.LearnTime) / float64(limit)
+			if rate > 1 {
+				rate = 1
+			}
+			completeRate += rate / float64(total)
+		}
+	}
+	return completeRate, nil
+
+}
+
+var LearnLogSettings = map[string]int{
+	"65a8edf6a7ef2b18346edd8c": 10,
+	"65a8ee02a7ef2b18346edd8d": 10,
+	"65a8ee0da7ef2b18346edd8e": 10,
+	"65a8ee18a7ef2b18346edd8f": 10,
+	"65a8ee21a7ef2b18346edd90": 10,
+	"65a8ee2ba7ef2b18346edd91": 60,
+	"65a8ee39a7ef2b18346edd92": 10,
+	"65a8ee39a7ef2b18346edd93": 10,
+	"65a8ee39a7ef2b18346edd94": 60,
+}

+ 3 - 0
src/api/router.go

@@ -37,6 +37,7 @@ func RegRouters(svc *Service) {
 	// 学习记录
 	root.POSTJWT("/learnLog/create/:scope", CreateLearnLog)
 	root.POSTJWT("/learnLog/sync/time/:id/:scope", SyncLearnTime)
+	root.GETJWT("/learnLog/statistics/:scope", LearnLogStatistics)
 
 	// 考核试题管理
 	root.POSTJWT("/admin/test/create/:scope", CreateTest)
@@ -57,6 +58,8 @@ func RegRouters(svc *Service) {
 	// 考核记录列表
 	root.GETJWT("/exeamLog/list/:scope", ExeamLogList)
 
+	root.GETJWT("/exeamLog/latest/export/:scope", LatestExamExportExcel)
+
 	// 用户id,可选:没有这个参数时是自己的信息,否则是指定id的信息
 	// 本年每月学习时长 *type/year/uid
 	root.GETJWT("/statistics/learn/process/:scope", StatisticsLearnProcess)

+ 156 - 0
src/api/tmp

@@ -0,0 +1,156 @@
+func DownLoadCompBills(c *gin.Context, apictx *ApiSession) (interface{}, error) {
+
+	f := excelize.NewFile()
+	f.SetDefaultFont("宋体")
+	flagIndex := -1
+	// 采购 加工 加工-印刷 加工-覆膜 成品采购
+	typeRows := []int{0, 0, 0, 0, 0}
+	for _, tId := range typeBillIds {
+		tidArr := strings.Split(tId, "_")
+		var billExcel IExcel
+		// 采购
+		billId, _ := primitive.ObjectIDFromHex(tidArr[1])
+		if tidArr[0] == "1" {
+			purchase := model.PurchaseBill{}
+			found, _ := repo.RepoSeachDoc(apictx.CreateRepoCtx(), &repo.DocSearchOptions{
+				CollectName: repo.CollectionBillPurchase,
+				Query:       repo.Map{"_id": billId},
+			}, &purchase)
+			if !found {
+				continue
+			}
+			sheetName := "采购单"
+			index := f.NewSheet(sheetName)
+			if flagIndex < 0 {
+				flagIndex = index
+			}
+			// index := f.NewSheet("采购单")
+			// f.SetActiveSheet(index)
+			billExcel = NewPurchaseBill(f)
+			billExcel.SetSheetName(sheetName)
+			if purchase.Reviewed == 1 {
+				if len(purchase.SignUsers) > 0 {
+					signs := []*model.Signature{}
+					repo.RepoDocsSearch(apictx.CreateRepoCtx(), &repo.PageSearchOptions{
+						CollectName: repo.CollectionSignature,
+						Query:       repo.Map{"_id": bson.M{"$in": purchase.SignUsers}},
+						Sort:        bson.M{"sort": 1},
+					}, &signs)
+					billExcel.SetSignatures(signs)
+				}
+
+			}
+			billExcel.SetContent(&purchase)
+			billExcel.SetTitle(fmt.Sprintf("%s原材料采购单", companyName))
+
+			billExcel.SetRow(typeRows[0])
+			billExcel.Draws()
+			typeRows[0] = billExcel.GetRow() + 5
+
+		}
+		// 工艺
+		if tidArr[0] == "2" {
+			produce := model.ProduceBill{}
+			found, _ := repo.RepoSeachDoc(apictx.CreateRepoCtx(), &repo.DocSearchOptions{
+				CollectName: repo.CollectionBillProduce,
+				Query:       repo.Map{"_id": billId},
+			}, &produce)
+			if !found {
+				continue
+			}
+			sheetName := "加工单"
+			if produce.IsPrint {
+				sheetName = "加工单-印刷"
+			} else if produce.IsLam {
+				sheetName = "加工单-覆膜"
+			}
+			index := f.NewSheet(sheetName)
+			if flagIndex < 0 {
+				flagIndex = index
+			}
+			billExcel = NewProduceBill(f)
+			billExcel.SetSheetName(sheetName)
+			if produce.Reviewed == 1 {
+				if len(produce.SignUsers) > 0 {
+					signs := []*model.Signature{}
+					repo.RepoDocsSearch(apictx.CreateRepoCtx(), &repo.PageSearchOptions{
+						CollectName: repo.CollectionSignature,
+						Query:       repo.Map{"_id": bson.M{"$in": produce.SignUsers}},
+						Sort:        bson.M{"sort": 1},
+					}, &signs)
+					billExcel.SetSignatures(signs)
+				}
+
+			}
+			billExcel.SetContent(&produce)
+			billExcel.SetTitle(fmt.Sprintf("%s加工单", companyName))
+
+			if produce.IsPrint {
+				billExcel.SetRow(typeRows[2])
+				billExcel.Draws()
+				typeRows[2] = billExcel.GetRow() + 5
+			} else if produce.IsLam {
+				billExcel.SetRow(typeRows[3])
+				billExcel.Draws()
+				typeRows[3] = billExcel.GetRow() + 5
+			} else {
+				billExcel.SetRow(typeRows[1])
+				billExcel.Draws()
+				typeRows[1] = billExcel.GetRow() + 5
+
+			}
+
+		}
+		// 成品采购
+		if tidArr[0] == "3" {
+			product := model.ProductBill{}
+			found, _ := repo.RepoSeachDoc(apictx.CreateRepoCtx(), &repo.DocSearchOptions{
+				CollectName: repo.CollectionBillProduct,
+				Query:       repo.Map{"_id": billId},
+			}, &product)
+			if !found {
+				continue
+			}
+			sheetName := "成品采购单"
+			index := f.NewSheet(sheetName)
+			if flagIndex < 0 {
+				flagIndex = index
+			}
+			billExcel = NewProductBill(f)
+			billExcel.SetSheetName(sheetName)
+			if product.Reviewed == 1 {
+				if len(product.SignUsers) > 0 {
+					signs := []*model.Signature{}
+					repo.RepoDocsSearch(apictx.CreateRepoCtx(), &repo.PageSearchOptions{
+						CollectName: repo.CollectionSignature,
+						Query:       repo.Map{"_id": bson.M{"$in": product.SignUsers}},
+						Sort:        bson.M{"sort": 1},
+					}, &signs)
+					billExcel.SetSignatures(signs)
+				}
+
+			}
+			billExcel.SetContent(&product)
+			billExcel.SetTitle(companyName)
+
+			billExcel.SetRow(typeRows[4])
+			billExcel.Draws()
+			typeRows[4] = billExcel.GetRow() + 5
+		}
+
+	}
+
+	// 设置活跃sheet
+	f.SetActiveSheet(flagIndex)
+	// 删除默认Sheet1
+	f.DeleteSheet("Sheet1")
+
+	c.Header("Content-Type", "application/octet-stream")
+	c.Header("Content-Disposition", "attachment; filename="+fmt.Sprintf("%s.xlsx", compNameId))
+	c.Header("Content-Transfer-Encoding", "binary")
+
+	f.Write(c.Writer)
+
+	return nil, nil
+
+}

+ 1 - 1
src/app.yaml

@@ -1,4 +1,4 @@
-port: 8101
+port: 8102
 name: copter-train
 version: 1.0.0
 

+ 1 - 0
src/copter-train.log

@@ -15,3 +15,4 @@
 {"level":"error","timestamp":"2024-02-22 17:29:10","message":"[write exception: write errors: [E11000 duplicate key error collection: copter.users index: loginName_1 dup key: { loginName: \"赵四_20140003\" }]]","service_name":"copter-train"}
 {"level":"error","timestamp":"2024-02-27 14:56:04","message":"[write exception: write errors: [E11000 duplicate key error collection: copter.users index: nid_1 dup key: { nid: \"20140003\" }]]","service_name":"copter-train"}
 {"level":"error","timestamp":"2024-02-27 14:59:47","message":"[write exception: write errors: [E11000 duplicate key error collection: copter.users index: loginName_1 dup key: { loginName: \"赵四2_20140006\" }]]","service_name":"copter-train"}
+{"level":"error","timestamp":"2024-03-07 15:55:09","message":"[write exception: write errors: [The dollar ($) prefixed field '$inc' in '$inc' is not valid for storage.]]","service_name":"copter-train"}

+ 5 - 0
src/db/model/exeamLog.go

@@ -27,3 +27,8 @@ type TestRecord struct {
 	Test
 	ExeamAnswer string `bson:"exeamAnswer,omitempty" json:"exeamAnswer"` // 答案
 }
+
+type LatestExamLog struct {
+	Id       primitive.ObjectID `bson:"_id,omitempty" json:"_id"`
+	ExeamLog *ExeamLog          `bson:"latestExeamLog,omitempty" json:"latestExeamLog"`
+}

+ 11 - 0
src/db/model/learnLog.go

@@ -19,3 +19,14 @@ type LearnLog struct {
 	CreateTime time.Time `bson:"createTime,omitempty" json:"createTime"`
 	UpdateTime time.Time `bson:"updateTime,omitempty" json:"updateTime"`
 }
+
+// 学习记录统计
+type LearnLogStatistics struct {
+	Id  primitive.ObjectID `bson:"_id,omitempty" json:"_id"`
+	Uid string             `bson:"uid,omitempty" json:"uid"` // 用户id
+	Cid string             `bson:"cid,omitempty" json:"cid"` // 分类id
+	// CateName   string             `bson:"cateName,omitempty" json:"cateName"`   // 分类名称
+	LearnTime  int       `bson:"learnTime,omitempty" json:"learnTime"` // 学习时长
+	CreateTime time.Time `bson:"createTime,omitempty" json:"createTime"`
+	UpdateTime time.Time `bson:"updateTime,omitempty" json:"updateTime"`
+}

+ 15 - 5
src/db/repo/repo.go

@@ -18,11 +18,12 @@ type RepoSession struct {
 }
 
 const (
-	CollectionCategory = "category"
-	CollectionUser     = "users"
-	CollectionLearnLog = "learn_logs"
-	CollectionTest     = "tests"
-	CollectionExeamLog = "exeam_logs"
+	CollectionCategory           = "category"
+	CollectionUser               = "users"
+	CollectionLearnLog           = "learn_logs"
+	CollectionTest               = "tests"
+	CollectionExeamLog           = "exeam_logs"
+	CollectionLearnLogStatistics = "learn_log_statistics"
 )
 
 type Map map[string]interface{}
@@ -109,6 +110,15 @@ func RepoUpdateSetDocs(ctx *RepoSession, collectName string, filter interface{},
 
 	return colls.UpdateOne(ctx.Ctx, filter, update)
 }
+func RepoUpdateSetDbDocs(ctx *RepoSession, Db string, collectName string, filter interface{}, update interface{}) (*mongo.UpdateResult, error) {
+
+	colls := ctx.Client.GetDbCollection(Db, collectName)
+
+	// update := bson.M{"$set": model}
+	opts := options.Update().SetUpsert(true)
+
+	return colls.UpdateOne(ctx.Ctx, filter, update, opts)
+}
 
 func RepoUpdateSetDoc(ctx *RepoSession, collectName string, idstr string, model interface{}) (*mongo.UpdateResult, error) {
 

+ 0 - 31
src/readme.md

@@ -1,31 +0,0 @@
-
-# copter-train项目
-
-## 环境windows7 无外网访问
-
-1. 不能使用docker
-2. bus/usercenter/copter-train本地运行
-3. mongodb安装
-
-## 开发内容
-
-1. 分类模块管理
-2. 用户管理
-3. 学习:(客户端预设,记录学习日志)
-4. 考核:(记录考核成绩)
-5. 题库管理:选择、判断、实操(客户端预设)
-
-
-```sh
-
-use admin;
-db.createUser({user:"admin",pwd:"admin123",roles:[{"role":"userAdminAnyDatabase","db":"admin"}]});
-db.auth("admin","admin123");
-db.createUser({user:"root",pwd:"copter-train-8888",roles:["dbOwner"]});
-
-```
-创建默认管理员账号
-MONGO_MIGRATION: mongo-migrations==mongodb://root:copter-train-8888@127.0.0.1:27017/copter?authSource=admin
-
-
-go build -ldflags -H=windowsgui .

+ 0 - 26
src/start.bat

@@ -1,26 +0,0 @@
-@echo off
-chcp 65001
-
-taskkill /IM copter-train.exe /F
-taskkill /IM config-server.exe /F
-
-echo 启动bus服务中...
-cd "G:\wk\copter-projects\copter-train\configer"
-start cmd /c "config-server.exe"
-
-timeout /t 3
-echo 启动主服务中...
-cd "G:\wk\copter-projects\copter-train\src"
-start cmd /c "copter-train.exe"
-
-timeout /t 1
-
-echo 启动完成
-echo "管理后台: http://ip:8101/web"
-
-timeout /t 5
-
-@REM 开机自动启动
-@REM 创建一个快捷方式。右键点击你的批处理文件,然后选择 "创建快捷方式"。
-@REM 打开启动文件夹。按下 Win + R 键来打开 "运行" 对话框,然后输入 shell:startup 并按下 Enter 键。
-@REM 将快捷方式复制到启动文件夹。将你在第一步中创建的快捷方式复制并粘贴到启动文件夹中。