refactor: 后端仓库层收口(A1:课程/产品/素材)+ 收进工作区既有对象化重构
本提交含两部分。第一部分是本轮工作;第二部分是此前一直留在工作区、
从未提交的对象化重构,与第一部分在文件上互相咬合(internal/repository
整个包都是未跟踪状态,且 api 层已有文件引用它),无法拆成两个可编译的提交。
一、仓库层收口 A1 批(本轮工作)
把 api 层手写的 store.DB 查询收进具名仓库方法,只给真正获益的对象做方法,
不机械包裹全量。本批迁移 22 处裸查询(courses.go 9 / media.go 12 / products.go 1),
新增方法:
- MediaFileRepo.ListByBind / ListForAudit / MarkExtracted
- KnowledgeChunkRepo.CountByMediaFile
- ProductRepo.GetVisibleByID
两条业务口径改由仓库单点持有,避免各处手写漂移:
「只有 approved 素材出现在课程详情」与「已停用产品不在课程详情露出」。
修掉两个真实缺陷:
- ProductRepo.GetByID 缺 Where 条件。此前 GET /api/products/{id} 对任意 id 都返回
第一条产品、对不存在的 id 返回 200,且 PUT /api/products/{id} 会覆盖第一条产品
—— 数据损坏级。全仓扫描确认这是唯一一处同型写法。
- ProductRepo.Delete 写 status="deleted",而 DELETE 处理器文档与回包都声称
"inactive",接口在说谎;管理员用 status=all 拉列表会看到前端不认识的状态。
已对齐为 inactive(与 CourseRepo.Delete 一致)。
删除 8 个零调用且列名不存在的死方法(一调即 SQL 报错):
- media_file 上的 file_path / file_type / approval_status 三列并不存在,
GetByPath / ListByType / UpdateStatus 全废
- knowledge_chunk 上的 space_id 列不存在(模型早已改为 knowledge_space_key),
List / Total / ListBySpaceIDs / DeleteBySpace / SearchByVector 全废
取舍边界:能对当前 schema 跑通的死方法保留,跑不通的删或修。
CourseRepo.List 补齐 status=all 档(此前传给它会当作 status='all' 过滤出空列表)。
该方法此前零调用,现与产品列表语义对齐。
验证:go build ./... 与 go test ./... 全绿;另用真实 HTTP 请求验证 34 项
(课程 17 / 产品 3 / 素材 14),跑在数据库副本与独立 KB_DATA_DIR 上,
含 multipart 真上传 → 审批 → pdftotext 提取 → 分片入库的完整链路。
二、此前未提交的对象化重构(非本轮工作)
- 新增 internal/repository 仓库层、connectors、skills、specialists、xapps、jsonutil,
model/task_record|task_run|task_artifact、api/task_runtime|action_definition|chat_message
- 删除 api/app_definition、connectors、my_app_center、notification、office_skill、
export_docx|pptx|xlsx、official_account_* 等,随 XApp/Skill/Specialist/Connector
可插拔打包方向(AR10/AR11)调整
- 资产目录归位:backend-go/knowledge_source → assets/knowledge/source、
training_materials → assets/training/materials;README 内相对路径同步加深两级;
deploy env 补 ASSET_ROOT_DIR 并改 KNOWLEDGE_SOURCE_DIR / TRAINING_MATERIALS_DIR
- 前端新增 skills/ specialists/ connectors/ xapps/ 目录与对应页面
验证:前端 npm run build 通过(7.26s)。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// Certificate 证书仓库。
|
||||
type CertificateRepo struct{ *QueryBuilder }
|
||||
|
||||
// GetByUserAndExam 获取某用户在某考试中的证书。
|
||||
func (r CertificateRepo) GetByUserAndExam(userID uint, examID uint) (model.Certificate, bool) {
|
||||
var c model.Certificate
|
||||
if r.Type(&c).Where("user_id = ? AND exam_id = ?", userID, examID).First(&c) {
|
||||
return c, true
|
||||
}
|
||||
return model.Certificate{}, false
|
||||
}
|
||||
|
||||
// ListByUser 获取某用户的证书列表。
|
||||
func (r CertificateRepo) ListByUser(userID uint) []model.Certificate {
|
||||
var items []model.Certificate
|
||||
if r.Type(&items).Where("user_id = ?", userID).Order("issued_at DESC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r CertificateRepo) GetByID(id uint) (model.Certificate, bool) {
|
||||
var c model.Certificate
|
||||
if r.Type(&c).Where("id = ?", id).First(&c) {
|
||||
return c, true
|
||||
}
|
||||
return model.Certificate{}, false
|
||||
}
|
||||
|
||||
// Insert 创建证书。
|
||||
func (r CertificateRepo) Insert(c *model.Certificate) bool {
|
||||
return r.QueryBuilder.Insert(c)
|
||||
}
|
||||
|
||||
// CountByUser 统计某用户的证书数。
|
||||
func (r CertificateRepo) CountByUser(userID uint) int64 {
|
||||
var c int64
|
||||
r.Inner().Model(&model.Certificate{}).Where("user_id = ?", userID).Count(&c)
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// Package repository 提供统一的数据访问层。
|
||||
//
|
||||
// 所有 handler 必须通过 Repository 访问数据,禁止直接调用 store.DB。
|
||||
// 核心 QueryBuilder 封装常用查询操作(FindAll / FindBy / FindByID / Create / Update / Delete),
|
||||
// 各实体仓库基于 QueryBuilder 构建领域方法。
|
||||
//
|
||||
// 设计原则:
|
||||
// - 只封装数据访问,不封装业务逻辑(业务逻辑留在 handler/service)
|
||||
// - 返回 *gorm.DB 的方法允许链式调用(Where / Order / Limit 等)
|
||||
// - 所有方法统一错误处理(错误已记录日志,调用方通过 bool 判断)
|
||||
package repository
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"eai_agentplatform/backend/internal/store"
|
||||
)
|
||||
|
||||
// DB 全局数据库句柄(供仓库方法使用)。
|
||||
// 实际使用时指向 store.DB。
|
||||
var DB *gorm.DB
|
||||
|
||||
func init() {
|
||||
DB = store.DB
|
||||
}
|
||||
|
||||
// SetDB 设置仓库使用的数据库句柄(测试时覆写)。
|
||||
func SetDB(db *gorm.DB) {
|
||||
DB = db
|
||||
}
|
||||
|
||||
// QueryBuilder 通用查询构建器。
|
||||
// 内部包装 *gorm.DB,提供安全的数据访问方法。
|
||||
type QueryBuilder struct {
|
||||
query *gorm.DB
|
||||
}
|
||||
|
||||
// New 创建新的查询构建器(用于新建记录)。
|
||||
func New(model any) *QueryBuilder {
|
||||
return &QueryBuilder{query: DB.Model(model)}
|
||||
}
|
||||
|
||||
// Table 创建基于已有表名的查询构建器(用于批量操作)。
|
||||
func Table(tableName string) *QueryBuilder {
|
||||
return &QueryBuilder{query: DB.Table(tableName)}
|
||||
}
|
||||
|
||||
// base 返回非 nil 的底层查询链。若调用方是零值仓库(内嵌 *QueryBuilder 为 nil),
|
||||
// 则从包级 DB 重新起步,避免 nil 指针解引用。
|
||||
func (q *QueryBuilder) base() *gorm.DB {
|
||||
if q == nil || q.query == nil {
|
||||
return DB
|
||||
}
|
||||
return q.query
|
||||
}
|
||||
|
||||
// Type 指定查询的模型类型(用于 Find 返回)。
|
||||
func (q *QueryBuilder) Type(model any) *QueryBuilder {
|
||||
return &QueryBuilder{query: q.base().Model(model)}
|
||||
}
|
||||
|
||||
// Query 返回当前查询构建器(用于链式调用)。
|
||||
func (q *QueryBuilder) Query() *QueryBuilder {
|
||||
return &QueryBuilder{query: q.base()}
|
||||
}
|
||||
|
||||
// Where 添加 WHERE 条件。
|
||||
func (q *QueryBuilder) Where(cond string, args ...any) *QueryBuilder {
|
||||
return &QueryBuilder{query: q.base().Where(cond, args...)}
|
||||
}
|
||||
|
||||
// Order 添加排序。
|
||||
func (q *QueryBuilder) Order(value string) *QueryBuilder {
|
||||
return &QueryBuilder{query: q.base().Order(value)}
|
||||
}
|
||||
|
||||
// Limit 限制返回数量。
|
||||
func (q *QueryBuilder) Limit(n int) *QueryBuilder {
|
||||
return &QueryBuilder{query: q.base().Limit(n)}
|
||||
}
|
||||
|
||||
// Offset 设置偏移。
|
||||
func (q *QueryBuilder) Offset(n int) *QueryBuilder {
|
||||
return &QueryBuilder{query: q.base().Offset(n)}
|
||||
}
|
||||
|
||||
// Scopes 应用 GORM scopes。
|
||||
func (q *QueryBuilder) Scopes(scopes ...func(*gorm.DB) *gorm.DB) *QueryBuilder {
|
||||
return &QueryBuilder{query: q.base().Scopes(scopes...)}
|
||||
}
|
||||
|
||||
// First 查询第一条记录。返回 (model, found)。
|
||||
func (q *QueryBuilder) First(model any) (found bool) {
|
||||
if err := q.base().First(model).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return false
|
||||
}
|
||||
log.Printf("[repository] First error: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Find 查询所有匹配记录。
|
||||
func (q *QueryBuilder) Find(dest any) (found bool) {
|
||||
if err := q.base().Find(dest).Error; err != nil {
|
||||
log.Printf("[repository] Find error: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Count 返回匹配记录数。
|
||||
func (q *QueryBuilder) Count() int64 {
|
||||
var c int64
|
||||
q.base().Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// Insert 插入记录。
|
||||
func (q *QueryBuilder) Insert(value any) bool {
|
||||
if err := q.base().Create(value).Error; err != nil {
|
||||
log.Printf("[repository] Insert error: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Create 插入记录(Insert 的别名)。
|
||||
func (q *QueryBuilder) Create(value any) bool {
|
||||
return q.Insert(value)
|
||||
}
|
||||
|
||||
// Save 保存记录(插入或更新)。
|
||||
func (q *QueryBuilder) Save(value any) bool {
|
||||
if err := q.base().Save(value).Error; err != nil {
|
||||
log.Printf("[repository] Save error: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Updates 按条件更新(只更新提供的非零字段)。
|
||||
func (q *QueryBuilder) Updates(value any) bool {
|
||||
if err := q.base().Updates(value).Error; err != nil {
|
||||
log.Printf("[repository] Updates error: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// UpdateColumn 更新单列。
|
||||
func (q *QueryBuilder) UpdateColumn(column string, value any) bool {
|
||||
if err := q.base().Update(column, value).Error; err != nil {
|
||||
log.Printf("[repository] UpdateColumn error: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Delete 删除记录。
|
||||
func (q *QueryBuilder) Delete(value any) bool {
|
||||
if err := q.base().Delete(value).Error; err != nil {
|
||||
log.Printf("[repository] Delete error: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// DeleteByID 按主键 ID 删除。
|
||||
func (q *QueryBuilder) DeleteByID(model any, id uint) bool {
|
||||
if err := q.base().Model(model).Where("id = ?", id).Delete(model).Error; err != nil {
|
||||
log.Printf("[repository] DeleteByID error: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Raw 执行原始 SQL。
|
||||
func (q *QueryBuilder) Raw(sql string, args ...any) *QueryBuilder {
|
||||
return &QueryBuilder{query: q.base().Raw(sql, args...)}
|
||||
}
|
||||
|
||||
// Scan 将结果扫描到目标结构体。
|
||||
func (q *QueryBuilder) Scan(dest any) bool {
|
||||
if err := q.base().Scan(dest).Error; err != nil {
|
||||
log.Printf("[repository] Scan error: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Inner 获取底层 *gorm.DB(高级用法,慎用)。
|
||||
func (q *QueryBuilder) Inner() *gorm.DB {
|
||||
return q.base()
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// Course 课程仓库。
|
||||
type CourseRepo struct{ *QueryBuilder }
|
||||
|
||||
// List 获取课程列表(按条件过滤)。
|
||||
//
|
||||
// status 的三档含义与产品列表一致,调用方直接透传 query 参数即可:
|
||||
// - ""(默认):仅 active —— 员工浏览视角
|
||||
// - "all":管理员维护视角,不按状态过滤
|
||||
// - 其他:按该状态精确过滤
|
||||
func (r CourseRepo) List(category, status string) []model.Course {
|
||||
q := r.Type(&model.Course{})
|
||||
if category != "" {
|
||||
q = q.Where("category = ?", category)
|
||||
}
|
||||
switch status {
|
||||
case "":
|
||||
q = q.Where("status = ?", "active")
|
||||
case "all":
|
||||
default:
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
var items []model.Course
|
||||
if q.Order("id ASC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r CourseRepo) GetByID(id uint) (model.Course, bool) {
|
||||
var c model.Course
|
||||
if r.Type(&c).Where("id = ?", id).First(&c) {
|
||||
return c, true
|
||||
}
|
||||
return model.Course{}, false
|
||||
}
|
||||
|
||||
// GetByCode 按编号获取。
|
||||
func (r CourseRepo) GetByCode(code string) (model.Course, bool) {
|
||||
var c model.Course
|
||||
if r.Type(&c).Where("code = ?", code).First(&c) {
|
||||
return c, true
|
||||
}
|
||||
return model.Course{}, false
|
||||
}
|
||||
|
||||
// Insert 创建课程。
|
||||
func (r CourseRepo) Insert(c *model.Course) bool {
|
||||
return r.QueryBuilder.Insert(c)
|
||||
}
|
||||
|
||||
// Update 更新课程。
|
||||
func (r CourseRepo) Update(c *model.Course) bool {
|
||||
return r.Save(c)
|
||||
}
|
||||
|
||||
// Delete 软删除。
|
||||
func (r CourseRepo) Delete(id uint) bool {
|
||||
return r.Type(&model.Course{}).Where("id = ?", id).Updates(map[string]any{"status": "inactive"})
|
||||
}
|
||||
|
||||
// CountByCode 按编号统计(唯一性检查)。
|
||||
func (r CourseRepo) CountByCode(code string, excludeID *uint) int64 {
|
||||
var c int64
|
||||
q := r.Inner().Model(&model.Course{}).Where("code = ?", code)
|
||||
if excludeID != nil {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
}
|
||||
q.Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// NamesByIDs 批量解析课程名称(id → name),用于列表页回填关联名称。
|
||||
// 未命中的 ID 不会出现在返回的 map 中,调用方需自行兜底。
|
||||
func (r CourseRepo) NamesByIDs(ids []uint) map[uint]string {
|
||||
names := map[uint]string{}
|
||||
if len(ids) == 0 {
|
||||
return names
|
||||
}
|
||||
var items []model.Course
|
||||
if r.Type(&items).Where("id IN ?", ids).Find(&items) {
|
||||
for _, c := range items {
|
||||
names[c.ID] = c.Name
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// Department 部门仓库。
|
||||
type DepartmentRepo struct{ *QueryBuilder }
|
||||
|
||||
// List 获取部门列表。
|
||||
func (r DepartmentRepo) List() []model.Department {
|
||||
var items []model.Department
|
||||
if r.Type(&items).Order("id ASC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r DepartmentRepo) GetByID(id uint) (model.Department, bool) {
|
||||
var d model.Department
|
||||
if r.Type(&d).Where("id = ?", id).First(&d) {
|
||||
return d, true
|
||||
}
|
||||
return model.Department{}, false
|
||||
}
|
||||
|
||||
// GetByName 按名称获取。
|
||||
func (r DepartmentRepo) GetByName(name string) (model.Department, bool) {
|
||||
var d model.Department
|
||||
if r.Type(&d).Where("name = ?", name).First(&d) {
|
||||
return d, true
|
||||
}
|
||||
return model.Department{}, false
|
||||
}
|
||||
|
||||
// Insert 创建部门。
|
||||
func (r DepartmentRepo) Insert(d *model.Department) bool {
|
||||
return r.QueryBuilder.Insert(d)
|
||||
}
|
||||
|
||||
// Update 更新部门。
|
||||
func (r DepartmentRepo) Update(d *model.Department) bool {
|
||||
return r.Save(d)
|
||||
}
|
||||
|
||||
// Delete 删除部门。
|
||||
func (r DepartmentRepo) Delete(id uint) bool {
|
||||
return r.DeleteByID(&model.Department{}, id)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// ExamPaper 试卷仓库。
|
||||
type ExamPaperRepo struct{ *QueryBuilder }
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r ExamPaperRepo) GetByID(id uint) (model.ExamPaper, bool) {
|
||||
var p model.ExamPaper
|
||||
if r.Type(&p).Where("id = ?", id).First(&p) {
|
||||
return p, true
|
||||
}
|
||||
return model.ExamPaper{}, false
|
||||
}
|
||||
|
||||
// GetActive 获取所有激活的试卷。
|
||||
func (r ExamPaperRepo) GetActive() []model.ExamPaper {
|
||||
var items []model.ExamPaper
|
||||
if r.Type(&items).Where("status = ?", "active").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Insert 创建试卷。
|
||||
func (r ExamPaperRepo) Insert(p *model.ExamPaper) bool {
|
||||
return r.QueryBuilder.Insert(p)
|
||||
}
|
||||
|
||||
// Update 更新试卷。
|
||||
func (r ExamPaperRepo) Update(p *model.ExamPaper) bool {
|
||||
return r.Save(p)
|
||||
}
|
||||
|
||||
// Delete 软删除。
|
||||
func (r ExamPaperRepo) Delete(id uint) bool {
|
||||
return r.Type(&model.ExamPaper{}).Where("id = ?", id).Updates(map[string]any{"status": "inactive"})
|
||||
}
|
||||
|
||||
// Count 统计试卷数。
|
||||
func (r ExamPaperRepo) Count() int64 {
|
||||
var c int64
|
||||
r.Inner().Model(&model.ExamPaper{}).Count(&c)
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// ExamRecord 考试记录仓库。
|
||||
type ExamRecordRepo struct{ *QueryBuilder }
|
||||
|
||||
// List 获取考试记录列表。
|
||||
func (r ExamRecordRepo) List(userIDStr, paperIDStr string) []model.ExamRecord {
|
||||
q := r.Type(&model.ExamRecord{})
|
||||
if userIDStr != "" {
|
||||
if uid, err := strconv.ParseUint(userIDStr, 10, 32); err == nil {
|
||||
q = q.Where("user_id = ?", uint(uid))
|
||||
}
|
||||
}
|
||||
if paperIDStr != "" {
|
||||
if pid, err := strconv.ParseUint(paperIDStr, 10, 32); err == nil {
|
||||
q = q.Where("paper_id = ?", uint(pid))
|
||||
}
|
||||
}
|
||||
var items []model.ExamRecord
|
||||
if q.Order("submitted_at DESC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r ExamRecordRepo) GetByID(id uint) (model.ExamRecord, bool) {
|
||||
var rec model.ExamRecord
|
||||
if r.Type(&rec).Where("id = ?", id).First(&rec) {
|
||||
return rec, true
|
||||
}
|
||||
return model.ExamRecord{}, false
|
||||
}
|
||||
|
||||
// Insert 创建记录。
|
||||
func (r ExamRecordRepo) Insert(rec *model.ExamRecord) bool {
|
||||
return r.QueryBuilder.Insert(rec)
|
||||
}
|
||||
|
||||
// Update 更新记录。
|
||||
func (r ExamRecordRepo) Update(rec *model.ExamRecord) bool {
|
||||
return r.Save(rec)
|
||||
}
|
||||
|
||||
// Remove 删除记录。
|
||||
func (r ExamRecordRepo) Remove(id uint) bool {
|
||||
return r.DeleteByID(&model.ExamRecord{}, id)
|
||||
}
|
||||
|
||||
// CountByUser 统计某用户的考试记录数。
|
||||
func (r ExamRecordRepo) CountByUser(userID uint) int64 {
|
||||
var c int64
|
||||
r.Inner().Model(&model.ExamRecord{}).Where("user_id = ?", userID).Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// HasTaken 判断用户是否已参加过某张试卷(正式考的唯一性依据)。
|
||||
// 试卷列表标注「已完成」、开考拦截、交卷拦截三处共用同一语义。
|
||||
func (r ExamRecordRepo) HasTaken(userID, paperID uint) bool {
|
||||
var c int64
|
||||
r.Inner().Model(&model.ExamRecord{}).
|
||||
Where("user_id = ? AND paper_id = ?", userID, paperID).
|
||||
Count(&c)
|
||||
return c > 0
|
||||
}
|
||||
|
||||
// ListByUser 取某用户的考试记录(按提交时间倒序)。
|
||||
func (r ExamRecordRepo) ListByUser(userID uint) []model.ExamRecord {
|
||||
var items []model.ExamRecord
|
||||
if r.Type(&items).Where("user_id = ?", userID).Order("submitted_at DESC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListAll 取全部考试记录(按提交时间倒序,供管理员导出与统计)。
|
||||
func (r ExamRecordRepo) ListAll() []model.ExamRecord {
|
||||
var items []model.ExamRecord
|
||||
if r.Type(&items).Order("submitted_at DESC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// KnowledgeChunk 知识分片仓库。
|
||||
type KnowledgeChunkRepo struct{ *QueryBuilder }
|
||||
|
||||
// ApprovedMetaMaps 批量加载解析分片元信息所需的「已审批」素材与知识源(id → 记录)。
|
||||
//
|
||||
// resolveChunkMeta 只认已审批的来源(素材 status=approved、知识源 audit_status=approved),
|
||||
// 所以这里的过滤条件必须与 resolveChunkMeta 内的判断保持一致:**传空 map 会让所有带
|
||||
// media_file_id / knowledge_source_id 的分片被整段丢弃**(历史上 loadKnowledgeCandidates
|
||||
// 就踩过这个坑,AI 检索候选集只剩「无指针」分片)。
|
||||
// AI 检索候选与知识索引构建共用此方法,避免两处各自手写过滤条件而漂移。
|
||||
func (r KnowledgeChunkRepo) ApprovedMetaMaps() (map[uint]model.MediaFile, map[uint]model.KnowledgeSource) {
|
||||
var mediaFiles []model.MediaFile
|
||||
r.Type(&mediaFiles).Where("status = ?", "approved").Find(&mediaFiles)
|
||||
mediaMap := make(map[uint]model.MediaFile, len(mediaFiles))
|
||||
for _, m := range mediaFiles {
|
||||
mediaMap[m.ID] = m
|
||||
}
|
||||
|
||||
var sources []model.KnowledgeSource
|
||||
r.Type(&sources).Where("audit_status = ?", "approved").Find(&sources)
|
||||
sourceMap := make(map[uint]model.KnowledgeSource, len(sources))
|
||||
for _, s := range sources {
|
||||
sourceMap[s.ID] = s
|
||||
}
|
||||
return mediaMap, sourceMap
|
||||
}
|
||||
|
||||
// CountByMediaFile 统计某素材切出的分片数(素材状态页用于展示提取结果)。
|
||||
func (r KnowledgeChunkRepo) CountByMediaFile(mediaID uint) int64 {
|
||||
var c int64
|
||||
r.Inner().Model(&model.KnowledgeChunk{}).Where("media_file_id = ?", mediaID).Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r KnowledgeChunkRepo) GetByID(id uint) (model.KnowledgeChunk, bool) {
|
||||
var c model.KnowledgeChunk
|
||||
if r.Type(&c).Where("id = ?", id).First(&c) {
|
||||
return c, true
|
||||
}
|
||||
return model.KnowledgeChunk{}, false
|
||||
}
|
||||
|
||||
// Insert 创建分片。
|
||||
func (r KnowledgeChunkRepo) Insert(c *model.KnowledgeChunk) bool {
|
||||
return r.QueryBuilder.Insert(c)
|
||||
}
|
||||
|
||||
// BulkInsert 批量创建分片。
|
||||
func (r KnowledgeChunkRepo) BulkInsert(items []model.KnowledgeChunk) int {
|
||||
count := 0
|
||||
for i := range items {
|
||||
if r.QueryBuilder.Insert(&items[i]) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Update 更新分片。
|
||||
func (r KnowledgeChunkRepo) Update(c *model.KnowledgeChunk) bool {
|
||||
return r.Save(c)
|
||||
}
|
||||
|
||||
// DeleteByID 按 ID 删除。
|
||||
func (r KnowledgeChunkRepo) RemoveByID(id uint) bool {
|
||||
return r.DeleteByID(&model.KnowledgeChunk{}, id)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// KnowledgeFAQ FAQ 仓库。
|
||||
type KnowledgeFAQRepo struct{ *QueryBuilder }
|
||||
|
||||
// List 获取 FAQ 列表(按知识空间过滤)。
|
||||
func (r KnowledgeFAQRepo) List(spaceID uint, keyword string) []model.KnowledgeFAQ {
|
||||
q := r.Type(&model.KnowledgeFAQ{}).Where("space_id = ?", spaceID)
|
||||
if keyword != "" {
|
||||
q = q.Where("question LIKE ?", "%"+keyword+"%")
|
||||
}
|
||||
var items []model.KnowledgeFAQ
|
||||
if q.Order("id ASC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r KnowledgeFAQRepo) GetByID(id uint) (model.KnowledgeFAQ, bool) {
|
||||
var f model.KnowledgeFAQ
|
||||
if r.Type(&f).Where("id = ?", id).First(&f) {
|
||||
return f, true
|
||||
}
|
||||
return model.KnowledgeFAQ{}, false
|
||||
}
|
||||
|
||||
// CountBySpace 统计某知识空间的 FAQ 数。
|
||||
func (r KnowledgeFAQRepo) CountBySpace(spaceID uint) int64 {
|
||||
var c int64
|
||||
r.Inner().Model(&model.KnowledgeFAQ{}).Where("space_id = ?", spaceID).Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// Insert 创建 FAQ。
|
||||
func (r KnowledgeFAQRepo) Insert(f *model.KnowledgeFAQ) bool {
|
||||
return r.QueryBuilder.Insert(f)
|
||||
}
|
||||
|
||||
// Update 更新 FAQ。
|
||||
func (r KnowledgeFAQRepo) Update(f *model.KnowledgeFAQ) bool {
|
||||
return r.Save(f)
|
||||
}
|
||||
|
||||
// Delete 删除 FAQ。
|
||||
func (r KnowledgeFAQRepo) Delete(id uint) bool {
|
||||
return r.DeleteByID(&model.KnowledgeFAQ{}, id)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// KnowledgeSource 知识源仓库。
|
||||
type KnowledgeSourceRepo struct{ *QueryBuilder }
|
||||
|
||||
// List 获取知识源列表。
|
||||
func (r KnowledgeSourceRepo) List(spaceID uint, status string) []model.KnowledgeSource {
|
||||
q := r.Type(&model.KnowledgeSource{}).Where("space_id = ?", spaceID)
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
var items []model.KnowledgeSource
|
||||
if q.Order("id ASC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r KnowledgeSourceRepo) GetByID(id uint) (model.KnowledgeSource, bool) {
|
||||
var s model.KnowledgeSource
|
||||
if r.Type(&s).Where("id = ?", id).First(&s) {
|
||||
return s, true
|
||||
}
|
||||
return model.KnowledgeSource{}, false
|
||||
}
|
||||
|
||||
// Insert 创建知识源。
|
||||
func (r KnowledgeSourceRepo) Insert(s *model.KnowledgeSource) bool {
|
||||
return r.QueryBuilder.Insert(s)
|
||||
}
|
||||
|
||||
// Update 更新知识源。
|
||||
func (r KnowledgeSourceRepo) Update(s *model.KnowledgeSource) bool {
|
||||
return r.Save(s)
|
||||
}
|
||||
|
||||
// Delete 软删除。
|
||||
func (r KnowledgeSourceRepo) Delete(id uint) bool {
|
||||
return r.Type(&model.KnowledgeSource{}).Where("id = ?", id).Updates(map[string]any{"status": "inactive"})
|
||||
}
|
||||
|
||||
// DeleteBySpace 删除某知识空间的所有知识源。
|
||||
func (r KnowledgeSourceRepo) DeleteBySpace(spaceID uint) bool {
|
||||
return r.Type(&model.KnowledgeSource{}).Where("space_id = ?", spaceID).Delete(&model.KnowledgeSource{})
|
||||
}
|
||||
|
||||
// CountBySpace 统计某知识空间的知识源数。
|
||||
func (r KnowledgeSourceRepo) CountBySpace(spaceID uint) int64 {
|
||||
var c int64
|
||||
r.Inner().Model(&model.KnowledgeSource{}).Where("space_id = ?", spaceID).Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// UpdateStatus 更新单个知识源的状态。
|
||||
func (r KnowledgeSourceRepo) UpdateStatus(id uint, status string) bool {
|
||||
return r.Type(&model.KnowledgeSource{}).Where("id = ?", id).UpdateColumn("status", status)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// KnowledgeSpace 知识空间仓库。
|
||||
type KnowledgeSpaceRepo struct{ *QueryBuilder }
|
||||
|
||||
// List 获取知识空间列表。
|
||||
func (r KnowledgeSpaceRepo) List(status string) []model.KnowledgeSpace {
|
||||
q := r.Type(&model.KnowledgeSpace{})
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
var items []model.KnowledgeSpace
|
||||
if q.Order("id ASC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r KnowledgeSpaceRepo) GetByID(id uint) (model.KnowledgeSpace, bool) {
|
||||
var s model.KnowledgeSpace
|
||||
if r.Type(&s).Where("id = ?", id).First(&s) {
|
||||
return s, true
|
||||
}
|
||||
return model.KnowledgeSpace{}, false
|
||||
}
|
||||
|
||||
// GetByName 按名称获取。
|
||||
func (r KnowledgeSpaceRepo) GetByName(name string) (model.KnowledgeSpace, bool) {
|
||||
var s model.KnowledgeSpace
|
||||
if r.Type(&s).Where("name = ?", name).First(&s) {
|
||||
return s, true
|
||||
}
|
||||
return model.KnowledgeSpace{}, false
|
||||
}
|
||||
|
||||
// Insert 创建知识空间。
|
||||
func (r KnowledgeSpaceRepo) Insert(s *model.KnowledgeSpace) bool {
|
||||
return r.QueryBuilder.Insert(s)
|
||||
}
|
||||
|
||||
// Update 更新知识空间。
|
||||
func (r KnowledgeSpaceRepo) Update(s *model.KnowledgeSpace) bool {
|
||||
return r.Save(s)
|
||||
}
|
||||
|
||||
// Delete 软删除。
|
||||
func (r KnowledgeSpaceRepo) Delete(id uint) bool {
|
||||
return r.Type(&model.KnowledgeSpace{}).Where("id = ?", id).Updates(map[string]any{"status": "inactive"})
|
||||
}
|
||||
|
||||
// CountByName 按名称统计(唯一性检查)。
|
||||
func (r KnowledgeSpaceRepo) CountByName(name string, excludeID *uint) int64 {
|
||||
q := r.Inner().Model(&model.KnowledgeSpace{}).Where("name = ?", name)
|
||||
if excludeID != nil {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
}
|
||||
var c int64
|
||||
q.Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// CountByID 按 ID 统计。
|
||||
func (r KnowledgeSpaceRepo) Count() int64 {
|
||||
var c int64
|
||||
r.Inner().Model(&model.KnowledgeSpace{}).Count(&c)
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// LearningProgress 学习进度仓库。
|
||||
type LearningProgressRepo struct{ *QueryBuilder }
|
||||
|
||||
// Get 获取用户某条学习进度。
|
||||
func (r LearningProgressRepo) Get(userID uint, itemType string, itemID uint) (model.LearningProgress, bool) {
|
||||
var p model.LearningProgress
|
||||
if r.Type(&p).Where("user_id = ? AND item_type = ? AND item_id = ?", userID, itemType, itemID).First(&p) {
|
||||
return p, true
|
||||
}
|
||||
return model.LearningProgress{}, false
|
||||
}
|
||||
|
||||
// ListByUser 获取某用户的所有学习进度。
|
||||
func (r LearningProgressRepo) ListByUser(userID uint) []model.LearningProgress {
|
||||
var items []model.LearningProgress
|
||||
if r.Type(&items).Where("user_id = ?", userID).Order("updated_at DESC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Upsert 插入或更新学习进度。
|
||||
func (r LearningProgressRepo) Upsert(p *model.LearningProgress) bool {
|
||||
existing, found := r.Get(p.UserID, p.ItemType, p.ItemID)
|
||||
if !found {
|
||||
return r.Insert(p)
|
||||
}
|
||||
existing.UpdatedAt = time.Now()
|
||||
return r.Save(&existing)
|
||||
}
|
||||
|
||||
// Record 记录一条学习进度。
|
||||
func (r LearningProgressRepo) Record(userID uint, itemType string, itemID uint) bool {
|
||||
_, found := r.Get(userID, itemType, itemID)
|
||||
if found {
|
||||
return r.Upsert(&model.LearningProgress{UserID: userID, ItemType: itemType, ItemID: itemID})
|
||||
}
|
||||
return r.Insert(&model.LearningProgress{UserID: userID, ItemType: itemType, ItemID: itemID, CreatedAt: time.Now(), UpdatedAt: time.Now()})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// MediaFile 媒体文件仓库。
|
||||
type MediaFileRepo struct{ *QueryBuilder }
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r MediaFileRepo) GetByID(id uint) (model.MediaFile, bool) {
|
||||
var m model.MediaFile
|
||||
if r.Type(&m).Where("id = ?", id).First(&m) {
|
||||
return m, true
|
||||
}
|
||||
return model.MediaFile{}, false
|
||||
}
|
||||
|
||||
// Insert 创建文件记录。
|
||||
func (r MediaFileRepo) Insert(m *model.MediaFile) bool {
|
||||
return r.QueryBuilder.Insert(m)
|
||||
}
|
||||
|
||||
// Update 更新文件记录。
|
||||
func (r MediaFileRepo) Update(m *model.MediaFile) bool {
|
||||
return r.Save(m)
|
||||
}
|
||||
|
||||
// Delete 删除文件记录。
|
||||
func (r MediaFileRepo) Delete(id uint) bool {
|
||||
return r.DeleteByID(&model.MediaFile{}, id)
|
||||
}
|
||||
|
||||
// ListByBind 获取绑定到某实体、且已审批通过的素材(id 升序)。
|
||||
//
|
||||
// 「审批前置」的落点:课程/产品详情只露出 approved 素材,pending 与 rejected 一律不出现在业务页面。
|
||||
// 调用方无需再自己拼 bind_type/bind_id/status 三条件,避免各处漏掉 status 过滤而泄漏未审批素材。
|
||||
func (r MediaFileRepo) ListByBind(bindType string, bindID uint) []model.MediaFile {
|
||||
var items []model.MediaFile
|
||||
if r.Type(&items).
|
||||
Where("bind_type = ? AND bind_id = ? AND status = ?", bindType, bindID, "approved").
|
||||
Order("id ASC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListForAudit 审批列表(分页):返回符合条件的总数与本页记录,created_at 倒序。
|
||||
//
|
||||
// status / spaceKey 为空表示不过滤该维度。page 从 1 起,size 由调用方校验后再传入。
|
||||
func (r MediaFileRepo) ListForAudit(status, spaceKey string, page, size int) (int64, []model.MediaFile) {
|
||||
q := r.Inner().Model(&model.MediaFile{})
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
if spaceKey != "" {
|
||||
q = q.Where("knowledge_space_key = ?", spaceKey)
|
||||
}
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
|
||||
var items []model.MediaFile
|
||||
q.Order("created_at DESC").Offset((page - 1) * size).Limit(size).Find(&items)
|
||||
return total, items
|
||||
}
|
||||
|
||||
// MarkExtracted 标记素材已完成提取(视频/图片仅有此标记,文档由提取管线置位)。
|
||||
func (r MediaFileRepo) MarkExtracted(id uint) bool {
|
||||
return r.Type(&model.MediaFile{}).Where("id = ?", id).UpdateColumn("extracted", true)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// MistakeRecordRepo 错题记录仓库。
|
||||
//
|
||||
// 错题本有两套口径,都收在本仓库里,避免调用方各写各的 Where:
|
||||
// - 入本/再次答错:唯一键 (user_id, question_id, source),见 RecordWrong
|
||||
// - 重练判分回写:按 (user_id, question_id) 扇出,把该题在所有来源下的记录一并更新,见 TouchOnPractice
|
||||
type MistakeRecordRepo struct{ *QueryBuilder }
|
||||
|
||||
// ListByUser 我的错题本(按最近更新倒序)。
|
||||
func (r MistakeRecordRepo) ListByUser(userID uint) []model.MistakeRecord {
|
||||
var items []model.MistakeRecord
|
||||
if r.Type(&items).Where("user_id = ?", userID).Order("updated_at DESC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListForPractice 错题重练选题:可按来源过滤、可只看未掌握(按最近更新倒序)。
|
||||
func (r MistakeRecordRepo) ListForPractice(userID uint, source string, onlyUnresolved bool) []model.MistakeRecord {
|
||||
q := r.Type(&model.MistakeRecord{}).Where("user_id = ?", userID)
|
||||
if source != "" {
|
||||
q = q.Where("source = ?", source)
|
||||
}
|
||||
if onlyUnresolved {
|
||||
q = q.Where("resolved = ?", false)
|
||||
}
|
||||
var items []model.MistakeRecord
|
||||
if q.Order("updated_at DESC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountByUser 统计某用户的错题总数。
|
||||
func (r MistakeRecordRepo) CountByUser(userID uint) int64 {
|
||||
var c int64
|
||||
r.Inner().Model(&model.MistakeRecord{}).Where("user_id = ?", userID).Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// CountResolvedByUser 统计某用户已掌握的错题数。
|
||||
func (r MistakeRecordRepo) CountResolvedByUser(userID uint) int64 {
|
||||
var c int64
|
||||
r.Inner().Model(&model.MistakeRecord{}).
|
||||
Where("user_id = ? AND resolved = ?", userID, true).Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r MistakeRecordRepo) GetByID(id uint) (model.MistakeRecord, bool) {
|
||||
var m model.MistakeRecord
|
||||
if r.Type(&m).Where("id = ?", id).First(&m) {
|
||||
return m, true
|
||||
}
|
||||
return model.MistakeRecord{}, false
|
||||
}
|
||||
|
||||
// RecordWrong 答错入本:按唯一键 (user_id, question_id, source) upsert。
|
||||
// 已有记录则整字段覆盖,并把 resolved 重置为未掌握——再次答错视为问题重新暴露。
|
||||
func (r MistakeRecordRepo) RecordWrong(m model.MistakeRecord) bool {
|
||||
m.Resolved = false
|
||||
var old model.MistakeRecord
|
||||
if !r.Type(&old).
|
||||
Where("user_id = ? AND question_id = ? AND source = ?", m.UserID, m.QuestionID, m.Source).
|
||||
First(&old) {
|
||||
return r.Insert(&m)
|
||||
}
|
||||
// Save 是整字段覆盖,必须带上原有主键与创建时间,否则 created_at 会被写成零值。
|
||||
m.ID = old.ID
|
||||
m.CreatedAt = old.CreatedAt
|
||||
return r.Save(&m)
|
||||
}
|
||||
|
||||
// TouchOnPractice 错题重练判分后回写:按 (user_id, question_id) 扇出,更新该题在
|
||||
// 所有来源下的作答与掌握状态。
|
||||
//
|
||||
// 返回 flipped 为本轮由「未掌握 → 已掌握」翻转的记录 ID,调用方据此加积分;
|
||||
// 只返回翻转的 ID(而非全部命中的记录)是为了避免反复重练刷分。
|
||||
// touched=false 表示该题在错题本里一条都没有,调用方需自行兜底补建记录。
|
||||
func (r MistakeRecordRepo) TouchOnPractice(m model.MistakeRecord, resolved bool) (flipped []uint, touched bool) {
|
||||
var recs []model.MistakeRecord
|
||||
if !r.Type(&recs).Where("user_id = ? AND question_id = ?", m.UserID, m.QuestionID).Find(&recs) {
|
||||
return nil, false
|
||||
}
|
||||
if len(recs) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
for i := range recs {
|
||||
was := recs[i].Resolved
|
||||
recs[i].Resolved = resolved
|
||||
recs[i].QuestionType = m.QuestionType
|
||||
recs[i].QuestionStem = m.QuestionStem
|
||||
recs[i].UserAnswer = m.UserAnswer
|
||||
recs[i].CorrectAnswer = m.CorrectAnswer
|
||||
recs[i].Explanation = m.Explanation
|
||||
r.Save(&recs[i])
|
||||
if resolved && !was {
|
||||
flipped = append(flipped, recs[i].ID)
|
||||
}
|
||||
}
|
||||
return flipped, true
|
||||
}
|
||||
|
||||
// Insert 创建错题记录。
|
||||
func (r MistakeRecordRepo) Insert(m *model.MistakeRecord) bool {
|
||||
return r.QueryBuilder.Insert(m)
|
||||
}
|
||||
|
||||
// Update 保存已有记录(改掌握状态等)。
|
||||
func (r MistakeRecordRepo) Update(m *model.MistakeRecord) bool {
|
||||
return r.Save(m)
|
||||
}
|
||||
|
||||
// Delete 删除错题记录。
|
||||
func (r MistakeRecordRepo) Delete(id uint) bool {
|
||||
return r.DeleteByID(&model.MistakeRecord{}, id)
|
||||
}
|
||||
|
||||
// DeleteByUser 删除某用户的全部错题。
|
||||
func (r MistakeRecordRepo) DeleteByUser(userID uint) bool {
|
||||
return r.Type(&model.MistakeRecord{}).Where("user_id = ?", userID).Delete(&model.MistakeRecord{})
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// Position 岗位仓库。
|
||||
type PositionRepo struct{ *QueryBuilder }
|
||||
|
||||
// List 获取岗位列表。
|
||||
func (r PositionRepo) List(status string) []model.Position {
|
||||
q := r.Type(&model.Position{})
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
} else {
|
||||
q = q.Where("status = ?", "active")
|
||||
}
|
||||
var items []model.Position
|
||||
if q.Order("id ASC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r PositionRepo) GetByID(id uint) (model.Position, bool) {
|
||||
var p model.Position
|
||||
if r.Type(&p).Where("id = ?", id).First(&p) {
|
||||
return p, true
|
||||
}
|
||||
return model.Position{}, false
|
||||
}
|
||||
|
||||
// GetByName 按名称获取。
|
||||
func (r PositionRepo) GetByName(name string) (model.Position, bool) {
|
||||
var p model.Position
|
||||
if r.Type(&p).Where("name = ?", name).First(&p) {
|
||||
return p, true
|
||||
}
|
||||
return model.Position{}, false
|
||||
}
|
||||
|
||||
// Insert 创建岗位。
|
||||
func (r PositionRepo) Insert(p *model.Position) bool {
|
||||
return r.QueryBuilder.Insert(p)
|
||||
}
|
||||
|
||||
// Update 更新岗位。
|
||||
func (r PositionRepo) Update(p *model.Position) bool {
|
||||
return r.Save(p)
|
||||
}
|
||||
|
||||
// Delete 软删除。
|
||||
func (r PositionRepo) Delete(id uint) bool {
|
||||
return r.Type(&model.Position{}).Where("id = ?", id).Updates(map[string]any{"status": "inactive"})
|
||||
}
|
||||
|
||||
// CountByDepartment 统计某部门的岗位数。
|
||||
func (r PositionRepo) CountByDepartment(departmentID uint) int64 {
|
||||
var c int64
|
||||
r.Inner().Model(&model.Position{}).Where("department_id = ?", departmentID).Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// CountByName 按名称统计(唯一性检查)。
|
||||
func (r PositionRepo) CountByName(name string, excludeID *uint) int64 {
|
||||
q := r.Inner().Model(&model.Position{}).Where("name = ?", name)
|
||||
if excludeID != nil {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
}
|
||||
var c int64
|
||||
q.Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// ============ 岗位知识映射(PositionKnowledge) ============
|
||||
|
||||
// Knowledge 取岗位知识映射(按 id ASC)。学员应学清单与管理员编辑页共用。
|
||||
func (r PositionRepo) Knowledge(positionID uint) []model.PositionKnowledge {
|
||||
var items []model.PositionKnowledge
|
||||
if r.Type(&items).Where("position_id = ?", positionID).Order("id ASC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplaceKnowledge 整表覆盖岗位知识映射(先删旧、再批量插入)。
|
||||
// 两步非原子:调用方若需强一致,应在事务中执行。
|
||||
func (r PositionRepo) ReplaceKnowledge(positionID uint, rows []model.PositionKnowledge) bool {
|
||||
if !r.Type(&model.PositionKnowledge{}).Where("position_id = ?", positionID).
|
||||
Delete(&model.PositionKnowledge{}) {
|
||||
return false
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return true
|
||||
}
|
||||
return r.Type(&rows).Create(&rows)
|
||||
}
|
||||
|
||||
// CountKnowledge 统计岗位知识映射条数。
|
||||
func (r PositionRepo) CountKnowledge(positionID uint) int64 {
|
||||
var c int64
|
||||
r.Inner().Model(&model.PositionKnowledge{}).Where("position_id = ?", positionID).Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// ============ 岗位考试蓝图(PositionExamBlueprint) ============
|
||||
|
||||
// Blueprints 取岗位考试蓝图(按 id ASC)。
|
||||
func (r PositionRepo) Blueprints(positionID uint) []model.PositionExamBlueprint {
|
||||
var items []model.PositionExamBlueprint
|
||||
if r.Type(&items).Where("position_id = ?", positionID).Order("id ASC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplaceBlueprints 整表覆盖岗位考试蓝图(先删旧、再批量插入)。
|
||||
func (r PositionRepo) ReplaceBlueprints(positionID uint, rows []model.PositionExamBlueprint) bool {
|
||||
if !r.Type(&model.PositionExamBlueprint{}).Where("position_id = ?", positionID).
|
||||
Delete(&model.PositionExamBlueprint{}) {
|
||||
return false
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return true
|
||||
}
|
||||
return r.Type(&rows).Create(&rows)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// Product 产品仓库。
|
||||
type ProductRepo struct{ *QueryBuilder }
|
||||
|
||||
// Query 返回基础查询构建器(供复合查询使用)。
|
||||
func (r ProductRepo) Query() *QueryBuilder {
|
||||
return r.Type(&model.Product{})
|
||||
}
|
||||
|
||||
// List 获取产品列表(按条件过滤)。
|
||||
func (r ProductRepo) List(conds ...map[string]any) []model.Product {
|
||||
q := r.QueryBuilder.Type(&model.Product{})
|
||||
for _, c := range conds {
|
||||
for k, v := range c {
|
||||
q = q.Where(k+" = ?", v)
|
||||
}
|
||||
}
|
||||
var items []model.Product
|
||||
if q.Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取产品。
|
||||
func (r ProductRepo) GetByID(id uint) (model.Product, bool) {
|
||||
var p model.Product
|
||||
if r.Type(&p).Where("id = ?", id).First(&p) {
|
||||
return p, true
|
||||
}
|
||||
return model.Product{}, false
|
||||
}
|
||||
|
||||
// GetVisibleByID 按 ID 获取「可对外露出」的产品。
|
||||
//
|
||||
// 与 GetByID 的区别是这里的业务口径:已停用(status=inactive)的产品不再出现在
|
||||
// 课程详情这类业务页面上。此前该判断散落在 handler 里手写,容易被漏掉或写歪。
|
||||
func (r ProductRepo) GetVisibleByID(id uint) (model.Product, bool) {
|
||||
var p model.Product
|
||||
if r.Type(&p).Where("id = ? AND status != ?", id, "inactive").First(&p) {
|
||||
return p, true
|
||||
}
|
||||
return model.Product{}, false
|
||||
}
|
||||
|
||||
// GetByCode 按编号获取产品。
|
||||
func (r ProductRepo) GetByCode(code string) (model.Product, bool) {
|
||||
var p model.Product
|
||||
if r.Type(&p).Where("code = ?", code).First(&p) {
|
||||
return p, true
|
||||
}
|
||||
return model.Product{}, false
|
||||
}
|
||||
|
||||
// Insert 创建产品。
|
||||
func (r ProductRepo) Insert(p *model.Product) bool {
|
||||
return r.QueryBuilder.Insert(p)
|
||||
}
|
||||
|
||||
// Update 更新产品。
|
||||
func (r ProductRepo) Update(p *model.Product) bool {
|
||||
return r.Save(p)
|
||||
}
|
||||
|
||||
// Delete 删除产品(软删除,status=inactive)。
|
||||
//
|
||||
// 这里必须写 "inactive" 而不是另造一个 "deleted":产品状态是与课程共用的
|
||||
// active/inactive 两档词汇,POST /products/{id} 的 DELETE 处理器也据此向调用方
|
||||
// 回包 {"status":"inactive"}。此前写成 "deleted" 会让接口回包与库内实际值不一致,
|
||||
// 管理员用 status=all 拉列表时会看到一个前端不认识的状态。
|
||||
func (r ProductRepo) Delete(id uint) bool {
|
||||
return r.Type(&model.Product{}).Where("id = ?", id).Updates(map[string]any{"status": "inactive"})
|
||||
}
|
||||
|
||||
// CountByCode 按编号统计(用于唯一性检查)。
|
||||
func (r ProductRepo) CountByCode(code string, excludeID *uint) int64 {
|
||||
q := r.Inner().Model(&model.Product{}).Where("code = ?", code)
|
||||
if excludeID != nil {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
}
|
||||
var c int64
|
||||
q.Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// NamesByIDs 批量解析产品名称(id → name),用于列表页回填关联名称。
|
||||
// 未命中的 ID 不会出现在返回的 map 中,调用方需自行兜底。
|
||||
func (r ProductRepo) NamesByIDs(ids []uint) map[uint]string {
|
||||
names := map[uint]string{}
|
||||
if len(ids) == 0 {
|
||||
return names
|
||||
}
|
||||
var items []model.Product
|
||||
if r.Type(&items).Where("id IN ?", ids).Find(&items) {
|
||||
for _, p := range items {
|
||||
names[p.ID] = p.Name
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// ProductsByStatus 按状态筛选产品列表。
|
||||
func (r ProductRepo) ProductsByStatus(status string, filter map[string]string) []model.Product {
|
||||
q := r.Type(&model.Product{})
|
||||
if cat, ok := filter["category"]; ok {
|
||||
q = q.Where("category = ?", cat)
|
||||
}
|
||||
switch status {
|
||||
case "": // 默认仅 active(员工浏览)
|
||||
q = q.Where("status = ?", "active")
|
||||
case "all": // 管理员维护全量
|
||||
default:
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
var items []model.Product
|
||||
if q.Order("id ASC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// Question 题目仓库。
|
||||
type QuestionRepo struct{ *QueryBuilder }
|
||||
|
||||
// List 获取题目列表(按条件过滤)。
|
||||
func (r QuestionRepo) List(domain, status string) []model.Question {
|
||||
q := r.Type(&model.Question{})
|
||||
if domain != "" {
|
||||
q = q.Where("domain = ?", domain)
|
||||
}
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
if status == "" {
|
||||
q = q.Where("status = ?", "active")
|
||||
}
|
||||
var items []model.Question
|
||||
if q.Order("id ASC").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取题目。
|
||||
func (r QuestionRepo) GetByID(id uint) (model.Question, bool) {
|
||||
var q model.Question
|
||||
if r.Type(&q).Where("id = ?", id).First(&q) {
|
||||
return q, true
|
||||
}
|
||||
return model.Question{}, false
|
||||
}
|
||||
|
||||
// GetActiveIDs 获取指定状态的题目 ID 列表。
|
||||
func (r QuestionRepo) GetActiveIDs(ids []uint) []uint {
|
||||
var result []uint
|
||||
r.Inner().Model(&model.Question{}).Where("id IN ? AND status = ?", ids, "active").Pluck("id", &result)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetByIDsWithDomain 获取题目列表(含知识域)。
|
||||
func (r QuestionRepo) GetByIDsWithDomain(ids []uint) []model.Question {
|
||||
var items []model.Question
|
||||
if r.Type(&items).Where("id IN ? AND status = ?", ids, "active").Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountByType 统计某类题目数量。
|
||||
func (r QuestionRepo) CountByType(domain, qType string) int64 {
|
||||
q := r.Inner().Model(&model.Question{})
|
||||
if domain != "" {
|
||||
q = q.Where("domain = ?", domain)
|
||||
}
|
||||
var c int64
|
||||
q.Where("type = ? AND status = ?", qType, "active").Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// Insert 创建题目。
|
||||
func (r QuestionRepo) Insert(item *model.Question) bool {
|
||||
return r.QueryBuilder.Insert(item)
|
||||
}
|
||||
|
||||
// Update 更新题目。
|
||||
func (r QuestionRepo) Update(item *model.Question) bool {
|
||||
return r.Save(item)
|
||||
}
|
||||
|
||||
// Delete 软删除。
|
||||
func (r QuestionRepo) Delete(id uint) bool {
|
||||
return r.Type(&model.Question{}).Where("id = ?", id).Updates(map[string]any{"status": "inactive"})
|
||||
}
|
||||
|
||||
// PickRandom 随机抽取 N 道题。
|
||||
func (r QuestionRepo) PickRandom(domain, qType string, count int, excludeIDs []uint) []model.Question {
|
||||
q := r.Type(&model.Question{}).Where("status = ? AND type = ?", "active", qType)
|
||||
if domain != "" {
|
||||
q = q.Where("domain = ?", domain)
|
||||
}
|
||||
if len(excludeIDs) > 0 {
|
||||
q = q.Where("id NOT IN ?", excludeIDs)
|
||||
}
|
||||
var items []model.Question
|
||||
if q.Order("RANDOM()").Limit(count).Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// SystemConfig 系统参数仓库。
|
||||
type SystemConfigRepo struct{ *QueryBuilder }
|
||||
|
||||
// GetByKey 按 key 获取参数值。
|
||||
func (r SystemConfigRepo) GetByKey(key string) string {
|
||||
var c model.SystemConfig
|
||||
if r.Type(&c).Where("config_key = ?", key).First(&c) {
|
||||
return c.ConfigValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get 按 key 获取完整记录。
|
||||
func (r SystemConfigRepo) Get(key string) (model.SystemConfig, bool) {
|
||||
var c model.SystemConfig
|
||||
if r.Type(&c).Where("config_key = ?", key).First(&c) {
|
||||
return c, true
|
||||
}
|
||||
return model.SystemConfig{}, false
|
||||
}
|
||||
|
||||
// SetOrUpdate 设置或更新参数。
|
||||
func (r SystemConfigRepo) SetOrUpdate(key, value string) bool {
|
||||
existing, found := r.Get(key)
|
||||
if !found {
|
||||
return r.Insert(&model.SystemConfig{ConfigKey: key, ConfigValue: value})
|
||||
}
|
||||
existing.ConfigValue = value
|
||||
return r.Save(&existing)
|
||||
}
|
||||
|
||||
// List 获取所有参数。
|
||||
func (r SystemConfigRepo) List() []model.SystemConfig {
|
||||
var items []model.SystemConfig
|
||||
if r.Type(&items).Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BulkUpsert 批量插入或更新。
|
||||
func (r SystemConfigRepo) BulkUpsert(items []model.SystemConfig) int {
|
||||
count := 0
|
||||
for _, item := range items {
|
||||
if r.SetOrUpdate(item.ConfigKey, item.ConfigValue) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
// User 用户仓库。
|
||||
type UserRepo struct{ *QueryBuilder }
|
||||
|
||||
// List 获取用户列表(分页)。
|
||||
func (r UserRepo) List(page, pageSize int, role, status string) []model.User {
|
||||
q := r.Type(&model.User{})
|
||||
if role != "" {
|
||||
q = q.Where("role = ?", role)
|
||||
}
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
var items []model.User
|
||||
if q.Order("id ASC").Limit(pageSize).Offset(offset).Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Total 统计用户总数。
|
||||
func (r UserRepo) Total(role, status string) int64 {
|
||||
q := r.Inner().Model(&model.User{})
|
||||
if role != "" {
|
||||
q = q.Where("role = ?", role)
|
||||
}
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
var c int64
|
||||
q.Count(&c)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetByID 按 ID 获取。
|
||||
func (r UserRepo) GetByID(id uint) (model.User, bool) {
|
||||
var u model.User
|
||||
if r.Type(&u).Where("id = ?", id).First(&u) {
|
||||
return u, true
|
||||
}
|
||||
return model.User{}, false
|
||||
}
|
||||
|
||||
// GetByUsername 按用户名获取。
|
||||
func (r UserRepo) GetByUsername(username string) (model.User, bool) {
|
||||
var u model.User
|
||||
if r.Type(&u).Where("username = ?", username).First(&u) {
|
||||
return u, true
|
||||
}
|
||||
return model.User{}, false
|
||||
}
|
||||
|
||||
// GetByEmail 按邮箱获取。
|
||||
func (r UserRepo) GetByEmail(email string) (model.User, bool) {
|
||||
var u model.User
|
||||
if r.Type(&u).Where("email = ?", email).First(&u) {
|
||||
return u, true
|
||||
}
|
||||
return model.User{}, false
|
||||
}
|
||||
|
||||
// Insert 创建用户。
|
||||
func (r UserRepo) Insert(u *model.User) bool {
|
||||
return r.QueryBuilder.Insert(u)
|
||||
}
|
||||
|
||||
// Update 更新用户。
|
||||
func (r UserRepo) Update(u *model.User) bool {
|
||||
return r.Save(u)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新状态。
|
||||
func (r UserRepo) UpdateStatus(id uint, status string) bool {
|
||||
return r.Type(&model.User{}).Where("id = ?", id).UpdateColumn("status", status)
|
||||
}
|
||||
Reference in New Issue
Block a user