把 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>
121 lines
3.5 KiB
Go
121 lines
3.5 KiB
Go
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})
|
||
}
|