package api import ( "fmt" "sort" "strings" "github.com/gin-gonic/gin" "eaisalestrain/backend/internal/model" "eaisalestrain/backend/internal/store" "eaisalestrain/backend/internal/web" ) // ListDepartments GET /api/departments?status= —— 部门字典列表(含成员数) func ListDepartments(c *gin.Context) { q := store.DB.Model(&model.Department{}) switch st := c.Query("status"); st { case "": // 默认仅 active q = q.Where("status = ?", "active") case "all": // 管理员维护全量 default: q = q.Where("status = ?", st) } var items []model.Department if err := q.Order("id ASC").Find(&items).Error; err != nil { web.Fail(c, web.NewBadRequest("查询部门失败")) return } // 成员数按 user.department 字符串匹配(部门为字典、用户以字符串归属) var users []model.User store.DB.Where("role = ? AND status = ?", "employee", "active").Find(&users) nameCount := map[string]int{} for _, u := range users { if strings.TrimSpace(u.Department) != "" { nameCount[u.Department]++ } } out := make([]gin.H, 0, len(items)) for _, d := range items { out = append(out, gin.H{ "id": d.ID, "name": d.Name, "description": d.Description, "status": d.Status, "member_count": nameCount[d.Name], "created_at": d.CreatedAt, }) } web.OK(c, out) } // CreateDepartment POST /api/departments (admin) func CreateDepartment(c *gin.Context) { var req struct { Name string `json:"name"` Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Name) == "" { web.Fail(c, web.NewBadRequest("部门名称为必填")) return } req.Name = strings.TrimSpace(req.Name) var n int64 store.DB.Model(&model.Department{}).Where("name = ?", req.Name).Count(&n) if n > 0 { web.Fail(c, web.NewConflictError("部门名称已存在")) return } d := model.Department{Name: req.Name, Description: req.Description, Status: "active"} if err := store.DB.Create(&d).Error; err != nil { web.Fail(c, web.NewBadRequest("创建部门失败")) return } web.OK(c, d) } // UpdateDepartment PUT /api/departments/{id} (admin) —— 改名时同步 user.department 字符串 func UpdateDepartment(c *gin.Context) { id, ok := parseID(c, "id") if !ok { return } var d model.Department if err := store.DB.First(&d, id).Error; err != nil { web.Fail(c, web.NewNotFoundError("部门不存在")) return } var req struct { Name string `json:"name"` Description string `json:"description"` Status string `json:"status"` } if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Name) == "" { web.Fail(c, web.NewBadRequest("部门名称为必填")) return } req.Name = strings.TrimSpace(req.Name) oldName := d.Name if req.Name != oldName { var n int64 store.DB.Model(&model.Department{}).Where("name = ? AND id <> ?", req.Name, id).Count(&n) if n > 0 { web.Fail(c, web.NewConflictError("部门名称已存在")) return } } d.Name = req.Name d.Description = req.Description if req.Status == "active" || req.Status == "inactive" { d.Status = req.Status } if err := store.DB.Save(&d).Error; err != nil { web.Fail(c, web.NewBadRequest("更新部门失败")) return } // 改名后同步员工归属,保证按部门统计与展示一致 if req.Name != oldName { store.DB.Model(&model.User{}).Where("department = ?", oldName).Update("department", req.Name) } web.OK(c, d) } // DeleteDepartment DELETE /api/departments/{id} (admin) —— 有员工归属时拒绝删除 func DeleteDepartment(c *gin.Context) { id, ok := parseID(c, "id") if !ok { return } var d model.Department if err := store.DB.First(&d, id).Error; err != nil { web.Fail(c, web.NewNotFoundError("部门不存在")) return } var n int64 store.DB.Model(&model.User{}).Where("department = ? AND status = ?", d.Name, "active").Count(&n) if n > 0 { web.Fail(c, web.NewConflictError(fmt.Sprintf("该部门下仍有 %d 名员工,请先调整其部门", n))) return } if err := store.DB.Delete(&d).Error; err != nil { web.Fail(c, web.NewBadRequest("删除部门失败")) return } web.OK(c, gin.H{"id": id, "deleted": true}) } // DepartmentStats GET /api/system/department-stats —— 按部门学情聚合 func DepartmentStats(c *gin.Context) { var employees []model.User store.DB.Where("role = ? AND status = ?", "employee", "active").Find(&employees) // 每用户聚合:积分 / 考试 / 学习进度 type userAgg struct { Points int FormalCount int FormalPassed int ScoreSum int CompanyViewed bool ProductCount int CourseCount int } perUser := map[uint]*userAgg{} get := func(id uint) *userAgg { a := perUser[id] if a == nil { a = &userAgg{} perUser[id] = a } return a } for _, e := range employees { get(e.ID).Points = e.LearningPoints } var recs []model.ExamRecord store.DB.Find(&recs) for _, r := range recs { a := get(r.UserID) a.FormalCount++ if r.Passed { a.FormalPassed++ } a.ScoreSum += r.Score } var lps []model.LearningProgress store.DB.Find(&lps) for _, lp := range lps { a := get(lp.UserID) switch lp.ItemType { case "company": a.CompanyViewed = true case "product": a.ProductCount++ case "course": a.CourseCount++ } } // 按部门字符串聚合 type deptAgg struct { EmployeeCount int PointsTotal int FormalCount int FormalPassed int ScoreSum int CompanyViewed int ProductSum int CourseSum int } perDept := map[string]*deptAgg{} order := []string{} for _, e := range employees { name := strings.TrimSpace(e.Department) if name == "" { name = "未分组" } a := perDept[name] if a == nil { a = &deptAgg{} perDept[name] = a order = append(order, name) } a.EmployeeCount++ if u := perUser[e.ID]; u != nil { a.PointsTotal += u.Points a.FormalCount += u.FormalCount a.FormalPassed += u.FormalPassed a.ScoreSum += u.ScoreSum if u.CompanyViewed { a.CompanyViewed++ } a.ProductSum += u.ProductCount a.CourseSum += u.CourseCount } } // 排序:未分组沉底,其余按名称 sort.Slice(order, func(i, j int) bool { if order[i] == "未分组" { return false } if order[j] == "未分组" { return true } return order[i] < order[j] }) out := make([]gin.H, 0, len(order)) for _, name := range order { a := perDept[name] passRate, avgScore, companyRate, productAvg, courseAvg := 0.0, 0.0, 0.0, 0.0, 0.0 if a.FormalCount > 0 { passRate = round1(float64(a.FormalPassed) * 100 / float64(a.FormalCount)) avgScore = round1(float64(a.ScoreSum) / float64(a.FormalCount)) } if a.EmployeeCount > 0 { companyRate = round1(float64(a.CompanyViewed) * 100 / float64(a.EmployeeCount)) productAvg = round1(float64(a.ProductSum) / float64(a.EmployeeCount)) courseAvg = round1(float64(a.CourseSum) / float64(a.EmployeeCount)) } out = append(out, gin.H{ "department": name, "employee_count": a.EmployeeCount, "points_total": a.PointsTotal, "formal_count": a.FormalCount, "formal_passed": a.FormalPassed, "pass_rate": passRate, "avg_score": avgScore, "company_rate": companyRate, "product_avg": productAvg, "course_avg": courseAvg, }) } web.OK(c, gin.H{"items": out}) }