diff --git a/pc/src/api/exam.js b/pc/src/api/exam.js new file mode 100644 index 0000000..119c92b --- /dev/null +++ b/pc/src/api/exam.js @@ -0,0 +1,140 @@ +import request from '@/utils/request' + +// 获取考试列表 +export function getExams(params) { + return request({ + url: '/api/exams', + method: 'get', + params + }) +} + +// 根据ID获取考试 +export function getExamById(id) { + return request({ + url: `/api/exams/${id}`, + method: 'get' + }) +} + +// 添加考试 +export function addExam(data) { + return request({ + url: '/api/exams', + method: 'post', + data: { + ...data, + is_global: data.is_global !== undefined ? data.is_global : 0 + } + }) +} + +// 更新考试 +export function updateExam(id, data) { + return request({ + url: `/api/exams/${id}`, + method: 'put', + data: { + ...data, + is_global: data.is_global !== undefined ? data.is_global : 0 + } + }) +} + +// 删除考试 +export function deleteExam(id) { + return request({ + url: `/api/exams/${id}`, + method: 'delete' + }) +} + + +/** 题库相关接口 */ +//题库列表 +export function getExamQuestionBanks(params) { + return request({ + url: '/api/exam-question-banks', + method: 'get', + params + }) +} + +//题库详情 +export function getExamQuestionBankDetail(id) { + return request({ + url: `/api/exam-question-banks/${id}`, + method: 'get' + }) +} + +//新增题库 +export function createExamQuestionBank(data) { + return request({ + url: '/api/exam-question-banks', + method: 'post', + data + }) +} + +//更新题库 +export function updateExamQuestionBank(id, data) { + return request({ + url: `/api/exam-question-banks/${id}`, + method: 'put', + data + }) +} + +//删除题库 +export function deleteExamQuestionBank(id) { + return request({ + url: `/api/exam-question-banks/${id}`, + method: 'delete' + }) +} + + +/** 题目相关接口 */ +// 题目列表 +export function getExamQuestions(params) { + return request({ + url: '/api/exam-questions', + method: 'get', + params + }) +} + +// 题目详情 +export function getExamQuestionDetail(id) { + return request({ + url: `/api/exam-questions/${id}`, + method: 'get' + }) +} + +// 新增题目 +export function createExamQuestion(data) { + return request({ + url: '/api/exam-questions', + method: 'post', + data + }) +} + +// 更新题目 +export function updateExamQuestion(id, data) { + return request({ + url: `/api/exam-questions/${id}`, + method: 'put', + data + }) +} + +// 删除题目 +export function deleteExamQuestion(id) { + return request({ + url: `/api/exam-questions/${id}`, + method: 'delete' + }) +} \ No newline at end of file diff --git a/pc/src/views/apps/exams/exam/index.vue b/pc/src/views/apps/exams/exam/index.vue index d36c1f5..efe5b75 100644 --- a/pc/src/views/apps/exams/exam/index.vue +++ b/pc/src/views/apps/exams/exam/index.vue @@ -1,40 +1,46 @@ + + + + + + + + + + + + + + + + + + + 查询 + 重置 + + + + 创建考试 - + - - - - - - - - - - - - - - - - 查询 - 重置 - - + @@ -63,14 +69,8 @@ - + @@ -154,8 +154,11 @@ onMounted(() => { .toolbar { margin-bottom: 20px; + padding: 15px 20px; + background: #fff; + border-radius: 4px; display: flex; - justify-content: flex-start; + justify-content: space-between; } .filter-bar { @@ -165,6 +168,29 @@ onMounted(() => { margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); border: 1px solid var(--el-border-color-lighter); + position: relative; + overflow: visible; + + .form-fields { + display: grid; + grid-template-columns: repeat(4, minmax(220px, 1fr)); + column-gap: 16px; + row-gap: 12px; + align-items: center; + } + + .col-span-2 { + grid-column: span 2; + } + + :deep(.el-divider) { + margin: 12px 0 16px; + } + + .form-actions { + display: flex; + gap: 12px; + } } .exam-list { diff --git a/pc/src/views/apps/exams/examinationQuestions/components/detail.vue b/pc/src/views/apps/exams/examinationQuestions/components/detail.vue new file mode 100644 index 0000000..57b2cae --- /dev/null +++ b/pc/src/views/apps/exams/examinationQuestions/components/detail.vue @@ -0,0 +1,92 @@ + + + + {{ q?.question_title || '-' }} + {{ displayType }} + {{ q?.score ?? '-' }} + + {{ (answerDisplay || []).join(', ') }} + {{ answerDisplay || '-' }} + + {{ q?.question_analysis || '-' }} + {{ formatTime(q?.create_time || q?.created_at) }} + {{ formatTime(q?.update_time || q?.updated_at) }} + + + + + + + + + + + 关闭 + + + + + + + diff --git a/pc/src/views/apps/exams/examinationQuestions/components/edit.vue b/pc/src/views/apps/exams/examinationQuestions/components/edit.vue new file mode 100644 index 0000000..28d8d0b --- /dev/null +++ b/pc/src/views/apps/exams/examinationQuestions/components/edit.vue @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + 选项 + + 新增选项 + + + + + {{ opt.label }} + + 删除 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 保存 + + + + + + + + diff --git a/pc/src/views/apps/exams/examinationQuestions/components/editBanks.vue b/pc/src/views/apps/exams/examinationQuestions/components/editBanks.vue new file mode 100644 index 0000000..5c21bca --- /dev/null +++ b/pc/src/views/apps/exams/examinationQuestions/components/editBanks.vue @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + 取消 + 保存 + + + + + + + + + diff --git a/pc/src/views/apps/exams/examinationQuestions/components/examList.vue b/pc/src/views/apps/exams/examinationQuestions/components/examList.vue new file mode 100644 index 0000000..5b0f824 --- /dev/null +++ b/pc/src/views/apps/exams/examinationQuestions/components/examList.vue @@ -0,0 +1,285 @@ + + + + + + 返回 + 题目列表 + + + + + + + + + + + + + + + + + 搜索 + 重置 + + + + + + + + + 新建题目 + + 刷新 + + + + + + + + + + {{ getTypeLabel(row.question_type) }} + + + + + {{ formatTime(row.create_time) }} + + + + 查看 + 编辑 + 删除 + + + + + + + + + + + + + + + + + + diff --git a/pc/src/views/apps/exams/examinationQuestions/index.vue b/pc/src/views/apps/exams/examinationQuestions/index.vue index e69de29..23dcd65 100644 --- a/pc/src/views/apps/exams/examinationQuestions/index.vue +++ b/pc/src/views/apps/exams/examinationQuestions/index.vue @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + 搜索 + 重置 + + + + + + + 新建题库 + + 批量导入 + 刷新 + + + + + + + + + + + + + 编辑 + 题目管理 + 删除 + + + + + + + + + + + + + + + + + + + + + diff --git a/pc/src/views/apps/exams/index.vue b/pc/src/views/apps/exams/index.vue index 32f0348..71e198d 100644 --- a/pc/src/views/apps/exams/index.vue +++ b/pc/src/views/apps/exams/index.vue @@ -4,4 +4,8 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/server/controllers/exam.go b/server/controllers/exam.go new file mode 100644 index 0000000..e0c3f8e --- /dev/null +++ b/server/controllers/exam.go @@ -0,0 +1,340 @@ +package controllers + +import ( + "encoding/json" + "strconv" + + "server/models" + "server/services" + + beego "github.com/beego/beego/v2/server/web" +) + +type ExamQuestionController struct { + beego.Controller +} + +type ExamQuestionBankController struct { + beego.Controller +} + +// Get list +// @router /exam-questions [get] +func (c *ExamQuestionController) GetList() { + tenantId, _ := c.Ctx.Input.GetData("tenantId").(int) + keyword := c.GetString("keyword") + typeStr := c.GetString("type") + var qtype *int8 + if typeStr != "" { + if iv, err := strconv.Atoi(typeStr); err == nil { + v := int8(iv) + qtype = &v + } + } + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 10) + list, total, err := services.GetExamQuestions(services.QuestionListParams{ + TenantId: tenantId, + Keyword: keyword, + QuestionType: qtype, + Page: page, + PageSize: pageSize, + }) + if err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "获取试题列表失败: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + // build minimal DTO to limit fields in response + items := make([]map[string]interface{}, 0, len(list)) + for _, q := range list { + if q == nil { + continue + } + items = append(items, map[string]interface{}{ + "id": q.Id, + "tenant_id": q.TenantId, + "question_type": q.QuestionType, + "question_title": q.QuestionTitle, + "score": q.Score, + }) + } + c.Data["json"] = map[string]interface{}{ + "code": 0, + "message": "ok", + "data": map[string]interface{}{ + "list": items, + "total": total, + }, + } + c.ServeJSON() +} + +// Get detail +// @router /exam-questions/:id [get] +func (c *ExamQuestionController) GetDetail() { + tenantId, _ := c.Ctx.Input.GetData("tenantId").(int) + id, err := c.GetInt64(":id") + if err != nil || id <= 0 { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil} + c.ServeJSON() + return + } + detail, err := services.GetExamQuestionDetail(tenantId, id) + if err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "试题不存在", "data": nil} + c.ServeJSON() + return + } + c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": detail} + c.ServeJSON() +} + +// Create +// @router /exam-questions [post] +func (c *ExamQuestionController) Create() { + tenantId, _ := c.Ctx.Input.GetData("tenantId").(int) + var payload struct { + QuestionTitle string `json:"question_title"` + QuestionType int8 `json:"question_type"` + Score float64 `json:"score"` + QuestionAnalysis string `json:"question_analysis"` + Options []map[string]string `json:"options"` + Answer interface{} `json:"answer"` + } + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数错误: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + q := &models.ExamQuestion{ + TenantId: tenantId, + QuestionType: payload.QuestionType, + QuestionTitle: payload.QuestionTitle, + QuestionAnalysis: payload.QuestionAnalysis, + Score: payload.Score, + Status: 1, + } + var opts []models.ExamQuestionOption + for _, o := range payload.Options { + opts = append(opts, models.ExamQuestionOption{OptionLabel: o["label"], OptionContent: o["content"]}) + } + answerContent := "" + switch v := payload.Answer.(type) { + case string: + answerContent = v + case []interface{}: + for i, item := range v { + if s, ok := item.(string); ok { + if i == 0 { + answerContent = s + } else { + answerContent += "," + s + } + } + } + } + id, err := services.CreateExamQuestion(tenantId, q, opts, answerContent) + if err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "创建失败: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + c.Data["json"] = map[string]interface{}{"code": 0, "message": "创建成功", "data": map[string]interface{}{"id": id}} + c.ServeJSON() +} + +// Update +// @router /exam-questions/:id [put] +func (c *ExamQuestionController) Update() { + tenantId, _ := c.Ctx.Input.GetData("tenantId").(int) + id, err := c.GetInt64(":id") + if err != nil || id <= 0 { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil} + c.ServeJSON() + return + } + var payload struct { + QuestionTitle string `json:"question_title"` + QuestionType int8 `json:"question_type"` + Score float64 `json:"score"` + QuestionAnalysis string `json:"question_analysis"` + Options []map[string]string `json:"options"` + Answer interface{} `json:"answer"` + } + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数错误: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + q := &models.ExamQuestion{QuestionType: payload.QuestionType, QuestionTitle: payload.QuestionTitle, QuestionAnalysis: payload.QuestionAnalysis, Score: payload.Score} + var opts []models.ExamQuestionOption + for _, o := range payload.Options { + opts = append(opts, models.ExamQuestionOption{OptionLabel: o["label"], OptionContent: o["content"]}) + } + answerContent := "" + switch v := payload.Answer.(type) { + case string: + answerContent = v + case []interface{}: + for i, item := range v { + if s, ok := item.(string); ok { + if i == 0 { + answerContent = s + } else { + answerContent += "," + s + } + } + } + } + if err := services.UpdateExamQuestion(tenantId, id, q, opts, answerContent); err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "更新失败: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + c.Data["json"] = map[string]interface{}{"code": 0, "message": "更新成功", "data": nil} + c.ServeJSON() +} + +// Delete +// @router /exam-questions/:id [delete] +func (c *ExamQuestionController) Delete() { + tenantId, _ := c.Ctx.Input.GetData("tenantId").(int) + id, err := c.GetInt64(":id") + if err != nil || id <= 0 { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil} + c.ServeJSON() + return + } + if err := services.DeleteExamQuestion(tenantId, id); err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "删除失败: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + c.Data["json"] = map[string]interface{}{"code": 0, "message": "删除成功", "data": nil} + c.ServeJSON() +} + +// GetBankList +// @router /exam-question-banks [get] +func (c *ExamQuestionBankController) GetBankList() { + tenantId, _ := c.Ctx.Input.GetData("tenantId").(int) + keyword := c.GetString("keyword") + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 10) + list, total, err := services.GetExamQuestionBanks(services.BankListParams{ + TenantId: tenantId, + Keyword: keyword, + Page: page, + PageSize: pageSize, + }) + if err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "获取试题库列表失败: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": map[string]interface{}{"list": list, "total": total}} + c.ServeJSON() +} + +// GetBankDetail +// @router /exam-question-banks/:id [get] +func (c *ExamQuestionBankController) GetBankDetail() { + tenantId, _ := c.Ctx.Input.GetData("tenantId").(int) + id, err := c.GetInt64(":id") + if err != nil || id <= 0 { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil} + c.ServeJSON() + return + } + bank, err := services.GetExamQuestionBankDetail(tenantId, id) + if err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "试题库不存在", "data": nil} + c.ServeJSON() + return + } + c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": bank} + c.ServeJSON() +} + +// CreateBank +// @router /exam-question-banks [post] +func (c *ExamQuestionBankController) CreateBank() { + tenantId, _ := c.Ctx.Input.GetData("tenantId").(int) + var payload struct { + BankName string `json:"bank_name"` + BankDesc string `json:"bank_desc"` + } + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数错误: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + bank := &models.ExamQuestionBank{ + TenantId: tenantId, + BankName: payload.BankName, + BankDesc: payload.BankDesc, + Status: 1, + } + id, err := services.CreateExamQuestionBank(tenantId, bank) + if err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "创建失败: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + c.Data["json"] = map[string]interface{}{"code": 0, "message": "创建成功", "data": map[string]interface{}{"id": id}} + c.ServeJSON() +} + +// UpdateBank +// @router /exam-question-banks/:id [put] +func (c *ExamQuestionBankController) UpdateBank() { + tenantId, _ := c.Ctx.Input.GetData("tenantId").(int) + id, err := c.GetInt64(":id") + if err != nil || id <= 0 { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil} + c.ServeJSON() + return + } + var payload struct { + BankName string `json:"bank_name"` + BankDesc string `json:"bank_desc"` + } + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数错误: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + bank := &models.ExamQuestionBank{ + Id: id, + TenantId: tenantId, + BankName: payload.BankName, + BankDesc: payload.BankDesc, + } + if err := services.UpdateExamQuestionBank(tenantId, id, bank); err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "更新失败: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + c.Data["json"] = map[string]interface{}{"code": 0, "message": "更新成功", "data": nil} + c.ServeJSON() +} + +// DeleteBank +// @router /exam-question-banks/:id [delete] +func (c *ExamQuestionBankController) DeleteBank() { + tenantId, _ := c.Ctx.Input.GetData("tenantId").(int) + id, err := c.GetInt64(":id") + if err != nil || id <= 0 { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "ID无效", "data": nil} + c.ServeJSON() + return + } + if err := services.DeleteExamQuestionBank(tenantId, id); err != nil { + c.Data["json"] = map[string]interface{}{"code": 1, "message": "删除失败: " + err.Error(), "data": nil} + c.ServeJSON() + return + } + c.Data["json"] = map[string]interface{}{"code": 0, "message": "删除成功", "data": nil} + c.ServeJSON() +} diff --git a/server/database/init_mysql.sql b/server/database/init_mysql.sql index 1f64d1f..671418a 100644 --- a/server/database/init_mysql.sql +++ b/server/database/init_mysql.sql @@ -1,4 +1,4 @@ --- MySQL dump 10.13 Distrib 8.0.41, for Win64 (x86_64) +-- MySQL dump 10.13 Distrib 8.0.41, for Win64 (x86_64) -- -- Host: 212.64.112.158 Database: gotest -- ------------------------------------------------------ @@ -228,6 +228,30 @@ CREATE TABLE `yz_exam_category` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试分类表(含多租户隔离)'; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `yz_exam_question_bank` +-- + +DROP TABLE IF EXISTS `yz_exam_question_bank`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `yz_exam_question_bank` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '题库ID', + `tenant_id` int(11) NOT NULL COMMENT '租户ID(多租户隔离)', + `bank_name` varchar(100) NOT NULL COMMENT '题库名称', + `bank_desc` varchar(500) DEFAULT '' COMMENT '题库描述', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态(1-启用,0-禁用)', + `sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序序号', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)', + PRIMARY KEY (`id`), + KEY `idx_tenant_status` (`tenant_id`,`status`), + KEY `idx_tenant_name` (`tenant_id`,`bank_name`), + KEY `idx_delete_time` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试题库表(支持租户隔离、软删除)'; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `yz_exam_users` -- diff --git a/server/models/exam.go b/server/models/exam.go new file mode 100644 index 0000000..1208d6a --- /dev/null +++ b/server/models/exam.go @@ -0,0 +1,79 @@ +package models + +import ( + "time" + + "github.com/beego/beego/v2/client/orm" +) + +// ExamQuestion 对应表 yz_exam_question +type ExamQuestion struct { + Id int64 `orm:"column(id);auto" json:"id"` + TenantId int `orm:"column(tenant_id)" json:"tenant_id"` + QuestionType int8 `orm:"column(question_type)" json:"question_type"` + QuestionTitle string `orm:"column(question_title);size(1000)" json:"question_title"` + QuestionAnalysis string `orm:"column(question_analysis);size(2000);null" json:"question_analysis"` + Score float64 `orm:"column(score);digits(5);decimals(2)" json:"score"` + SortOrder int8 `orm:"column(sort_order);null" json:"sort_order"` + Status int8 `orm:"column(status)" json:"status"` + CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time,omitempty"` +} + +func (q *ExamQuestion) TableName() string { + return "yz_exam_question" +} + +// ExamQuestionOption 对应表 yz_exam_question_option +type ExamQuestionOption struct { + Id int64 `orm:"column(id);auto" json:"id"` + TenantId int `orm:"column(tenant_id)" json:"tenant_id"` + QuestionId int64 `orm:"column(question_id)" json:"question_id"` + OptionLabel string `orm:"column(option_label);size(10)" json:"option_label"` + OptionContent string `orm:"column(option_content);size(500)" json:"option_content"` + SortOrder int8 `orm:"column(sort_order);null" json:"sort_order"` + CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time,omitempty"` +} + +func (o *ExamQuestionOption) TableName() string { + return "yz_exam_question_option" +} + +// ExamQuestionAnswer 对应表 yz_exam_question_answer +type ExamQuestionAnswer struct { + Id int64 `orm:"column(id);auto" json:"id"` + TenantId int `orm:"column(tenant_id)" json:"tenant_id"` + QuestionId int64 `orm:"column(question_id)" json:"question_id"` + AnswerContent string `orm:"column(answer_content);size(1000)" json:"answer_content"` + CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time,omitempty"` +} + +func (a *ExamQuestionAnswer) TableName() string { + return "yz_exam_question_answer" +} + +// ExamQuestionBank 对应表 yz_exam_question_bank +type ExamQuestionBank struct { + Id int64 `orm:"column(id);auto" json:"id"` + TenantId int `orm:"column(tenant_id)" json:"tenant_id"` + BankName string `orm:"column(bank_name);size(100)" json:"bank_name"` + BankDesc string `orm:"column(bank_desc);size(500);null" json:"bank_desc"` + Status int8 `orm:"column(status)" json:"status"` + SortOrder int8 `orm:"column(sort_order);null" json:"sort_order"` + CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time,omitempty"` +} + +func (b *ExamQuestionBank) TableName() string { + return "yz_exam_question_bank" +} + +func init() { + orm.RegisterModel(new(ExamQuestion), new(ExamQuestionOption), new(ExamQuestionAnswer), new(ExamQuestionBank)) +} diff --git a/server/routers/router.go b/server/routers/router.go index eef2d5d..0cb3c27 100644 --- a/server/routers/router.go +++ b/server/routers/router.go @@ -283,12 +283,20 @@ func init() { beego.Router("/api/files/search", &controllers.FileController{}, "get:SearchFiles") beego.Router("/api/files/statistics", &controllers.FileController{}, "get:GetFileStatistics") + // 考试题目路由 + beego.Router("/api/exam-questions", &controllers.ExamQuestionController{}, "get:GetList;post:Create") + beego.Router("/api/exam-questions/:id", &controllers.ExamQuestionController{}, "get:GetDetail;put:Update;delete:Delete") + + // 考试题库路由 + beego.Router("/api/exam-question-banks", &controllers.ExamQuestionBankController{}, "get:GetBankList;post:CreateBank") + beego.Router("/api/exam-question-banks/:id", &controllers.ExamQuestionBankController{}, "get:GetBankDetail;put:UpdateBank;delete:DeleteBank") + // 知识库路由 beego.Router("/api/knowledge/list", &controllers.KnowledgeController{}, "get:List") beego.Router("/api/knowledge/count", &controllers.KnowledgeController{}, "get:GetCount") beego.Router("/api/knowledge/detail", &controllers.KnowledgeController{}, "get:Detail") beego.Router("/api/knowledge/create", &controllers.KnowledgeController{}, "post:Create") - beego.Router("/api/knowledge/update", &controllers.KnowledgeController{}, "post:Update") + // ... beego.Router("/api/knowledge/delete", &controllers.KnowledgeController{}, "post:Delete") beego.Router("/api/knowledge/categories", &controllers.KnowledgeController{}, "get:GetCategories") beego.Router("/api/knowledge/tags", &controllers.KnowledgeController{}, "get:GetTags") @@ -314,10 +322,10 @@ func init() { // OA基础数据合并接口(一次性获取部门、职位、角色) beego.Router("/api/oa/base-data/:tenantId", &controllers.OAController{}, "get:GetOABaseData") - // OA任务管理路由 - beego.Router("/api/oa/tasks", &controllers.TaskController{}, "get:GetTasks;post:CreateTask") - beego.Router("/api/oa/tasks/:id", &controllers.TaskController{}, "get:GetTaskById;put:UpdateTask;delete:DeleteTask") - beego.Router("/api/oa/tasks/todo", &controllers.TaskController{}, "get:GetTodoTasks") + // OA任务管理路由 + beego.Router("/api/oa/tasks", &controllers.TaskController{}, "get:GetTasks;post:CreateTask") + beego.Router("/api/oa/tasks/:id", &controllers.TaskController{}, "get:GetTaskById;put:UpdateTask;delete:DeleteTask") + beego.Router("/api/oa/tasks/todo", &controllers.TaskController{}, "get:GetTodoTasks") // CRM 客户路由 beego.Router("/api/crm/customer/list", &controllers.CustomerController{}, "get:List") diff --git a/server/services/exam.go b/server/services/exam.go new file mode 100644 index 0000000..a2b1d4b --- /dev/null +++ b/server/services/exam.go @@ -0,0 +1,224 @@ +package services + +import ( + "errors" + "time" + + "server/models" + + "github.com/beego/beego/v2/client/orm" +) + +type QuestionListParams struct { + TenantId int + Keyword string + QuestionType *int8 + Page int + PageSize int +} + +type QuestionWithMeta struct { + Question *models.ExamQuestion + Options []*models.ExamQuestionOption + Answer *models.ExamQuestionAnswer +} + +type BankListParams struct { + TenantId int + Keyword string + Page int + PageSize int +} + +func GetExamQuestions(params QuestionListParams) ([]*models.ExamQuestion, int64, error) { + o := orm.NewOrm() + qs := o.QueryTable(new(models.ExamQuestion)).Filter("delete_time__isnull", true).Filter("status", 1) + if params.TenantId > 0 { + qs = qs.Filter("tenant_id", params.TenantId) + } + if params.Keyword != "" { + qs = qs.Filter("question_title__icontains", params.Keyword) + } + if params.QuestionType != nil { + qs = qs.Filter("question_type", *params.QuestionType) + } + total, err := qs.Count() + if err != nil { + return nil, 0, err + } + if params.Page <= 0 { + params.Page = 1 + } + if params.PageSize <= 0 { + params.PageSize = 10 + } + offset := (params.Page - 1) * params.PageSize + var list []*models.ExamQuestion + _, err = qs.OrderBy("-id").Limit(params.PageSize, offset).All(&list, "Id", "TenantId", "QuestionType", "QuestionTitle", "Score") + return list, total, err +} + +func GetExamQuestionDetail(tenantId int, id int64) (*QuestionWithMeta, error) { + o := orm.NewOrm() + q := new(models.ExamQuestion) + if err := o.QueryTable(q).Filter("id", id).Filter("delete_time__isnull", true).One(q); err != nil { + return nil, err + } + if tenantId > 0 && q.TenantId != tenantId { + return nil, errors.New("not found") + } + var opts []*models.ExamQuestionOption + _, _ = o.QueryTable(new(models.ExamQuestionOption)).Filter("question_id", q.Id).Filter("delete_time__isnull", true).All(&opts) + a := new(models.ExamQuestionAnswer) + _ = o.QueryTable(a).Filter("question_id", q.Id).Filter("delete_time__isnull", true).One(a) + return &QuestionWithMeta{Question: q, Options: opts, Answer: a}, nil +} + +func CreateExamQuestion(tenantId int, q *models.ExamQuestion, options []models.ExamQuestionOption, answerContent string) (int64, error) { + o := orm.NewOrm() + if tenantId > 0 { + q.TenantId = tenantId + } + q.Status = 1 + if _, err := o.Insert(q); err != nil { + return 0, err + } + for i := range options { + options[i].TenantId = q.TenantId + options[i].QuestionId = q.Id + _, _ = o.Insert(&options[i]) + } + ans := &models.ExamQuestionAnswer{TenantId: q.TenantId, QuestionId: q.Id, AnswerContent: answerContent} + _, _ = o.Insert(ans) + return q.Id, nil +} + +func UpdateExamQuestion(tenantId int, id int64, q *models.ExamQuestion, options []models.ExamQuestionOption, answerContent string) error { + o := orm.NewOrm() + existing := models.ExamQuestion{Id: id} + if err := o.Read(&existing); err != nil { + return err + } + if tenantId > 0 && existing.TenantId != tenantId { + return errors.New("not found") + } + existing.QuestionType = q.QuestionType + existing.QuestionTitle = q.QuestionTitle + existing.QuestionAnalysis = q.QuestionAnalysis + existing.Score = q.Score + if _, err := o.Update(&existing, "QuestionType", "QuestionTitle", "QuestionAnalysis", "Score"); err != nil { + return err + } + // soft delete old options and answer + now := time.Now() + o.QueryTable(new(models.ExamQuestionOption)).Filter("question_id", id).Filter("delete_time__isnull", true).Update(orm.Params{"delete_time": now}) + o.QueryTable(new(models.ExamQuestionAnswer)).Filter("question_id", id).Filter("delete_time__isnull", true).Update(orm.Params{"delete_time": now}) + for i := range options { + options[i].TenantId = existing.TenantId + options[i].QuestionId = id + _, _ = o.Insert(&options[i]) + } + ans := &models.ExamQuestionAnswer{TenantId: existing.TenantId, QuestionId: id, AnswerContent: answerContent} + _, _ = o.Insert(ans) + return nil +} + +func DeleteExamQuestion(tenantId int, id int64) error { + o := orm.NewOrm() + existing := models.ExamQuestion{Id: id} + if err := o.Read(&existing); err != nil { + return err + } + if tenantId > 0 && existing.TenantId != tenantId { + return errors.New("not found") + } + now := time.Now() + if _, err := o.QueryTable(new(models.ExamQuestion)).Filter("id", id).Update(orm.Params{"delete_time": now}); err != nil { + return err + } + o.QueryTable(new(models.ExamQuestionOption)).Filter("question_id", id).Filter("delete_time__isnull", true).Update(orm.Params{"delete_time": now}) + o.QueryTable(new(models.ExamQuestionAnswer)).Filter("question_id", id).Filter("delete_time__isnull", true).Update(orm.Params{"delete_time": now}) + return nil +} + +func GetExamQuestionBanks(params BankListParams) ([]*models.ExamQuestionBank, int64, error) { + o := orm.NewOrm() + qs := o.QueryTable(new(models.ExamQuestionBank)).Filter("delete_time__isnull", true).Filter("status", 1) + if params.TenantId > 0 { + qs = qs.Filter("tenant_id", params.TenantId) + } + if params.Keyword != "" { + qs = qs.Filter("bank_name__icontains", params.Keyword) + } + total, err := qs.Count() + if err != nil { + return nil, 0, err + } + if params.Page <= 0 { + params.Page = 1 + } + if params.PageSize <= 0 { + params.PageSize = 10 + } + offset := (params.Page - 1) * params.PageSize + var list []*models.ExamQuestionBank + _, err = qs.OrderBy("-id").Limit(params.PageSize, offset).All(&list, "Id", "TenantId", "BankName", "BankDesc") + return list, total, err +} + +func GetExamQuestionBankDetail(tenantId int, id int64) (*models.ExamQuestionBank, error) { + o := orm.NewOrm() + bank := new(models.ExamQuestionBank) + if err := o.QueryTable(bank).Filter("id", id).Filter("delete_time__isnull", true).One(bank); err != nil { + return nil, err + } + if tenantId > 0 && bank.TenantId != tenantId { + return nil, errors.New("not found") + } + return bank, nil +} + +func CreateExamQuestionBank(tenantId int, bank *models.ExamQuestionBank) (int64, error) { + o := orm.NewOrm() + if tenantId > 0 { + bank.TenantId = tenantId + } + bank.Status = 1 + if _, err := o.Insert(bank); err != nil { + return 0, err + } + return bank.Id, nil +} + +func UpdateExamQuestionBank(tenantId int, id int64, bank *models.ExamQuestionBank) error { + o := orm.NewOrm() + existing := models.ExamQuestionBank{Id: id} + if err := o.Read(&existing); err != nil { + return err + } + if tenantId > 0 && existing.TenantId != tenantId { + return errors.New("not found") + } + existing.BankName = bank.BankName + existing.BankDesc = bank.BankDesc + if _, err := o.Update(&existing, "BankName", "BankDesc"); err != nil { + return err + } + return nil +} + +func DeleteExamQuestionBank(tenantId int, id int64) error { + o := orm.NewOrm() + existing := models.ExamQuestionBank{Id: id} + if err := o.Read(&existing); err != nil { + return err + } + if tenantId > 0 && existing.TenantId != tenantId { + return errors.New("not found") + } + now := time.Now() + if _, err := o.QueryTable(new(models.ExamQuestionBank)).Filter("id", id).Update(orm.Params{"delete_time": now}); err != nil { + return err + } + return nil +}