Files
pj0235-eai_agentplatform/eai_agentplatform/backend-go/internal/api/learning.go
T
eaiadminandClaude Code 19cf6fb5f2 refactor: 后端仓库层收口(A3:考试/部门/学习/证书/档案/公司介绍)
把 A 档剩余对象的 api 裸查询全部收进仓库,api 层裸 store.DB 从 182 降到 99,
剩下的全是 B 档(任务/项目/笔记等尚无仓库的对象)与 C 档(报表聚合查询)。

按对象补齐的仓库方法:
- QuestionRepo:List 重写(status 档位改为显式 all/空/具体值)、
  ListByIDs(判分不过滤 status)、ListActiveByIDs(下发剔除停用)、
  ActivePool(抽题口径,主流程与蓝图共用)、DomainMap(能力雷达反查域)
- ExamPaperRepo.List;ExamRecordRepo.ListByUserChronological(趋势图正序)
- DepartmentRepo.ListByStatus / CountByName
- UserRepo.ListEmployees / CountActiveByDepartment / RenameDepartment
- LearningProgressRepo.ListAll;CertificateRepo.ListAll / GetByExamRecord
- MediaFileRepo.ListApprovedByBindType

顺带修掉两处隐患:
- CertificateRepo.GetByUserAndExam 按不存在的 exam_id 列查,一调即 SQL 报错,
  换成按 exam_record_id 的 GetByExamRecord(颁发幂等本来就该按考试记录)
- exam.go 与 system.go 各声明了一个 ExamRecordRepo 变量,同一个仓库两份变量
  会导致测试覆写时行为分叉,统一为一个 examRecordRepo

考证来源(趋势图正序 vs 列表页倒序)与抽题口径(岗位蓝图/岗位知识映射两条路径)
各自抽成单一出处,避免两处手写漂移。聚合与百分比计算仍留在 handler,未搬进仓库。

验证:tmp 验证程序走真实路由 + 真实 HTTP,对 DB 副本跑 111 项断言全绿
(覆盖停用题仍可判分、错题重练剔除停用题、趋势正序、改名同步 user.department
且 updated_at 仍刷新、未通过的正式考不发证书、公司介绍只出 approved 素材等)。
另对其中 8 条关键语义做了变异测试:逐条注入反向实现,确认断言确实会失败,
并因此发现并修掉验证程序自身一处漏洞(写语句的约束错误只在 rows.Err() 浮出,
原先未检查,导致一条断言实为空断言)。

原始 data/eai_agentplatform.db 全程未触碰,md5 复核一致。

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

121 lines
3.5 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 (
"time"
"github.com/gin-gonic/gin"
"eai_agentplatform/backend/internal/middleware"
"eai_agentplatform/backend/internal/model"
"eai_agentplatform/backend/internal/repository"
"eai_agentplatform/backend/internal/web"
)
// learningRepo 学习进度仓库(便于测试时覆写),包内共享。
var learningRepo repository.LearningProgressRepo
func init() {
learningRepo = repository.LearningProgressRepo{}
}
var validItemTypes = map[string]bool{"company": true, "product": true, "course": true}
// RecordLearningProgress POST /api/learning/progress —— 员工浏览内容时上报进度
func RecordLearningProgress(c *gin.Context) {
u := middleware.CurrentUser(c)
if u == nil {
web.Fail(c, web.NewAuthError("未登录"))
return
}
var req struct {
ItemType string `json:"item_type"`
ItemID uint `json:"item_id"`
}
if err := c.ShouldBindJSON(&req); err != nil || !validItemTypes[req.ItemType] {
web.Fail(c, web.NewBadRequest("item_type 必填且合法(company/product/course)"))
return
}
if req.ItemType == "company" {
req.ItemID = 0
}
lp := model.LearningProgress{UserID: u.ID, ItemType: req.ItemType, ItemID: req.ItemID}
// 幂等:已存在则仅刷新 updated_at;首次记录才加分(避免重复刷分)
if _, exists := learningRepo.Get(u.ID, req.ItemType, req.ItemID); !exists {
if !learningRepo.Insert(&lp) {
web.Fail(c, web.NewBadRequest("记录学习进度失败"))
return
}
awardFirstView(u.ID, req.ItemType, req.ItemID)
} else {
// Upsert 命中已存在分支时只刷新 updated_at,不新增行
learningRepo.Upsert(&lp)
}
web.OK(c, gin.H{"recorded": true, "item_type": req.ItemType, "item_id": req.ItemID})
}
// MyLearningProgress GET /api/learning/me —— 我的学习进度
func MyLearningProgress(c *gin.Context) {
u := middleware.CurrentUser(c)
if u == nil {
web.Fail(c, web.NewAuthError("未登录"))
return
}
items := learningRepo.ListByUser(u.ID)
if items == nil {
items = []model.LearningProgress{}
}
web.OK(c, items)
}
// AdminLearningProgress GET /api/system/learning-progress —— 全员学习进度(管理员)
func AdminLearningProgress(c *gin.Context) {
// 管理端全员视图:不限状态,已停用的员工也要能看到其历史进度
employees := userRepo.ListEmployees("")
items := learningRepo.ListAll()
type agg struct {
CompanyViewed bool `json:"company_viewed"`
ProductCount int `json:"product_count"`
CourseCount int `json:"course_count"`
TotalItems int `json:"total_items"`
LastViewedAt *time.Time `json:"last_viewed_at"`
}
perUser := map[uint]*agg{}
for _, lp := range items {
a := perUser[lp.UserID]
if a == nil {
a = &agg{}
perUser[lp.UserID] = a
}
switch lp.ItemType {
case "company":
a.CompanyViewed = true
case "product":
a.ProductCount++
case "course":
a.CourseCount++
}
a.TotalItems++
if a.LastViewedAt == nil || lp.UpdatedAt.After(*a.LastViewedAt) {
t := lp.UpdatedAt
a.LastViewedAt = &t
}
}
out := make([]gin.H, 0, len(employees))
for _, e := range employees {
a := perUser[e.ID]
if a == nil {
a = &agg{}
}
out = append(out, gin.H{
"user_id": e.ID, "username": e.Username, "full_name": e.FullName,
"department": e.Department, "position": e.Position,
"company_viewed": a.CompanyViewed, "product_count": a.ProductCount,
"course_count": a.CourseCount, "total_items": a.TotalItems,
"last_viewed_at": a.LastViewedAt,
})
}
web.OK(c, gin.H{"users": out})
}