yunzer_go/server/controllers/exam.go
2025-11-19 16:07:43 +08:00

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, "&nbsp;", " ")
str = strings.ReplaceAll(str, "&#160;", " ")
// 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()
}