优化题目代码
This commit is contained in:
parent
5fddba8a30
commit
7664821d88
3
.gitignore
vendored
3
.gitignore
vendored
@ -25,6 +25,9 @@ dist-ssr
|
||||
*.local
|
||||
server/uploads
|
||||
server/conf/
|
||||
pc/dist.zip
|
||||
pc/dist.7z
|
||||
pc/dist
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@ -6,18 +6,13 @@
|
||||
<el-step title="选项与答案" />
|
||||
</el-steps>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-divider />
|
||||
<!-- 第一步:题型、分值、题目 -->
|
||||
<div v-if="currentStep === 1" class="form-flex">
|
||||
<!-- 第一步表单:基础信息 -->
|
||||
<el-form v-if="currentStep === 1" ref="formRefStep1" :model="form" :rules="rulesStep1" label-width="100px">
|
||||
<div class="form-flex">
|
||||
<el-form-item label="题型" prop="question_type">
|
||||
<el-select v-model="form.question_type" placeholder="请选择题型" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in typeOptions"
|
||||
:key="opt.dict_value"
|
||||
:label="opt.dict_label"
|
||||
:value="String(opt.dict_value)"
|
||||
/>
|
||||
<el-option v-for="opt in typeOptions" :key="opt.dict_value" :label="opt.dict_label"
|
||||
:value="String(opt.dict_value)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分值" prop="score">
|
||||
@ -27,49 +22,49 @@
|
||||
<WangEditor v-model="form.question_title" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<!-- 第二步:选项、答案、解析 -->
|
||||
<template v-else>
|
||||
<template v-if="showOptions">
|
||||
<el-divider />
|
||||
<div class="options-header">
|
||||
<span>选项</span>
|
||||
<div class="op">
|
||||
<el-button v-if="canCustomizeOptions" size="small" @click="addOption">新增选项</el-button>
|
||||
</div>
|
||||
<!-- 第二步表单:选项、答案、解析 -->
|
||||
<el-form v-else ref="formRefStep2" :model="form" :rules="rulesStep2" label-width="100px" style="margin-top: 20px">
|
||||
<template v-if="showOptions">
|
||||
<div class="options-header">
|
||||
<!-- <span>选项</span> -->
|
||||
<div class="op">
|
||||
<el-button v-if="canCustomizeOptions" type="primary" @click="addOption">新增选项</el-button>
|
||||
</div>
|
||||
<div class="option-list">
|
||||
<div v-for="(opt, idx) in form.options" :key="opt.key" class="option-item">
|
||||
<div class="label">{{ opt.label }}</div>
|
||||
<el-input v-model="opt.content" placeholder="请输入选项内容" />
|
||||
<el-button v-if="canCustomizeOptions" link type="danger" @click="removeOption(idx)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-list">
|
||||
<div v-for="(opt, idx) in form.options" :key="opt.key" class="option-item">
|
||||
<div class="label">{{ opt.label }}</div>
|
||||
<el-input v-model="opt.content" placeholder="请输入选项内容" />
|
||||
<el-button v-if="canCustomizeOptions" link type="danger" @click="removeOption(idx)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="showAnswer">
|
||||
<el-divider />
|
||||
<el-form-item label="正确答案" prop="answer">
|
||||
<template v-if="isSingleType">
|
||||
<el-select v-model="form.answer" placeholder="请选择">
|
||||
<el-option v-for="opt in form.options" :key="opt.key" :label="opt.label" :value="opt.label" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else-if="isMultipleType">
|
||||
<el-select v-model="multiAnswer" multiple placeholder="请选择">
|
||||
<el-option v-for="opt in form.options" :key="opt.key" :label="opt.label" :value="opt.label" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-input v-model="form.answer" type="textarea" :rows="2" placeholder="请输入答案" />
|
||||
</template>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item label="解析">
|
||||
<WangEditor v-model="form.question_analysis" />
|
||||
<template v-if="showAnswer">
|
||||
<el-divider />
|
||||
<el-form-item label="正确答案" prop="answer">
|
||||
<template v-if="isSingleType">
|
||||
<el-radio-group v-model="form.answer">
|
||||
<el-radio-button v-for="opt in form.options" :key="opt.key" :label="opt.label">{{ opt.label }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
<template v-else-if="isMultipleType">
|
||||
<el-checkbox-group v-model="multiAnswer">
|
||||
<el-checkbox-button v-for="opt in form.options" :key="opt.key" :label="opt.label">{{ opt.label }}</el-checkbox-button>
|
||||
</el-checkbox-group>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-input v-model="form.answer" type="textarea" :rows="2" placeholder="请输入答案" />
|
||||
</template>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-divider />
|
||||
<el-form-item label="解析">
|
||||
<WangEditor v-model="form.question_analysis" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
@ -79,13 +74,14 @@
|
||||
<template v-else>
|
||||
<el-button @click="handlePrev">上一步</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSubmit">保存</el-button>
|
||||
<el-button type="primary" plain :loading="saving" @click="() => handleSubmit(true)">保存并继续</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 相似题目提示弹窗:仅在新增时发现匹配题目时显示 -->
|
||||
<el-dialog v-model="duplicateDialogVisible" title="发现相似题目" width="640px" append-to-body>
|
||||
<el-dialog v-model="duplicateDialogVisible" title="发现相似题目" width="800px;" append-to-body>
|
||||
<div class="duplicate-tip">当前题目:{{ form.question_title }}</div>
|
||||
<el-table :data="duplicateList" border style="margin-top: 12px">
|
||||
<el-table-column label="匹配题目">
|
||||
@ -98,17 +94,17 @@
|
||||
{{ row._match_rate != null ? row._match_rate + '%' : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<el-table-column label="操作" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleUseExisting(row)">使用该题目</el-button>
|
||||
<el-button type="primary" link @click="handleUseExisting(row)">拉取题目</el-button>
|
||||
<el-button type="primary" link @click="handleContinueCreate">继续新增</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<div class="dlg-actions">
|
||||
<el-button @click="duplicateDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleContinueCreate">继续新增</el-button>
|
||||
<el-button @click="duplicateDialogVisible = false">返回修改</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -118,7 +114,7 @@
|
||||
import { ref, reactive, computed, watch, nextTick, onBeforeUnmount } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getDictItemsByCode } from '@/api/dict'
|
||||
import { getExamQuestions } from '@/api/exam'
|
||||
import { getExamQuestions, getExamQuestionDetail, createExamQuestion } from '@/api/exam'
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
|
||||
const props = defineProps({
|
||||
@ -130,7 +126,8 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['close', 'saved'])
|
||||
|
||||
const formRef = ref()
|
||||
const formRefStep1 = ref()
|
||||
const formRefStep2 = ref()
|
||||
const saving = ref(false)
|
||||
const currentStep = ref(1) // 1: 基础信息, 2: 选项与答案
|
||||
const typeOptions = ref([])
|
||||
@ -157,18 +154,31 @@ const defaultOptions = () => ([
|
||||
|
||||
const form = reactive({
|
||||
question_title: '',
|
||||
question_type: '',
|
||||
question_type: '1',
|
||||
score: 2,
|
||||
question_analysis: '',
|
||||
options: defaultOptions(),
|
||||
answer: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
question_title: [{ required: true, message: '请输入题目', trigger: 'blur' }],
|
||||
const rulesStep1 = {
|
||||
question_type: [{ required: true, message: '请选择题型', trigger: 'change' }],
|
||||
score: [{ required: true, message: '请输入分值', trigger: 'change' }],
|
||||
answer: [{ validator: validateAnswer, trigger: 'change' }]
|
||||
question_title: [{ required: true, message: '请输入题目', trigger: 'blur' }],
|
||||
}
|
||||
const rulesStep2 = {
|
||||
answer: [{ validator: validateAnswer, trigger: 'change' }],
|
||||
}
|
||||
|
||||
function isRichTextEmpty(html) {
|
||||
const s = String(html || '')
|
||||
const text = s
|
||||
.replace(/<p><br\/?><\/p>/gi, '')
|
||||
.replace(/<br\s*\/?>(\s| | )*?/gi, '')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ | /g, ' ')
|
||||
.trim()
|
||||
return text.length === 0
|
||||
}
|
||||
|
||||
function handleTitleEditorCreated(editor) {
|
||||
@ -256,7 +266,9 @@ function initForm() {
|
||||
}
|
||||
} else {
|
||||
form.question_title = ''
|
||||
form.question_type = ''
|
||||
// 默认从字典中选择“单选题”(dict_value==='1'),若无则取第一项;再退回 '1'
|
||||
const preferred = typeOptions.value.find(o => String(o.dict_value) === '1') || typeOptions.value[0]
|
||||
form.question_type = String(preferred?.dict_value ?? '1')
|
||||
form.score = 2
|
||||
form.question_analysis = ''
|
||||
form.options = defaultOptions()
|
||||
@ -279,7 +291,7 @@ async function handleNext() {
|
||||
ElMessage.error('请输入分值')
|
||||
return
|
||||
}
|
||||
if (!form.question_title) {
|
||||
if (isRichTextEmpty(form.question_title)) {
|
||||
ElMessage.error('请输入题目')
|
||||
return
|
||||
}
|
||||
@ -289,8 +301,9 @@ async function handleNext() {
|
||||
try {
|
||||
const res = await getExamQuestions({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
pageSize: 10,
|
||||
keyword: form.question_title,
|
||||
min_rate: 50,
|
||||
})
|
||||
let items = []
|
||||
if (res && res.code === 0 && res.data && Array.isArray(res.data.list)) {
|
||||
@ -299,30 +312,18 @@ async function handleNext() {
|
||||
items = res.data
|
||||
}
|
||||
if (items.length > 0) {
|
||||
// 前端根据题干与当前输入的相似度计算匹配度,存到 _match_rate 字段
|
||||
const currentTitle = String(form.question_title || '')
|
||||
duplicateList.value = items.map((it) => {
|
||||
const title = String(it.question_title || '')
|
||||
let rate = null
|
||||
if (currentTitle && title) {
|
||||
const maxLen = Math.max(currentTitle.length, title.length)
|
||||
let same = 0
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
const c1 = currentTitle[i]
|
||||
const c2 = title[i]
|
||||
if (c1 && c2 && c1 === c2) same++
|
||||
}
|
||||
rate = maxLen > 0 ? Math.round((same / maxLen) * 100) : null
|
||||
}
|
||||
return { ...it, _match_rate: rate }
|
||||
})
|
||||
// 使用后端返回的匹配结果(包含 _match_rate)
|
||||
// 可选:按匹配度排序
|
||||
const sorted = items.slice().sort((a, b) => (Number(b._match_rate || 0) - Number(a._match_rate || 0)))
|
||||
duplicateList.value = sorted
|
||||
duplicateDialogVisible.value = true
|
||||
// 让用户在列表中选择是否使用已存在题目或继续新增,不直接进入第二步
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
// 查询失败不阻塞继续操作,仅在控制台记录
|
||||
console.error('检查重复题目失败', e)
|
||||
ElMessage.error('检查相似题目失败,请稍后重试')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -368,8 +369,8 @@ function handleClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
formRef.value.validate(async (ok) => {
|
||||
function handleSubmit(continueNext) {
|
||||
formRefStep2.value.validate(async (ok) => {
|
||||
if (!ok) return
|
||||
saving.value = true
|
||||
try {
|
||||
@ -382,7 +383,13 @@ function handleSubmit() {
|
||||
answer: isMultipleType.value ? [...multiAnswer.value] : form.answer
|
||||
}
|
||||
if (props.bankId) payload.bank_id = props.bankId
|
||||
emit('saved', payload)
|
||||
emit('saved', payload, { continue: !!continueNext })
|
||||
if (continueNext) {
|
||||
initForm()
|
||||
currentStep.value = 1
|
||||
ElMessage.success('已保存,继续新增')
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
@ -391,10 +398,50 @@ function handleSubmit() {
|
||||
})
|
||||
}
|
||||
|
||||
function handleUseExisting(row) {
|
||||
// 使用已存在题目:先关闭相似题目弹窗,再关闭当前新增弹窗
|
||||
duplicateDialogVisible.value = false
|
||||
emit('close')
|
||||
async function handleUseExisting(row) {
|
||||
try {
|
||||
// 拉取题目详情
|
||||
const res = await getExamQuestionDetail(row.id)
|
||||
let data = null
|
||||
if (res && res.code === 0 && res.data) data = res.data
|
||||
else if (res && res.success && res.data) data = res.data
|
||||
else data = row
|
||||
|
||||
const src = data || {}
|
||||
const q = src.question || src.Question || src
|
||||
const options = src.options || src.Options || []
|
||||
let answer = src.answer || src.Answer || src.answer_content || src.AnswerContent || null
|
||||
if (answer && typeof answer === 'object') {
|
||||
if ('answer_content' in answer && answer.answer_content) {
|
||||
answer = answer.answer_content
|
||||
} else if ('AnswerContent' in answer && answer.AnswerContent) {
|
||||
answer = answer.AnswerContent
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
question_title: q.question_title || q.QuestionTitle || row.question_title,
|
||||
question_type: q.question_type || q.QuestionType || row.question_type,
|
||||
score: q.score || q.Score || row.score || 0,
|
||||
question_analysis: q.question_analysis || q.QuestionAnalysis || row.question_analysis || '',
|
||||
options: Array.isArray(options)
|
||||
? options.map((o, idx) => ({
|
||||
key: o.key || o.label || o.option_label || o.OptionLabel || String.fromCharCode(65 + idx),
|
||||
label: o.label || o.option_label || o.OptionLabel || String.fromCharCode(65 + idx),
|
||||
content: o.content || o.option_content || o.OptionContent || '',
|
||||
}))
|
||||
: (row.options || []),
|
||||
answer: answer !== null && answer !== undefined ? answer : (row.answer ?? ''),
|
||||
}
|
||||
if (props.bankId) payload.bank_id = props.bankId
|
||||
|
||||
await createExamQuestion(payload)
|
||||
ElMessage.success('已拉取到当前题库')
|
||||
duplicateDialogVisible.value = false
|
||||
emit('close')
|
||||
} catch (e) {
|
||||
ElMessage.error('拉取失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handleContinueCreate() {
|
||||
@ -430,29 +477,35 @@ onBeforeUnmount(() => {
|
||||
.form-flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap:15px;
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.options-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.option-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dlg-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@ -465,6 +518,10 @@ onBeforeUnmount(() => {
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
:deep(.editor-container) {
|
||||
min-height: 200px !important;
|
||||
}
|
||||
|
||||
.duplicate-tip {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
|
||||
@ -606,7 +606,7 @@ async function handleDelete(row) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaved(payload) {
|
||||
async function handleSaved(payload, meta) {
|
||||
try {
|
||||
const submit = { ...payload, question_type: normalizeType(payload.question_type) }
|
||||
if (props.bankId) submit.bank_id = props.bankId
|
||||
@ -616,7 +616,10 @@ async function handleSaved(payload) {
|
||||
await createExamQuestion(submit)
|
||||
}
|
||||
ElMessage.success('保存成功')
|
||||
editVisible.value = false
|
||||
// 继续新增时不关闭弹窗
|
||||
if (!meta || !meta.continue) {
|
||||
editVisible.value = false
|
||||
}
|
||||
fetchList()
|
||||
emit('saved')
|
||||
} catch (e) {
|
||||
|
||||
@ -298,4 +298,8 @@ onMounted(() => {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
min-height: 200px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,6 +3,8 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"regexp"
|
||||
|
||||
"server/models"
|
||||
"server/services"
|
||||
@ -14,6 +16,65 @@ 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
|
||||
}
|
||||
@ -22,9 +83,10 @@ type ExamQuestionBankController struct {
|
||||
// @router /exam-questions [get]
|
||||
func (c *ExamQuestionController) GetList() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
keyword := c.GetString("keyword")
|
||||
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 != "" {
|
||||
@ -37,32 +99,62 @@ func (c *ExamQuestionController) GetList() {
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 10)
|
||||
|
||||
list, total, err := services.GetExamQuestions(services.QuestionListParams{
|
||||
TenantId: tenantId,
|
||||
Keyword: keyword,
|
||||
QuestionType: qtype,
|
||||
BankId: bankId,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
// 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
|
||||
}
|
||||
items = append(items, map[string]interface{}{
|
||||
// 可选相似度计算与过滤
|
||||
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{}{
|
||||
@ -160,7 +252,7 @@ func (c *ExamQuestionController) BatchCreate() {
|
||||
tenantId, _ := c.Ctx.Input.GetData("tenantId").(int)
|
||||
var payload struct {
|
||||
BankId int64 `json:"bank_id"`
|
||||
Items []struct {
|
||||
Items []struct {
|
||||
QuestionTitle string `json:"question_title"`
|
||||
QuestionType int8 `json:"question_type"`
|
||||
Score float64 `json:"score"`
|
||||
|
||||
@ -298,7 +298,7 @@ func init() {
|
||||
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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user