Files
pj0235-eai_agentplatform/eai_agentplatform/backend-go/internal/api/position.go
T
eaiadminandClaude Code e8aedd50d2 refactor: 后端仓库层更名为数据访问层(internal/repository → internal/dal)
「仓库层」是 repository 的直译,中文里与「代码仓库 / git 仓库」同词,
而这一层做的事就是数据访问。名字改成它实际在做的事。

改名口径(纯机械替换,无逻辑改动):
- 包:internal/repository → internal/dal(package repository → package dal)
- 类型:XxxRepo → XxxDAO(TaskRecordDAO / SpecialistDAO / PositionDAO …)
- 变量:xxxRepo → xxxDAO
- import 路径、包限定符、日志前缀 [repository] → [dal] 同步
- 注释里的「仓库层」→「数据访问层」;core.go 包注释补上 DAL/DAO 全称

命名规范补登(AR09 是命名问题的最高依据,改了名就得回去登记):
- AR09 §3.1 术语表新增「数据访问层 dal / DAO」一行
- AR09 §5.6 缩写表新增 DAO / dal —— 原文是「只有下表内的缩写允许使用」,
  不登记就是自己破自己的规矩
- PROJECT_STATE.md 新增 D27 记录本次更名决策

验证:全部在 db 副本上做,生产库 data/eai_agentplatform.db 未触碰。
- 等价性对照:拿 HEAD 源码 + 仅改名 造出第二棵树,两棵树各自起
  httptest 服务跑同一份探针(60 个 GET + 13 个写/回读,覆盖专员/技能/应用/
  任务/交付物/项目/岗位/考试/知识/积分/管理端只读等),逐端点比对响应体:
  73 项里 52 项字节完全一致、21 项仅运行期时间戳不同、内容差异 0。
- 探针非空:往改名后的树注入「SpecialistDAO.List 限 3 条」变异,
  /api/specialists 立刻被抓出 —— 证明上面那个 0 不是没测到。
- 暂存区自洽:把索引整个导出成源码树,go build / go vet / go test ./... 全绿。
- gofmt:因 import 排序变化而错位的 19 个文件已修;另 2 个文件(skill_definition.go、
  seed.go)的格式问题是工作区里别人的在制品带来的,未替其改动。

未纳入本次提交:工作区里正在进行中的「文生语音技能 + 技能展示色/交互卡」
(tts_handlers.go、text_to_speech/manifest.go、skillCatalog.js 等),
以及 router.go / skill_definition.go / seed.go 三个文件里属于该在制品的改动 ——
这三个文件只把「改名那一版」放进索引,工作区原样保留。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 09:19:21 +08:00

369 lines
9.7 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 (
"github.com/gin-gonic/gin"
"eai_agentplatform/backend/internal/dal"
"eai_agentplatform/backend/internal/middleware"
"eai_agentplatform/backend/internal/model"
"eai_agentplatform/backend/internal/web"
)
var (
positionDAO dal.PositionDAO
courseDAO dal.CourseDAO // productDAO 定义在 products.go
)
func init() {
positionDAO = dal.PositionDAO{}
courseDAO = dal.CourseDAO{}
}
// ============ 岗位 CRUD(管理员) ============
// ListPositions GET /api/positions?status= —— 岗位列表
func ListPositions(c *gin.Context) {
items := positionDAO.List(c.Query("status"))
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
}
if p.Status == "" {
p.Status = "active"
}
if positionDAO.CountByName(p.Code, nil) > 0 {
web.Fail(c, web.NewConflictError("岗位编号已存在"))
return
}
if !positionDAO.Insert(&p) {
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
}
p, found := positionDAO.GetByID(id)
if !found {
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 {
if positionDAO.CountByName(req.Code, &id) > 0 {
web.Fail(c, web.NewConflictError("岗位编号已存在"))
return
}
p.Code = req.Code
}
p.Name = req.Name
p.Description = req.Description
p.Status = req.Status
if !positionDAO.Update(&p) {
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
}
if !positionDAO.Delete(id) {
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"`
// 三态:nil = 客户端没传,非 nil = 明确说了 true/false。
// 与 skillapi.definitionReq.ExposedToUser 同一处理:压成 bool 的话,
// 「没传」和「传 false」都会被当成「显式要求选学」,而后者必须写得进去。
IsMandatory *bool `json:"is_mandatory"`
}
// mandatoryOrTrue 解析 is_mandatory 的三态:没传按 true(必学)。
//
// 这个默认值原先由库列默认值(default:true)兜着,但 GORM 在 Create 时会跳过
// 带 default 标签的布尔零值字段,于是「显式传 false(选学)」被库默认值改写回
// true —— 前端那个必学/选学开关怎么拨都存成必学。默认值挪到这一层,归接口契约管。
func mandatoryOrTrue(v *bool) bool {
if v == nil {
return true
}
return *v
}
// ListPositionKnowledge GET /api/positions/{id}/knowledge —— 某岗位知识映射列表
func ListPositionKnowledge(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
items := positionDAO.Knowledge(id)
if items == nil {
items = []model.PositionKnowledge{}
}
web.OK(c, items)
}
// SavePositionKnowledge PUT /api/positions/{id}/knowledge —— 保存映射(整表覆盖)
func SavePositionKnowledge(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
_, found := positionDAO.GetByID(id)
if !found {
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: mandatoryOrTrue(it.IsMandatory),
})
}
// 整表覆盖:先删旧,再批量插入
if !positionDAO.ReplaceKnowledge(id, rows) {
web.Fail(c, web.NewBadRequest("保存岗位知识映射失败"))
return
}
out := positionDAO.Knowledge(id)
if out == nil {
out = []model.PositionKnowledge{}
}
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
}
u, found := userDAO.GetByID(id)
if !found {
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
}
if req.PositionID != nil {
_, found := positionDAO.GetByID(*req.PositionID)
if !found {
web.Fail(c, web.NewBadRequest("岗位不存在或已停用"))
return
}
}
u.PositionID = req.PositionID
if !userDAO.Update(&u) {
web.Fail(c, web.NewBadRequest("设置用户岗位失败"))
return
}
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
}
pos, found := positionDAO.GetByID(*u.PositionID)
if !found || pos.Status != "active" {
web.OK(c, gin.H{"position": nil, "knowledge": []gin.H{}, "count": 0})
return
}
pks := positionDAO.Knowledge(pos.ID)
// 批量解析课程/产品名称
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 := courseDAO.NamesByIDs(courseIDs)
productName := productDAO.NamesByIDs(productIDs)
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),
})
}
// ============ 岗位考试蓝图(管理员) ============
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
}
items := positionDAO.Blueprints(id)
if items == nil {
items = []model.PositionExamBlueprint{}
}
web.OK(c, items)
}
// SavePositionBlueprint PUT /api/positions/{id}/blueprint —— 保存蓝图(整表覆盖)
func SavePositionBlueprint(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
_, found := positionDAO.GetByID(id)
if !found {
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 !positionDAO.ReplaceBlueprints(id, rows) {
web.Fail(c, web.NewBadRequest("保存岗位考试蓝图失败"))
return
}
out := positionDAO.Blueprints(id)
if out == nil {
out = []model.PositionExamBlueprint{}
}
web.OK(c, out)
}
func nameOrDash(m map[uint]string, id *uint) string {
if id == nil {
return ""
}
if n, ok := m[*id]; ok {
return n
}
return ""
}