541 lines
16 KiB
Go
541 lines
16 KiB
Go
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strconv"
|
|
"strings"
|
|
"regexp"
|
|
|
|
"server/models"
|
|
"server/services"
|
|
|
|
beego "github.com/beego/beego/v2/server/web"
|
|
)
|
|
|
|
type ExamQuestionController struct {
|
|
beego.Controller
|
|
}
|
|
|
|
// normalizeText removes HTML tags, converts to space, and collapses whitespace
|
|
func normalizeText(s string) string {
|
|
str := strings.TrimSpace(s)
|
|
if str == "" {
|
|
return ""
|
|
}
|
|
// remove HTML tags
|
|
re := regexp.MustCompile("<[^>]*>")
|
|
str = re.ReplaceAllString(str, "")
|
|
// decode common entities
|
|
str = strings.ReplaceAll(str, " ", " ")
|
|
str = strings.ReplaceAll(str, " ", " ")
|
|
// collapse whitespace
|
|
reSpace := regexp.MustCompile(`\s+`)
|
|
str = reSpace.ReplaceAllString(str, " ")
|
|
return strings.TrimSpace(str)
|
|
}
|
|
|
|
// computeMatchRate returns a simple similarity percentage between two strings based on
|
|
// position-wise identical characters over the max length, rounded to integer 0-100.
|
|
func computeMatchRate(a, b string) int {
|
|
s1 := normalizeText(a)
|
|
s2 := normalizeText(b)
|
|
if s1 == "" && s2 == "" {
|
|
return 100
|
|
}
|
|
maxLen := len([]rune(s1))
|
|
if l := len([]rune(s2)); l > maxLen {
|
|
maxLen = l
|
|
}
|
|
if maxLen == 0 {
|
|
return 0
|
|
}
|
|
r1 := []rune(s1)
|
|
r2 := []rune(s2)
|
|
same := 0
|
|
for i := 0; i < maxLen; i++ {
|
|
var c1, c2 rune
|
|
if i < len(r1) {
|
|
c1 = r1[i]
|
|
}
|
|
if i < len(r2) {
|
|
c2 = r2[i]
|
|
}
|
|
if c1 != 0 && c2 != 0 && c1 == c2 {
|
|
same++
|
|
}
|
|
}
|
|
// round to nearest int
|
|
rate := int(float64(same)/float64(maxLen)*100.0 + 0.5)
|
|
if rate < 0 {
|
|
return 0
|
|
}
|
|
if rate > 100 {
|
|
return 100
|
|
}
|
|
return rate
|
|
}
|
|
|
|
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")
|
|
bankId, _ := c.GetInt64("bank_id", 0)
|
|
minRate, _ := c.GetInt("min_rate", 0)
|
|
|
|
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)
|
|
|
|
// use normalized keyword for DB searching to improve hit rate when editor HTML is posted
|
|
normKeyword := normalizeText(keyword)
|
|
list, total, err := services.GetExamQuestions(services.QuestionListParams{
|
|
TenantId: tenantId,
|
|
Keyword: normKeyword,
|
|
QuestionType: qtype,
|
|
BankId: bankId,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
})
|
|
if err != nil {
|
|
c.Data["json"] = map[string]interface{}{"code": 1, "message": "获取试题列表失败: " + err.Error(), "data": nil}
|
|
c.ServeJSON()
|
|
return
|
|
}
|
|
// 当启用相似度筛选且按关键字无结果时,回退到不带关键字的最近记录中做相似度匹配
|
|
if minRate > 0 && strings.TrimSpace(normKeyword) != "" && len(list) == 0 {
|
|
fbPageSize := 50
|
|
fbList, _, fbErr := services.GetExamQuestions(services.QuestionListParams{
|
|
TenantId: tenantId,
|
|
Keyword: "",
|
|
BankId: bankId,
|
|
Page: 1,
|
|
PageSize: fbPageSize,
|
|
})
|
|
if fbErr == nil {
|
|
list = fbList
|
|
}
|
|
}
|
|
|
|
items := make([]map[string]interface{}, 0, len(list))
|
|
currentTitle := normalizeText(keyword)
|
|
for _, q := range list {
|
|
if q == nil {
|
|
continue
|
|
}
|
|
// 可选相似度计算与过滤
|
|
var rate *int
|
|
if currentTitle != "" {
|
|
v := computeMatchRate(currentTitle, q.QuestionTitle)
|
|
rate = &v
|
|
if minRate > 0 && v < minRate {
|
|
continue
|
|
}
|
|
}
|
|
item := map[string]interface{}{
|
|
"id": q.Id,
|
|
"tenant_id": q.TenantId,
|
|
"question_type": q.QuestionType,
|
|
"question_title": q.QuestionTitle,
|
|
"score": q.Score,
|
|
}
|
|
if rate != nil {
|
|
item["_match_rate"] = *rate
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
|
|
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"`
|
|
BankId int64 `json:"bank_id"`
|
|
}
|
|
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,
|
|
}
|
|
if payload.BankId > 0 {
|
|
q.BankId = payload.BankId
|
|
}
|
|
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()
|
|
}
|
|
|
|
// BatchCreate
|
|
// @router /exam-questions/batch [post]
|
|
func (c *ExamQuestionController) BatchCreate() {
|
|
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
|
var payload struct {
|
|
BankId int64 `json:"bank_id"`
|
|
Items []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"`
|
|
} `json:"items"`
|
|
}
|
|
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
|
|
}
|
|
if len(payload.Items) == 0 {
|
|
c.Data["json"] = map[string]interface{}{"code": 1, "message": "导入数据为空", "data": nil}
|
|
c.ServeJSON()
|
|
return
|
|
}
|
|
|
|
success := 0
|
|
fails := 0
|
|
createdIds := make([]int64, 0, len(payload.Items))
|
|
updatedIds := make([]int64, 0, len(payload.Items))
|
|
for _, item := range payload.Items {
|
|
q := &models.ExamQuestion{
|
|
TenantId: tenantId,
|
|
QuestionType: item.QuestionType,
|
|
QuestionTitle: item.QuestionTitle,
|
|
QuestionAnalysis: item.QuestionAnalysis,
|
|
Score: item.Score,
|
|
Status: 1,
|
|
}
|
|
if payload.BankId > 0 {
|
|
q.BankId = payload.BankId
|
|
}
|
|
var opts []models.ExamQuestionOption
|
|
for _, o := range item.Options {
|
|
opts = append(opts, models.ExamQuestionOption{OptionLabel: o["label"], OptionContent: o["content"]})
|
|
}
|
|
answerContent := ""
|
|
switch v := item.Answer.(type) {
|
|
case string:
|
|
answerContent = v
|
|
case []interface{}:
|
|
for i, it := range v {
|
|
if s, ok := it.(string); ok {
|
|
if i == 0 {
|
|
answerContent = s
|
|
} else {
|
|
answerContent += "," + s
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 如果存在完全匹配的题目标题,则覆盖更新;否则新增
|
|
if existing, err := services.FindExamQuestionByTitle(tenantId, item.QuestionTitle); err == nil && existing != nil {
|
|
if err := services.UpdateExamQuestion(tenantId, existing.Id, q, opts, answerContent); err != nil {
|
|
fails++
|
|
continue
|
|
}
|
|
updatedIds = append(updatedIds, existing.Id)
|
|
success++
|
|
} else {
|
|
id, err := services.CreateExamQuestion(tenantId, q, opts, answerContent)
|
|
if err != nil {
|
|
fails++
|
|
continue
|
|
}
|
|
createdIds = append(createdIds, id)
|
|
success++
|
|
}
|
|
}
|
|
|
|
c.Data["json"] = map[string]interface{}{
|
|
"code": 0,
|
|
"message": "批量导入完成",
|
|
"data": map[string]interface{}{
|
|
"success": success,
|
|
"failed": fails,
|
|
"created_ids": createdIds,
|
|
"updated_ids": updatedIds,
|
|
},
|
|
}
|
|
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"`
|
|
BankId int64 `json:"bank_id"`
|
|
}
|
|
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}
|
|
if payload.BankId > 0 {
|
|
q.BankId = payload.BankId
|
|
}
|
|
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()
|
|
}
|