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

352 lines
9.6 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/csv"
"encoding/json"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"eai_agentplatform/backend/internal/auth"
"eai_agentplatform/backend/internal/dal"
"eai_agentplatform/backend/internal/model"
"eai_agentplatform/backend/internal/web"
)
// systemConfigDAO 声明在此;examRecordDAO 与考试域同属一个对象,
// 统一声明在 exam.go 的「考试域仓库」块内,避免同一个仓库有两份变量导致覆写时行为分叉。
var (
userDAO dal.UserDAO
configDAO dal.SystemConfigDAO
)
func init() {
userDAO = dal.UserDAO{}
configDAO = dal.SystemConfigDAO{}
}
// ============ 用户管理(管理员) ============
// ListUsers GET /api/system/users —— 含考试统计(考试次数/通过数/最近成绩)
func ListUsers(c *gin.Context) {
var items []model.User
userDAO.Query().Type(&model.User{}).Order("id ASC").Find(&items)
// 获取考试记录
var recs []model.ExamRecord
examRecordDAO.Inner().Model(&model.ExamRecord{}).Order("submitted_at DESC").Find(&recs)
type stat struct {
ExamCount int
PassedCount int
LatestScore *int
LatestPassed *bool
LatestExamName string
LatestSubmittedAt *time.Time
}
stats := map[uint]*stat{}
for _, r := range recs {
s := stats[r.UserID]
if s == nil {
s = &stat{}
stats[r.UserID] = s
}
s.ExamCount++
if r.Passed {
s.PassedCount++
}
if s.LatestSubmittedAt == nil {
sc, ps := r.Score, r.Passed
t := r.SubmittedAt
s.LatestScore, s.LatestPassed = &sc, &ps
s.LatestExamName = r.ExamName
s.LatestSubmittedAt = &t
}
}
out := make([]gin.H, 0, len(items))
for _, u := range items {
h := gin.H{
"id": u.ID, "username": u.Username, "full_name": u.FullName,
"role": u.Role, "status": u.Status, "ai_points": u.AiPoints,
"department": u.Department, "position": u.Position, "hire_batch": u.HireBatch,
"position_id": u.PositionID,
"created_at": u.CreatedAt,
"exam_count": 0, "passed_count": 0,
"latest_score": nil, "latest_passed": nil, "latest_exam_name": "", "latest_submitted_at": nil,
}
if s := stats[u.ID]; s != nil {
h["exam_count"] = s.ExamCount
h["passed_count"] = s.PassedCount
h["latest_score"] = s.LatestScore
h["latest_passed"] = s.LatestPassed
h["latest_exam_name"] = s.LatestExamName
h["latest_submitted_at"] = s.LatestSubmittedAt
}
out = append(out, h)
}
web.OK(c, out)
}
// CreateUser POST /api/system/users —— {username,password,full_name,role,department,position,hire_batch}
func CreateUser(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
FullName string `json:"full_name"`
Role string `json:"role"`
Department string `json:"department"`
Position string `json:"position"`
HireBatch string `json:"hire_batch"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" || req.Password == "" || req.FullName == "" {
web.Fail(c, web.NewBadRequest("username/password/full_name 必填"))
return
}
if req.Role != "admin" && req.Role != "employee" {
req.Role = "employee"
}
_, found := userDAO.GetByUsername(req.Username)
if found {
web.Fail(c, web.NewConflictError("用户名已存在"))
return
}
hash, err := auth.HashPassword(req.Password)
if err != nil {
web.Fail(c, web.NewBadRequest("密码加密失败"))
return
}
u := model.User{
Username: req.Username, PasswordHash: hash, FullName: req.FullName,
Role: req.Role, Status: "active", AiPoints: defaultAiPoints(),
Department: req.Department, Position: req.Position, HireBatch: req.HireBatch,
}
if req.Role == "admin" {
u.AiPoints = 999999
}
if !userDAO.Insert(&u) {
web.Fail(c, web.NewBadRequest("创建用户失败"))
return
}
web.OK(c, u)
}
// UpdateUser PUT /api/system/users/{id} —— 编辑(可选改密/禁用/改角色)
func UpdateUser(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 {
FullName string `json:"full_name"`
Role string `json:"role"`
Status string `json:"status"`
Password string `json:"password"`
AiPoints *int `json:"ai_points"`
Department string `json:"department"`
Position string `json:"position"`
HireBatch string `json:"hire_batch"`
}
if err := c.ShouldBindJSON(&req); err != nil {
web.Fail(c, web.NewBadRequest("请求参数错误"))
return
}
if req.FullName != "" {
u.FullName = req.FullName
}
if req.Role == "admin" || req.Role == "employee" {
u.Role = req.Role
}
if req.Status == "active" || req.Status == "disabled" {
u.Status = req.Status
}
if req.Department != "" {
u.Department = req.Department
}
if req.Position != "" {
u.Position = req.Position
}
if req.HireBatch != "" {
u.HireBatch = req.HireBatch
}
if req.Password != "" {
hash, err := auth.HashPassword(req.Password)
if err != nil {
web.Fail(c, web.NewBadRequest("密码加密失败"))
return
}
u.PasswordHash = hash
}
if req.AiPoints != nil {
if *req.AiPoints < 0 {
web.Fail(c, web.NewBadRequest("ai_points 不能为负"))
return
}
u.AiPoints = *req.AiPoints
}
if !userDAO.Update(&u) {
web.Fail(c, web.NewBadRequest("更新用户失败"))
return
}
web.OK(c, u)
}
// defaultAiPoints 读取新用户默认 AI 算力点(system_config.ai_points_default,缺省 100)
func defaultAiPoints() int {
val := configDAO.GetByKey("ai_points_default")
if v, err := strconv.Atoi(strings.TrimSpace(val)); err == nil {
return v
}
return 100
}
// ============ 成绩管理(管理员) ============
// ListExamRecords GET /api/system/exam-records?user_id=&paper_id=
func ListExamRecords(c *gin.Context) {
items := examRecordDAO.List(c.Query("user_id"), c.Query("paper_id"))
web.OK(c, items)
}
// pointerUint 将字符串解析为 *uint;空串返回 nil。
func pointerUint(s string) *uint {
if s == "" {
return nil
}
if v, err := strconv.ParseUint(s, 10, 32); err == nil {
uv := uint(v)
return &uv
}
return nil
}
// GetExamRecord GET /api/system/exam-records/{id} —— 详情
func GetExamRecord(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
rec, found := examRecordDAO.GetByID(id)
if !found {
web.Fail(c, web.NewNotFoundError("考试记录不存在"))
return
}
var detail any
_ = json.Unmarshal([]byte(rec.DetailJSON), &detail)
web.OK(c, gin.H{
"id": rec.ID, "user_id": rec.UserID, "paper_id": rec.PaperID, "exam_name": rec.ExamName,
"score": rec.Score, "total_score": rec.TotalScore, "pass_score": rec.PassScore, "passed": rec.Passed,
"correct_count": rec.CorrectCount, "wrong_count": rec.WrongCount,
"detail": detail, "submitted_at": rec.SubmittedAt,
})
}
// DeleteExamRecord DELETE /api/system/exam-records/{id} —— 删除成绩记录(用于重置正式考重考资格)
func DeleteExamRecord(c *gin.Context) {
id, ok := parseID(c, "id")
if !ok {
return
}
if !examRecordDAO.Remove(id) {
web.Fail(c, web.NewBadRequest("删除考试记录失败"))
return
}
web.OK(c, gin.H{"id": id, "deleted": true})
}
// ExportExamRecords GET /api/system/exam-records/export —— 导出 CSV(支持 user_id/paper_id 过滤)
func ExportExamRecords(c *gin.Context) {
var items []model.ExamRecord
q := dal.DB.Model(&model.ExamRecord{})
if uid := c.Query("user_id"); uid != "" {
q = q.Where("user_id = ?", uid)
}
if pid := c.Query("paper_id"); pid != "" {
q = q.Where("paper_id = ?", pid)
}
if err := q.Order("submitted_at DESC").Find(&items).Error; err != nil {
web.Fail(c, web.NewBadRequest("查询成绩失败"))
return
}
// 用户名映射
var users []model.User
dal.DB.Find(&users)
nameMap := map[uint]model.User{}
for _, u := range users {
nameMap[u.ID] = u
}
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", `attachment; filename="exam_records.csv"`)
c.Writer.WriteString("\xEF\xBB\xBF") // UTF-8 BOM,兼容 Excel
w := csv.NewWriter(c.Writer)
_ = w.Write([]string{"编号", "用户名", "姓名", "部门", "考试名称", "得分", "总分", "结果", "答对", "答错", "提交时间"})
for _, r := range items {
u := nameMap[r.UserID]
passed := "未通过"
if r.Passed {
passed = "通过"
}
_ = w.Write([]string{
strconv.FormatUint(uint64(r.ID), 10),
u.Username,
u.FullName,
u.Department,
r.ExamName,
strconv.Itoa(r.Score),
strconv.Itoa(r.TotalScore),
passed,
strconv.Itoa(r.CorrectCount),
strconv.Itoa(r.WrongCount),
r.SubmittedAt.Format("2006-01-02 15:04:05"),
})
}
w.Flush()
}
// ============ 系统参数配置(管理员) ============
// GetConfig GET /api/system/config —— 所有系统参数
func GetConfig(c *gin.Context) {
items := configDAO.List()
type cfgItem struct {
Key string `json:"config_key"`
Value string `json:"config_value"`
Description string `json:"description"`
}
out := make([]cfgItem, 0, len(items))
for _, it := range items {
out = append(out, cfgItem{Key: it.ConfigKey, Value: it.ConfigValue, Description: it.Description})
}
web.OK(c, gin.H{"configs": out})
}
// UpdateConfig PUT /api/system/config —— {configs: {key: value}}
func UpdateConfig(c *gin.Context) {
var req struct {
Configs map[string]string `json:"configs"`
}
if err := c.ShouldBindJSON(&req); err != nil || len(req.Configs) == 0 {
web.Fail(c, web.NewBadRequest("configs 必填"))
return
}
configDAO.BulkUpsert(toSystemConfigs(req.Configs))
web.OK(c, gin.H{"updated": len(req.Configs)})
}
func toSystemConfigs(m map[string]string) []model.SystemConfig {
items := make([]model.SystemConfig, 0, len(m))
for k, v := range m {
items = append(items, model.SystemConfig{ConfigKey: k, ConfigValue: v})
}
return items
}