init: 数字员工平台初始代码
包含前端(Vue3 + VueFlow 画布)、后端(Go)、文档体系。 - 工作台画布:节点拖放、连线模式、右键菜单、AI 助手 - 后端:连接器 API、专员种子数据 - 导航:左侧导航、工坊、市场、控制台
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
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})
|
||||
}
|
||||
Reference in New Issue
Block a user