1
0
mirror of https://github.com/fumiama/paper-manager.git synced 2026-06-04 23:40:24 +08:00

finish chkdup

This commit is contained in:
源文雨
2023-05-04 23:05:40 +08:00
parent f5c5d0d526
commit ad46584760
9 changed files with 466 additions and 41 deletions

View File

@@ -1,7 +1,10 @@
package backend
import (
"container/heap"
"encoding/json"
"errors"
"math"
"net/http"
"strconv"
"strings"
@@ -31,8 +34,8 @@ type filelist struct {
Per uint `json:"percent"`
}
func getFileList(count int, istemp *bool) ([]filelist, error) {
lst, err := global.FileDB.ListUploadedFile(istemp)
func getFileList(count, uid int, istemp *bool) ([]filelist, error) {
lst, err := global.FileDB.ListUploadedFile(istemp, uid)
if err != nil && err != sql.ErrNullResult {
return nil, err
}
@@ -124,23 +127,127 @@ type filestatus struct {
}
func getFileStatus(lstid int, user *global.User) (*filestatus, error) {
file, sz, istemp, err := global.FileDB.GetFile(lstid, *user.ID)
file, lst, err := global.FileDB.GetFile(lstid, *user.ID)
if err != nil {
return nil, err
}
qs, ds, filerate, err := parseFileQuestions(file.Questions, istemp)
qs, ds, filerate, err := parseFileQuestions(file.Questions, lst.IsTemp, true)
if err != nil {
return nil, err
}
return &filestatus{
Name: file.Class + ".docx",
Size: float64(sz) / 1024 / 1024, // MB
Size: float64(lst.Size) / 1024 / 1024, // MB
Rate: filerate * 100,
Questions: qs,
Duplications: ds,
}, nil
}
type filestatusdup struct {
Name string `json:"name"`
Size float64 `json:"size"`
Rate int `json:"rate"`
Questions []question `json:"questions"`
Duplications []duplication `json:"duplications"` // Duplications length == 10
Files []duplication `json:"files"` // Files is {dup: desc}[] in yearstart..yearend
}
func checkFileDup(lstid int, user *global.User, yearstart, yearend global.StudyYear) (*filestatusdup, error) {
file, lst, err := global.FileDB.GetFile(lstid, *user.ID)
if err != nil {
return nil, err
}
files, err := global.FileDB.GetFilesByYearRange(yearstart, yearend)
if err != nil {
return nil, err
}
qs := make([]question, 0, 16)
filesdups := make([]duplication, 0, 16)
ques := make([]global.QuestionJSON, 0, 64)
myques := make([]global.QuestionJSON, 0, 64)
err = json.Unmarshal(file.Questions, &myques)
if err != nil {
return nil, err
}
dh := make(duplications, 0, 16)
heap.Init(&dh)
filerate := 0
isfirstmy := true
totl := 0
for _, f := range files {
if f.ListID == lstid {
continue
}
ques = ques[:0]
err := json.Unmarshal(f.Questions, &ques)
if err != nil {
return nil, err
}
sum := 0.0
cnt := 0
for _, q := range myques {
if isfirstmy {
qs = append(qs, question{
Count: len(q.Sub),
Point: q.Points,
Name: q.Name,
})
}
for i, subq := range q.Sub {
p, err := getQuestionDupFromPaper(subq, ques, lst.IsTemp)
if err != nil {
return nil, err
}
heap.Push(&dh, duplication{
Percent: int(math.Round(p * 100)),
Name: q.Name + "." + strconv.Itoa(i+1),
})
sum += p
cnt++
}
}
isfirstmy = false
pc := int(math.Round(sum * 100 / float64(cnt)))
flst, err := f.GetList(&global.FileDB)
name := ""
if err != nil {
name = err.Error()
} else {
name = flst.Desc
}
filesdups = append(filesdups, duplication{
Percent: pc,
Name: name,
})
filerate += pc
totl++
}
if totl == 0 {
return nil, sql.ErrNullResult
}
i := dh.Len()
ds := make([]duplication, 10)
if i > 10 {
i = 10
} else {
for j := i; j < 10; j++ {
ds[j] = duplication{Name: "N/A"}
}
}
for i--; i >= 0; i-- {
ds[i] = heap.Pop(&dh).(duplication)
}
return &filestatusdup{
Name: file.Class + ".docx",
Size: float64(lst.Size) / 1024 / 1024, // MB
Rate: filerate / totl,
Questions: qs,
Duplications: ds,
Files: filesdups,
}, nil
}
func init() {
apimap["/api/getFileList"] = &apihandler{"GET", func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
@@ -165,7 +272,7 @@ func init() {
return
}
}
lst, err := getFileList(count, istemp)
lst, err := getFileList(count, *user.ID, istemp)
if err != nil {
writeresult(w, codeError, nil, err.Error(), typeError)
return
@@ -281,6 +388,51 @@ func init() {
}
writeresult(w, codeSuccess, fstat, messageOk, typeSuccess)
}}
apimap["/api/checkFileDup"] = &apihandler{"GET", func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user := usertokens.Get(token)
if user == nil {
writeresult(w, codeError, nil, errInvalidToken.Error(), typeError)
return
}
idstr := r.URL.Query().Get("id")
if idstr == "" {
writeresult(w, codeError, nil, "empty id", typeError)
return
}
id, err := strconv.Atoi(idstr)
if err != nil {
writeresult(w, codeError, nil, err.Error(), typeError)
return
}
ysstr := r.URL.Query().Get("ys")
if ysstr == "" {
writeresult(w, codeError, nil, "empty ys", typeError)
return
}
ys, err := strconv.Atoi(ysstr)
if err != nil {
writeresult(w, codeError, nil, err.Error(), typeError)
return
}
yestr := r.URL.Query().Get("ye")
if yestr == "" {
writeresult(w, codeError, nil, "empty ye", typeError)
return
}
ye, err := strconv.Atoi(yestr)
if err != nil {
writeresult(w, codeError, nil, err.Error(), typeError)
return
}
fstat, err := checkFileDup(id, user, global.StudyYear(ys), global.StudyYear(ye))
if err != nil {
writeresult(w, codeError, nil, err.Error(), typeError)
return
}
writeresult(w, codeSuccess, fstat, messageOk, typeSuccess)
}}
}
// FileHandler serves contents in global.FileFolder

View File

@@ -98,6 +98,12 @@ func (sy StudyYear) String() string {
return strconv.Itoa(int(sy)) + "-" + strconv.Itoa(int(next)) + "学年"
}
func (file *File) GetList(f *FileDatabase) (List, error) {
f.mu.RLock()
defer f.mu.RUnlock()
return sql.Find[List](&f.db, FileTableList, "WHERE ID="+strconv.Itoa(file.ListID))
}
// DelFile by listid
func (f *FileDatabase) DelFile(lstid, uid int, istemp bool) error {
user, err := UserDB.GetUserByID(uid)
@@ -162,10 +168,10 @@ func (f *FileDatabase) DelFile(lstid, uid int, istemp bool) error {
}
// GetFile get analyzed file's structure from List(ID)
func (f *FileDatabase) GetFile(lstid, uid int) (file *File, sz int64, istemp bool, err error) {
func (f *FileDatabase) GetFile(lstid, uid int) (file *File, lst List, err error) {
f.mu.RLock()
defer f.mu.RUnlock()
lst, err := sql.Find[List](&f.db, FileTableList, "WHERE ID="+strconv.Itoa(lstid))
lst, err = sql.Find[List](&f.db, FileTableList, "WHERE ID="+strconv.Itoa(lstid))
if err != nil {
return
}
@@ -187,5 +193,12 @@ func (f *FileDatabase) GetFile(lstid, uid int) (file *File, sz int64, istemp boo
if err != nil {
return
}
return &filestruct, lst.Size, lst.IsTemp, nil
return &filestruct, lst, nil
}
// GetFilesByYearRange ...
func (f *FileDatabase) GetFilesByYearRange(yearstart, yearend StudyYear) ([]*File, error) {
f.mu.RLock()
defer f.mu.RUnlock()
return sql.FindAll[File](&f.db, FileTableFile, "WHERE Year>="+strconv.Itoa(int(yearstart))+" AND Year<="+strconv.Itoa(int(yearend)))
}

View File

@@ -83,13 +83,13 @@ func (f *FileDatabase) SaveFileToTemp(uploader int, file io.Reader, name string)
}
// ListUploadedFile will select all file that HasntAnalyzed && IsTemp or !HasntAnalyzed && !IsTemp
func (f *FileDatabase) ListUploadedFile(istemp *bool) (lst []*List, err error) {
func (f *FileDatabase) ListUploadedFile(istemp *bool, uid int) (lst []*List, err error) {
q := ""
switch {
case istemp == nil:
q = "ORDER BY UpTime DESC"
q = "WHERE (NOT IsTemp) OR (Uploader=" + strconv.Itoa(uid) + ") ORDER BY UpTime DESC"
case *istemp:
q = "WHERE IsTemp ORDER BY UpTime DESC"
q = "WHERE IsTemp AND Uploader=" + strconv.Itoa(uid) + " ORDER BY UpTime DESC"
default:
q = "WHERE (HasntAnalyzed AND IsTemp) OR (NOT HasntAnalyzed AND NOT IsTemp) ORDER BY UpTime DESC"
}

View File

@@ -103,7 +103,7 @@ func (f *FileDatabase) GetQuestion(id int64, istemp bool) (Question, error) {
}
// GetQuestionHex by hexid
func (f *FileDatabase) GetQuestionHex(hexid string, istemp bool) (q Question, err error) {
func (f *FileDatabase) GetQuestionByHex(hexid string, istemp bool) (q Question, err error) {
idb, err := hex.DecodeString(hexid)
if err != nil {
return

View File

@@ -50,15 +50,19 @@ func (d *duplications) Pop() any {
return x
}
func parseFileQuestions(qb []byte, istemp bool) ([]question, []duplication, float64, error) {
func parseFileQuestions(qb []byte, istemp, getdup bool) ([]question, []duplication, float64, error) {
ques := make([]global.QuestionJSON, 0, 16)
qs := make([]question, 0, 16)
err := json.Unmarshal(qb, &ques)
if err != nil {
return nil, nil, 0, err
}
dh := make(duplications, 0, 16)
heap.Init(&dh)
dhp := (*duplications)(nil)
if getdup {
dh := make(duplications, 0, 16)
heap.Init(&dh)
dhp = &dh
}
sum := 0.0
cnt := 0
for _, q := range ques {
@@ -68,33 +72,63 @@ func parseFileQuestions(qb []byte, istemp bool) ([]question, []duplication, floa
Name: q.Name,
})
for i, subq := range q.Sub {
qstruct, err := global.FileDB.GetQuestionHex(subq.Name, istemp)
qstruct, err := global.FileDB.GetQuestionByHex(subq.Name, istemp)
if err != nil {
continue
}
p := qstruct.MaxDuplicateRate()
heap.Push(&dh, duplication{
Percent: int(math.Round(p * 100)),
Name: q.Name + "." + strconv.Itoa(i+1),
})
if getdup {
heap.Push(dhp, duplication{
Percent: int(math.Round(p * 100)),
Name: q.Name + "." + strconv.Itoa(i+1),
})
}
sum += p
cnt++
}
}
i := dh.Len()
ds := make([]duplication, 10)
if i > 10 {
i = 10
} else {
for j := i; j < 10; j++ {
ds[j] = duplication{Name: "N/A"}
if getdup {
i := dhp.Len()
ds := make([]duplication, 10)
if i > 10 {
i = 10
} else {
for j := i; j < 10; j++ {
ds[j] = duplication{Name: "N/A"}
}
}
}
for i--; i >= 0; i-- {
ds[i] = heap.Pop(&dh).(duplication)
for i--; i >= 0; i-- {
ds[i] = heap.Pop(dhp).(duplication)
}
return qs, ds, sum / float64(cnt), nil
}
return qs, ds, sum / float64(cnt), nil
return qs, nil, sum / float64(cnt), nil
}
// getQuestionDupFromPaper returns rate, error
func getQuestionDupFromPaper(que global.QuestionJSON, ques []global.QuestionJSON, istemp bool) (float64, error) {
myqstruct, err := global.FileDB.GetQuestionByHex(que.Name, istemp)
if err != nil {
return -1, err
}
maxp := 0.0
for _, q := range ques {
for _, subq := range q.Sub {
qstruct, err := global.FileDB.GetQuestionByHex(subq.Name, istemp)
if err != nil {
continue
}
p, err := myqstruct.GetDuplicateRate(&qstruct)
if err != nil {
continue
}
if maxp < p {
maxp = p
}
}
}
return maxp, nil
}
func init() {

View File

@@ -5,7 +5,7 @@ import {
FileListGroupItem,
GenerateConfig,
} from './model/fileListModel'
import { DownloadFile, FileStatus } from './model/fileModel'
import { DownloadFile, FileDupStatus, FileStatus } from './model/fileModel'
enum Api {
GetFileList = '/getFileList',
@@ -15,6 +15,7 @@ enum Api {
AnalyzeFile = '/analyzeFile',
DlFile = '/dlFile',
GetFileStatus = '/getFileStatus',
CheckFileDup = '/checkFileDup',
GetMajors = '/getMajors',
GenFile = '/genFile',
DlGen = '/dlGen',
@@ -95,6 +96,16 @@ export const getFileStatus = (id: number) => {
return defHttp.get<FileStatus>({ url: Api.GetFileStatus, params: { id: id } })
}
/**
* @description: Check file duplication
*/
export const checkFileDup = (id: number, ys: number, ye: number) => {
return defHttp.get<FileDupStatus>(
{ url: Api.CheckFileDup, params: { id, ys, ye } },
{ errorMessageMode: 'none' },
)
}
/**
* @description: Get majors
*/

View File

@@ -20,3 +20,12 @@ export interface FileStatus {
questions: Question[]
duplications: Duplication[]
}
export interface FileDupStatus {
name: string
size: number
rate: number
questions: Question[]
duplications: Duplication[]
files: Duplication[]
}

View File

@@ -1,4 +1,5 @@
import { FormSchema } from '/@/components/Form'
import { BasicColumn } from '/@/components/Table/src/types/table'
export const schemas: FormSchema[] = [
{
@@ -22,3 +23,16 @@ export const taskSchemas: FormSchema[] = [
},
},
]
export const columns: BasicColumn[] = [
{
title: '试卷',
dataIndex: 'name',
fixed: 'left',
width: 200,
},
{
title: '重复率(%)',
dataIndex: 'percent',
width: 150,
},
]

View File

@@ -24,8 +24,31 @@
<a-card title="查重限定条件" :bordered="false" class="!mt-5">
<BasicForm @register="registerTask" />
</a-card>
<a-card title="查重报告" :bordered="false" class="!mt-5">
<p> aaaaa </p>
<a-card title="查重报告" :bordered="false" class="!mt-5" v-if="tableRef && tableRef.length > 0">
<div ref="chartRef" :style="{ height, width }"></div>
<BasicTable
title="详细信息"
:columns="columns"
:dataSource="tableRef"
:canResize="canResize"
:loading="loading"
:striped="striped"
:bordered="border"
showTableSetting
:pagination="pagination"
>
<template #toolbar>
<a-button type="primary" @click="toggleCanResize">
{{ !canResize ? '自适应高度' : '取消自适应' }}
</a-button>
<a-button type="primary" @click="toggleBorder">
{{ !border ? '显示边框' : '隐藏边框' }}
</a-button>
<a-button type="primary" @click="toggleStriped">
{{ !striped ? '显示斑马纹' : '隐藏斑马纹' }}
</a-button>
</template>
</BasicTable>
</a-card>
<template #rightFooter>
@@ -35,23 +58,55 @@
</template>
<script lang="ts">
import { BasicForm, useForm, ApiSelect } from '/@/components/Form'
import { defineComponent, ref, unref, computed } from 'vue'
import { BasicTable } from '/@/components/Table'
import { defineComponent, ref, unref, computed, Ref } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import { useECharts } from '/@/hooks/web/useECharts'
import { PageWrapper } from '/@/components/Page'
import { schemas, taskSchemas } from './data'
import { useMessage } from '/@/hooks/web/useMessage'
import { schemas, taskSchemas, columns } from './data'
import { Card } from 'ant-design-vue'
import { useI18n } from '/@/hooks/web/useI18n'
import { getFileOptions } from '/@/api/page'
import { getFileOptions, checkFileDup } from '/@/api/page'
export default defineComponent({
name: 'FormHightPage',
components: { ApiSelect, BasicForm, PageWrapper, [Card.name]: Card },
components: { ApiSelect, BasicForm, BasicTable, PageWrapper, [Card.name]: Card },
props: {
width: {
type: String as PropType<string>,
default: '100%',
},
height: {
type: String as PropType<string>,
default: 'calc(100vh - 78px)',
},
},
setup() {
const { t } = useI18n()
const keyword = ref<string>('')
const searchParams = computed<Recordable>(() => {
return { keyword: unref(keyword) }
})
const chartRef = ref<HTMLDivElement | null>(null)
const tableRef = ref<any>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
const { createMessage } = useMessage()
const canResize = ref(false)
const loading = ref(false)
const striped = ref(true)
const border = ref(true)
const pagination = ref<any>(false)
function toggleCanResize() {
canResize.value = !canResize.value
}
function toggleStriped() {
striped.value = !striped.value
}
function toggleBorder() {
border.value = !border.value
}
const [register, { validate }] = useForm({
layout: 'vertical',
@@ -74,8 +129,134 @@
async function submitAll() {
try {
const [values, taskValues] = await Promise.all([validate(), validateTaskForm()])
console.log('form data:', values, taskValues)
} catch (error) {}
const ret = await checkFileDup(
Number(values.f1),
taskValues.t1[0].$y,
taskValues.t1[1].$y,
)
if (
!(
ret &&
ret.duplications.length > 0 &&
ret.questions.length > 0 &&
ret.files.length > 0
)
) {
createMessage.warn('请先分析该试卷!')
return
}
const barNames = ret.duplications.map((value) => {
return value.name
})
const barData = ret.duplications.map((value) => {
return value.percent
})
const queData = ret.questions.map((value) => {
return { value: value.count, name: value.name }
})
const ptData = ret.questions.map((value) => {
return { value: value.point, name: value.name }
})
tableRef.value = ret.files
setOptions({
title: [
{
text: '题量占比',
left: '2%',
top: '1%',
textStyle: {
fontSize: 20,
},
},
{
text: '平均重复率: ' + ret.rate.toFixed(2) + '%, 前十如下',
left: '40%',
top: '1%',
textStyle: {
fontSize: 20,
},
},
{
text: '分数占比',
left: '2%',
top: '50%',
textStyle: {
fontSize: 20,
},
},
],
grid: [{ left: '50%', top: '7%', width: '45%', height: '90%' }],
tooltip: {
formatter: '{b} ({c})',
},
xAxis: [
{
gridIndex: 0,
axisTick: { show: false },
axisLabel: { show: false },
splitLine: { show: false },
axisLine: { show: false },
},
],
yAxis: [
{
gridIndex: 0,
interval: 0,
data: barNames,
axisTick: { show: false },
axisLabel: { show: true },
splitLine: { show: false },
axisLine: { show: true },
},
],
series: [
{
name: '题量占比',
type: 'pie',
radius: '30%',
center: ['22%', '25%'],
data: queData,
labelLine: { show: false },
label: {
show: true,
formatter: function (d) {
return d.name + '(' + d.value + ')'
},
},
},
{
name: '分数占比',
type: 'pie',
radius: '30%',
center: ['22%', '75%'],
labelLine: { show: false },
data: ptData,
label: {
show: true,
formatter: '{b}\n  ({d}%) ',
},
},
{
name: '重复率前十',
type: 'bar',
xAxisIndex: 0,
yAxisIndex: 0,
barWidth: '45%',
itemStyle: { color: '#86c9f4' },
label: {
show: true,
position: 'right',
formatter: function (d) {
return d.data + '%'
},
},
data: barData,
},
],
})
} catch (error) {
createMessage.error('加载分析数据错误: ' + (error as unknown as Error).message)
}
}
function onSearch(value: string) {
@@ -84,12 +265,23 @@
return {
t,
columns,
chartRef,
tableRef,
register,
registerTask,
submitAll,
getFileOptions,
searchParams,
onSearch: useDebounceFn(onSearch, 300),
canResize,
loading,
striped,
border,
toggleStriped,
toggleCanResize,
toggleBorder,
pagination,
}
},
})