Files
pj0235-eai_agentplatform/eai_agentplatform/backend-go/internal/api/profile.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

157 lines
4.0 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 (
"encoding/json"
"github.com/gin-gonic/gin"
"eai_agentplatform/backend/internal/middleware"
"eai_agentplatform/backend/internal/model"
"eai_agentplatform/backend/internal/store"
"eai_agentplatform/backend/internal/web"
)
// 本文件用到的仓库:questionRepo/examRecordRepo 声明在 exam.go,learningRepo 声明在 learning.go。
// domainLabels 能力雷达维度(正式考试按域聚合的掌握度)。
var domainLabels = map[string]string{
"company": "公司认知",
"product": "产品知识",
"sales": "销售能力",
}
// domainOrder 雷达图固定排序。
var domainOrder = []string{"company", "product", "sales"}
// detailWrap 正式考试答题明细(与 ExamSubmit 持久化的 detail_json 结构对齐)。
type detailWrap struct {
Questions []struct {
QuestionID uint `json:"question_id"`
IsCorrect bool `json:"is_correct"`
} `json:"questions"`
}
// MyProfile GET /api/my/profile —— 学员学习档案:能力雷达 + 学习统计 + 成绩趋势 + 积分。
func MyProfile(c *gin.Context) {
u := middleware.CurrentUser(c)
if u == nil {
web.Fail(c, web.NewAuthError("未登录"))
return
}
// 学习进度(公司/产品/课程浏览)
lps := learningRepo.ListByUser(u.ID)
companyViewed := false
productViewed, courseViewed := 0, 0
for _, lp := range lps {
switch lp.ItemType {
case "company":
companyViewed = true
case "product":
productViewed++
case "course":
courseViewed++
}
}
// 错题统计
mistakeTotal := mistakeRepo.CountByUser(u.ID)
mistakeResolved := mistakeRepo.CountResolvedByUser(u.ID)
// 自测次数(自测不落 exam_record,改由积分流水统计)
var selfTestCount int64
store.DB.Model(&model.PointEvent{}).Where("user_id = ? AND event_type = ?", u.ID, "self_test").Count(&selfTestCount)
// 正式考试记录 + 按域能力聚合(趋势图要按时间正序)
recs := examRecordRepo.ListByUserChronological(u.ID)
// 题目 → 域映射(用于从答题明细反推各域掌握度)
qDomain := questionRepo.DomainMap()
type domainAgg struct {
Correct int `json:"correct"`
Total int `json:"total"`
}
perDomain := map[string]*domainAgg{}
formalPassed := 0
trend := make([]gin.H, 0, len(recs))
for _, r := range recs {
if r.Passed {
formalPassed++
}
trend = append(trend, gin.H{
"exam_name": r.ExamName,
"score": r.Score,
"total_score": r.TotalScore,
"passed": r.Passed,
"submitted_at": r.SubmittedAt,
})
var dw detailWrap
if err := json.Unmarshal([]byte(r.DetailJSON), &dw); err != nil {
continue
}
for _, qd := range dw.Questions {
domain := qDomain[qd.QuestionID]
if domain == "" {
continue
}
a := perDomain[domain]
if a == nil {
a = &domainAgg{}
perDomain[domain] = a
}
a.Total++
if qd.IsCorrect {
a.Correct++
}
}
}
domains := make([]gin.H, 0, len(domainOrder))
for _, d := range domainOrder {
a := perDomain[d]
if a == nil {
a = &domainAgg{}
}
mastery := 0.0
if a.Total > 0 {
mastery = round1(float64(a.Correct) * 100 / float64(a.Total))
}
domains = append(domains, gin.H{
"domain": d,
"label": domainLabels[d],
"correct": a.Correct,
"total": a.Total,
"mastery": mastery,
})
}
// 薄弱点:有作答记录中掌握度最低的域
weakDomain := ""
weakMastery := 100.0
for _, d := range domains {
if d["total"].(int) > 0 && d["mastery"].(float64) < weakMastery {
weakMastery = d["mastery"].(float64)
weakDomain = d["domain"].(string)
}
}
web.OK(c, gin.H{
"learning_points": u.LearningPoints,
"stats": gin.H{
"company_viewed": companyViewed,
"product_viewed": productViewed,
"course_viewed": courseViewed,
"self_test_count": selfTestCount,
"formal_count": len(recs),
"formal_passed": formalPassed,
"mistake_count": mistakeTotal,
"mistake_resolved": mistakeResolved,
},
"domains": domains,
"weak_domain": weakDomain,
"recent_scores": trend,
})
}