Files
pj0235-eai_agentplatform/eai_ap_app/backend-go/internal/api/position.go
T
eaiadmin 4e8817d768 init: 数字员工平台初始代码
包含前端(Vue3 + VueFlow 画布)、后端(Go)、文档体系。
- 工作台画布:节点拖放、连线模式、右键菜单、AI 助手
- 后端:连接器 API、专员种子数据
- 导航:左侧导航、工坊、市场、控制台
2026-08-18 20:19:58 +08:00

398 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"fmt"
"github.com/gin-gonic/gin"
"eaisalestrain/backend/internal/middleware"
"eaisalestrain/backend/internal/model"
"eaisalestrain/backend/internal/store"
"eaisalestrain/backend/internal/web"
)
// ============ 岗位 CRUD(管理员) ============
// ListPositions GET /api/positions?status= —— 岗位列表
func ListPositions(c *gin.Context) {
q := store.DB.Model(&model.Position{})
switch st := c.Query("status"); st {
case "": // 默认仅 active
q = q.Where("status = ?", "active")
case "all": // 管理员维护全量
default:
q = q.Where("status = ?", st)
}
var items []model.Position
if err := q.Order("id ASC").Find(&items).Error; err != nil {
web.Fail(c, web.NewBadRequest("查询岗位失败"))
return
}
web.OK(c, items)
}
// CreatePosition POST /api/positions (admin)
func CreatePosition(c *gin.Context) {
var p model.Position
if err := c.ShouldBindJSON(&p); err != nil {
web.Fail(c, web.NewBadRequest("请求参数错误"))
return
}
if p.Code == "" || p.Name == "" {
web.Fail(c, web.NewBadRequest("编号、名称为必填"))
return
}
var count int64
store.DB.Model(&model.Position{}).Where("code = ?", p.Code).Count(&count)
if count > 0 {
web.Fail(c, web.NewConflictError("岗位编号已存在"))
return
}
if p.Status == "" {
p.Status = "active"
}
if err := store.DB.Create(&p).Error; err != nil {
web.Fail(c, web.NewBadRequest("创建岗位失败"))
return
}
web.OK(c, p)
}
// UpdatePosition PUT /api/positions/{id} (admin)
func UpdatePosition(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
var p model.Position
if err := store.DB.First(&p, id).Error; err != nil {
web.Fail(c, web.NewNotFoundError("岗位不存在"))
return
}
var req model.Position
if err := c.ShouldBindJSON(&req); err != nil {
web.Fail(c, web.NewBadRequest("请求参数错误"))
return
}
if req.Name == "" {
web.Fail(c, web.NewBadRequest("名称为必填"))
return
}
if req.Status == "" {
req.Status = "active"
}
if req.Code != "" && req.Code != p.Code {
var count int64
store.DB.Model(&model.Position{}).Where("code = ? AND id <> ?", req.Code, id).Count(&count)
if count > 0 {
web.Fail(c, web.NewConflictError("岗位编号已存在"))
return
}
p.Code = req.Code
}
p.Name = req.Name
p.Description = req.Description
p.Status = req.Status
if err := store.DB.Save(&p).Error; err != nil {
web.Fail(c, web.NewBadRequest("更新岗位失败"))
return
}
web.OK(c, p)
}
// DeletePosition DELETE /api/positions/{id} (admin) —— 软删除(status=inactive)
func DeletePosition(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
var p model.Position
if err := store.DB.First(&p, id).Error; err != nil {
web.Fail(c, web.NewNotFoundError("岗位不存在"))
return
}
if err := store.DB.Model(&p).Update("status", "inactive").Error; err != nil {
web.Fail(c, web.NewBadRequest("停用岗位失败"))
return
}
web.OK(c, gin.H{"id": id, "status": "inactive"})
}
// ============ 岗位知识映射(管理员) ============
// positionKnowledgeReq 映射条目入参(与 model.PositionKnowledge 同构)
type positionKnowledgeReq struct {
Domain string `json:"domain"`
CourseID *uint `json:"course_id"`
ProductID *uint `json:"product_id"`
RequiredLevel string `json:"required_level"`
Weight float64 `json:"weight"`
IsMandatory bool `json:"is_mandatory"`
}
// ListPositionKnowledge GET /api/positions/{id}/knowledge —— 某岗位知识映射列表
func ListPositionKnowledge(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
var items []model.PositionKnowledge
if err := store.DB.Where("position_id = ?", id).Order("id ASC").Find(&items).Error; err != nil {
web.Fail(c, web.NewBadRequest("查询岗位知识映射失败"))
return
}
web.OK(c, items)
}
// SavePositionKnowledge PUT /api/positions/{id}/knowledge —— 保存映射(整表覆盖)
func SavePositionKnowledge(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
var pos model.Position
if err := store.DB.First(&pos, id).Error; err != nil {
web.Fail(c, web.NewNotFoundError("岗位不存在"))
return
}
var req struct {
Items []positionKnowledgeReq `json:"items"`
}
if err := c.ShouldBindJSON(&req); err != nil {
web.Fail(c, web.NewBadRequest("请求参数错误"))
return
}
// 校验并规整映射条目
rows := make([]model.PositionKnowledge, 0, len(req.Items))
for _, it := range req.Items {
if !validDomains[it.Domain] {
web.Fail(c, web.NewBadRequest("知识域 domain 非法(仅 company/product/sales)"))
return
}
if it.RequiredLevel == "" {
it.RequiredLevel = model.LevelL1
}
if !model.ValidLevels[it.RequiredLevel] {
web.Fail(c, web.NewBadRequest("级别 required_level 非法(仅 L1/L2/L3/L4)"))
return
}
if it.Weight <= 0 {
it.Weight = 1
}
rows = append(rows, model.PositionKnowledge{
PositionID: id,
Domain: it.Domain,
CourseID: it.CourseID,
ProductID: it.ProductID,
RequiredLevel: it.RequiredLevel,
Weight: it.Weight,
IsMandatory: it.IsMandatory,
})
}
// 整表覆盖:先删旧,再批量插入
if err := store.DB.Where("position_id = ?", id).Delete(&model.PositionKnowledge{}).Error; err != nil {
web.Fail(c, web.NewBadRequest("清除旧映射失败"))
return
}
if len(rows) > 0 {
if err := store.DB.Create(&rows).Error; err != nil {
web.Fail(c, web.NewBadRequest("保存岗位知识映射失败"))
return
}
}
var out []model.PositionKnowledge
store.DB.Where("position_id = ?", id).Order("id ASC").Find(&out)
web.OK(c, out)
}
// ============ 用户设岗(管理员) ============
// SetUserPosition PUT /api/users/{id}/position —— {position_id: 5 | null}
func SetUserPosition(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
var u model.User
if err := store.DB.First(&u, id).Error; err != nil {
web.Fail(c, web.NewNotFoundError("用户不存在"))
return
}
var req struct {
PositionID *uint `json:"position_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
web.Fail(c, web.NewBadRequest("请求参数错误"))
return
}
posName := ""
if req.PositionID != nil {
var pos model.Position
if err := store.DB.First(&pos, *req.PositionID).Error; err != nil || pos.Status != "active" {
web.Fail(c, web.NewBadRequest("岗位不存在或已停用"))
return
}
posName = pos.Name
}
u.PositionID = req.PositionID
if err := store.DB.Save(&u).Error; err != nil {
web.Fail(c, web.NewBadRequest("设置用户岗位失败"))
return
}
if req.PositionID != nil {
notifyUser(u.ID, "position_set", "岗位已设置",
fmt.Sprintf("你的岗位已设置为「%s」,可在「我的岗位清单」查看应学内容", posName),
"/exam/my-position")
}
web.OK(c, gin.H{"id": u.ID, "position_id": u.PositionID})
}
// ============ 学员:我的岗位应学清单 ============
// MyPosition GET /api/my/position —— 我的岗位 + 应学范围(课程/产品/域)
func MyPosition(c *gin.Context) {
u := middleware.CurrentUser(c)
if u == nil || u.PositionID == nil {
web.OK(c, gin.H{"position": nil, "knowledge": []gin.H{}, "count": 0})
return
}
var pos model.Position
if err := store.DB.First(&pos, *u.PositionID).Error; err != nil || pos.Status != "active" {
web.OK(c, gin.H{"position": nil, "knowledge": []gin.H{}, "count": 0})
return
}
var pks []model.PositionKnowledge
store.DB.Where("position_id = ?", pos.ID).Order("id ASC").Find(&pks)
// 批量解析课程/产品名称
courseIDs := make([]uint, 0, len(pks))
productIDs := make([]uint, 0, len(pks))
for _, pk := range pks {
if pk.CourseID != nil {
courseIDs = append(courseIDs, *pk.CourseID)
}
if pk.ProductID != nil {
productIDs = append(productIDs, *pk.ProductID)
}
}
courseName := map[uint]string{}
if len(courseIDs) > 0 {
var courses []model.Course
store.DB.Where("id IN ?", courseIDs).Find(&courses)
for _, c := range courses {
courseName[c.ID] = c.Name
}
}
productName := map[uint]string{}
if len(productIDs) > 0 {
var products []model.Product
store.DB.Where("id IN ?", productIDs).Find(&products)
for _, p := range products {
productName[p.ID] = p.Name
}
}
list := make([]gin.H, 0, len(pks))
for _, pk := range pks {
list = append(list, gin.H{
"id": pk.ID,
"domain": pk.Domain,
"course_id": pk.CourseID,
"product_id": pk.ProductID,
"course_name": nameOrDash(courseName, pk.CourseID),
"product_name": nameOrDash(productName, pk.ProductID),
"required_level": pk.RequiredLevel,
"weight": pk.Weight,
"is_mandatory": pk.IsMandatory,
})
}
web.OK(c, gin.H{
"position": pos,
"knowledge": list,
"count": len(list),
})
}
func nameOrDash(m map[uint]string, id *uint) string {
if id == nil {
return ""
}
if n, ok := m[*id]; ok {
return n
}
return ""
}
// ============ 岗位考试蓝图(管理员) ============
type blueprintReq struct {
Domain string `json:"domain"`
Type string `json:"type"`
Count int `json:"count"`
}
// ListPositionBlueprint GET /api/positions/{id}/blueprint —— 某岗位考试蓝图列表
func ListPositionBlueprint(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
var items []model.PositionExamBlueprint
if err := store.DB.Where("position_id = ?", id).Order("id ASC").Find(&items).Error; err != nil {
web.Fail(c, web.NewBadRequest("查询岗位考试蓝图失败"))
return
}
web.OK(c, items)
}
// SavePositionBlueprint PUT /api/positions/{id}/blueprint —— 保存蓝图(整表覆盖)
func SavePositionBlueprint(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
var pos model.Position
if err := store.DB.First(&pos, id).Error; err != nil {
web.Fail(c, web.NewNotFoundError("岗位不存在"))
return
}
var req struct {
Items []blueprintReq `json:"items"`
}
if err := c.ShouldBindJSON(&req); err != nil {
web.Fail(c, web.NewBadRequest("请求参数错误"))
return
}
rows := make([]model.PositionExamBlueprint, 0, len(req.Items))
for _, it := range req.Items {
if !validQTypes[it.Type] {
web.Fail(c, web.NewBadRequest("题型 type 非法(仅 single/multiple/judge/essay)"))
return
}
if it.Domain != "" && !validDomains[it.Domain] {
web.Fail(c, web.NewBadRequest("知识域 domain 非法(仅 company/product/sales,或留空不限)"))
return
}
if it.Count <= 0 {
web.Fail(c, web.NewBadRequest("蓝图各条 count 必须大于 0"))
return
}
rows = append(rows, model.PositionExamBlueprint{
PositionID: id, Domain: it.Domain, Type: it.Type, Count: it.Count,
})
}
if err := store.DB.Where("position_id = ?", id).Delete(&model.PositionExamBlueprint{}).Error; err != nil {
web.Fail(c, web.NewBadRequest("清除旧蓝图失败"))
return
}
if len(rows) > 0 {
if err := store.DB.Create(&rows).Error; err != nil {
web.Fail(c, web.NewBadRequest("保存岗位考试蓝图失败"))
return
}
}
var out []model.PositionExamBlueprint
store.DB.Where("position_id = ?", id).Order("id ASC").Find(&out)
web.OK(c, out)
}