From 2e44127672e4fcd23b3fbf488b2391a0d40fd7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BA=90=E6=96=87=E9=9B=A8?= <41315874+fumiama@users.noreply.github.com> Date: Sat, 22 Apr 2023 21:31:12 +0800 Subject: [PATCH] implement genfile --- backend/global/analyze.go | 408 ++++++++++++++++++ backend/global/file.go | 399 +---------------- backend/global/generate.go | 24 ++ backend/global/papertype.go | 4 +- backend/global/question.go | 1 + .../src/locales/lang/zh-CN/routes/genfile.ts | 3 + .../vben/src/router/menus/modules/genfile.ts | 17 + .../vben/src/router/menus/modules/templist.ts | 2 +- .../vben/src/router/routes/modules/genfile.ts | 30 ++ .../src/router/routes/modules/templist.ts | 2 +- .../vben/src/views/dashboard/regex/index.vue | 8 +- .../vben/src/views/page/genfile/Step1.vue | 103 +++++ .../vben/src/views/page/genfile/Step2.vue | 78 ++++ .../vben/src/views/page/genfile/Step3.vue | 49 +++ frontend/vben/src/views/page/genfile/data.tsx | 78 ++++ .../vben/src/views/page/genfile/index.vue | 96 +++++ 16 files changed, 898 insertions(+), 404 deletions(-) create mode 100644 backend/global/analyze.go create mode 100644 backend/global/generate.go create mode 100644 frontend/vben/src/locales/lang/zh-CN/routes/genfile.ts create mode 100644 frontend/vben/src/router/menus/modules/genfile.ts create mode 100644 frontend/vben/src/router/routes/modules/genfile.ts create mode 100644 frontend/vben/src/views/page/genfile/Step1.vue create mode 100644 frontend/vben/src/views/page/genfile/Step2.vue create mode 100644 frontend/vben/src/views/page/genfile/Step3.vue create mode 100644 frontend/vben/src/views/page/genfile/data.tsx create mode 100644 frontend/vben/src/views/page/genfile/index.vue diff --git a/backend/global/analyze.go b/backend/global/analyze.go new file mode 100644 index 0000000..6689164 --- /dev/null +++ b/backend/global/analyze.go @@ -0,0 +1,408 @@ +package global + +import ( + "bytes" + "crypto/md5" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "image" + "io" + "os" + "regexp" + "strconv" + "strings" + "time" + + sql "github.com/FloatTech/sqlite" + "github.com/corona10/goimagehash" + base14 "github.com/fumiama/go-base16384" + "github.com/fumiama/go-docx" + "github.com/fumiama/paper-manager/backend/utils" + "github.com/sirupsen/logrus" +) + +// AddFile from lst and copy it to analyzed path. +// The para reg must belong to a valid user +func (f *FileDatabase) AddFile(lstid int, reg *Regex, istemp bool, progress func(uint)) error { + user, err := UserDB.GetUserByID(reg.ID) + if err != nil { + return err + } + if !user.IsFileManager() && !istemp { + return ErrInvalidRole + } + progress(1) + f.mu.RLock() + lst, err := sql.Find[List](&f.db, FileTableList, "WHERE ID="+strconv.Itoa(lstid)) + f.mu.RUnlock() + if err != nil { + return err + } + if lst.Path == "" || strings.Contains(lst.Path, "..") { + return os.ErrNotExist + } + tempath := lst.Path + docf, err := os.Open(tempath) + if err != nil { + return err + } + defer docf.Close() + progress(2) + h := md5.New() + _, err = io.Copy(h, docf) + if err != nil { + return err + } + var buf [md5.Size]byte + id := int64(binary.LittleEndian.Uint64(h.Sum(buf[:0]))) + _, err = docf.Seek(0, io.SeekStart) + if err != nil { + return err + } + stat, err := docf.Stat() + if err != nil { + return err + } + sz := stat.Size() + progress(3) + doc, err := docx.Parse(docf, sz) + if err != nil { + return err + } + progress(5) + doc.Document.Body.DropDrawingOf("NilPicture") + majorre, err := regexp.Compile(reg.Major) + if err != nil { + return err + } + docs := doc.SplitByParagraph(docx.SplitDocxByPlainTextRegex(majorre)) + if len(docs) < 2 { + return ErrMajorSplitsTooShort + } + progress(9) + // filling File struct + file := &File{ + ID: id, + ListID: *lst.ID, + } + titlere, err := regexp.Compile(reg.Title) + if err != nil { + return err + } + classre, err := regexp.Compile(reg.Class) + if err != nil { + return err + } + opclre, err := regexp.Compile(reg.OpenCl) + if err != nil { + return err + } + datere, err := regexp.Compile(reg.Date) + if err != nil { + return err + } + timere, err := regexp.Compile(reg.Time) + if err != nil { + return err + } + ratere, err := regexp.Compile(reg.Rate) + if err != nil { + return err + } + progress(10) + for _, it := range docs[0].Document.Body.Items { + if p, ok := it.(*docx.Paragraph); ok { + text := p.String() + title := titlere.FindStringSubmatch(text) + if len(title) >= 5 { + years, semesters, mfs, abs := title[1], title[2], title[3], title[4] + y, err := strconv.Atoi(years) + if err != nil { + return err + } + file.Year = StudyYear(y) + if len(semesters) > 0 { + file.Type = file.Type.SetFirstSecond(semesters[0]) + } + file.Type = file.Type.SetMiddleFinal(mfs) + if len(abs) > 0 { + file.Type = file.Type.SetAB(abs[0]) + } + } + class := classre.FindStringSubmatch(text) + if len(class) >= 3 { + file.Class = class[2] + } + opcl := opclre.FindStringSubmatch(text) + if len(opcl) >= 2 { + file.Type = file.Type.SetOpenClose(opcl[1]) + } + date := datere.FindStringSubmatch(text) + if len(date) >= 4 { + y, m, d := date[1], date[2], date[3] + if y != "" && m != "" { + if d == "" { + d = "1" + } + yyyy, err := strconv.ParseUint(y, 10, 64) + if err == nil && yyyy > 1600 { + mm, err := strconv.ParseUint(m, 10, 64) + if err == nil && mm >= 1 && mm <= 12 { + dd, err := strconv.ParseUint(d, 10, 64) + if err == nil && dd >= 1 && dd <= 31 { + file.Date = uint32(yyyy*10000 + mm*100 + dd) + } + } + } + } + } + times := timere.FindStringSubmatch(text) + if len(times) >= 2 { + min, err := strconv.Atoi(times[1]) + if err == nil && min > 0 { + file.Time = time.Minute * time.Duration(min) + } + } + rate := ratere.FindStringSubmatch(text) + if len(rate) >= 3 { + file.Rate = rate[2] + } + } + } + progress(19) + if file.Class == "" || strings.Contains(file.Class, "..") || strings.ContainsAny(file.Class, `/\`) { + return ErrEmptyClass + } + filebasepath := "" + if istemp { + filebasepath = PaperFolder + "temp/" + strconv.Itoa(*user.ID) + "/" + file.Class + "/" + } else { + filebasepath = fmt.Sprintf( + PaperFolder+file.Class+"/%v/%v/%v/%c/", + file.Year, file.Type.FirstSecond(), file.Type.MiddleFinal(), file.Type.AB(), + ) + } + questionpath := filebasepath + "questions/" + err = os.MkdirAll(questionpath, 0755) + if err != nil { + return err + } + docs = docs[1:] + // parse questions + subre, err := regexp.Compile(reg.Sub) + if err != nil { + return err + } + filequestions := make([]QuestionJSON, 0, len(docs)) + lst.QuesC = 0 + progress(20) + p := uint(20) + delta := uint(70 / len(docs)) + if delta == 0 { + delta = 1 + } + for _, majordoc := range docs { + p += delta + if p > 90 { + p = 90 + } + progress(p) + majorq := QuestionJSON{} + for _, it := range majordoc.Document.Body.Items { + if p, ok := it.(*docx.Paragraph); ok { + text := p.String() + majorinfo := majorre.FindStringSubmatch(text) + if len(majorinfo) >= 6 { + name, points := majorinfo[2], majorinfo[5] + majorq.Name = name + majorq.Points, _ = strconv.Atoi(points) + } + } + } + subdocs := majordoc.SplitByParagraph(docx.SplitDocxByPlainTextRegex(subre)) + if len(subdocs) < 2 { + continue + } + subdocs = subdocs[1:] + majorq.Sub = make([]QuestionJSON, 0, len(subdocs)) + for _, subdoc := range subdocs { + sb := bytes.NewBuffer(make([]byte, 0, 4096)) + for _, it := range subdoc.Document.Body.Items { + sb.WriteString(fmt.Sprint(it)) + } + m := md5.Sum(sb.Bytes()) + que := &Question{ + ID: int64(binary.LittleEndian.Uint64(m[:8])), + FileID: file.ID, + Plain: base14.BytesToString(sb.Bytes()), + Images: func() []byte { + m := make(map[string]string) + _ = subdoc.RangeRelationships(func(r *docx.Relationship) error { + if r.Type != docx.REL_IMAGE { + return nil + } + if r.Target == "" { + return nil + } + i := strings.LastIndex(r.Target, "/") + if i < 0 { + return nil + } + name := r.Target[i+1:] + if name == "" { + return nil + } + md := subdoc.Media(name) + if md == nil { + return nil + } + img, _, err := image.Decode(bytes.NewReader(md.Data)) + if err != nil { + return nil + } + dh, err := goimagehash.DifferenceHash(img) + if err != nil { + return nil + } + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], dh.GetHash()) + m[name] = hex.EncodeToString(buf[:]) + return nil + }) + if len(m) == 0 { + return nil + } + data, err := json.Marshal(m) + if err != nil { + return nil + } + return data + }(), + Vector: func() []byte { + words := utils.Segmenter.Cut(base14.BytesToString(sb.Bytes()), true) + if len(words) == 0 { + return nil + } + v := make(map[string]uint8, len(words)*2) + for _, word := range words { + if word != "" && !strings.Contains("\n 。,、的是使,.()()1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ", word) { + if v[word] == 0 { // 二值化 + v[word] = 1 + } + } + } + data, err := json.Marshal(v) + if err != nil { + return nil + } + return data + }(), + } + var q Question + dupmap := make(map[string]float64, 64) + f.mu.RLock() + err = f.db.FindFor(FileTableQuestion, &q, "", func() error { + r, err := q.GetDuplicateRate(que) + if err != nil { + logrus.Warnln("[global.AddFile] GetDuplicateRate err:", err) + return err + } + if r < 0.5 { + return nil + } + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], uint64(q.ID)) + dupmap[hex.EncodeToString(buf[:])] = r + return nil + }) + f.mu.RUnlock() + if err == nil && len(dupmap) > 0 { + que.Dup, _ = json.Marshal(dupmap) + } + w := bytes.NewBuffer(make([]byte, 0, 65536)) + _, err = subdoc.WriteTo(w) + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], uint64(que.ID)) + queidstr := hex.EncodeToString(buf[:]) + if err == nil { + m5 := md5.Sum(w.Bytes()) + quepath := questionpath + hex.EncodeToString(m5[:]) + ".docx" + qf, err := os.Create(quepath) + if err == nil { + _, _ = io.Copy(qf, w) + _ = qf.Close() + } + que.Path = quepath + if istemp { + f.mu.Lock() + _ = f.db.Insert(FileTableTempQuestion, que) + f.mu.Unlock() + } else { + f.mu.Lock() + for k, v := range dupmap { + err = f.db.Find(FileTableQuestion, &q, "WHERE ID=0x"+k) + if err == nil { + thismap := make(map[string]float64, 64) + err := json.Unmarshal(q.Dup, &thismap) + if err == nil { + thismap[queidstr] = v + q.Dup, err = json.Marshal(thismap) + if err == nil { + _ = f.db.Insert(FileTableQuestion, &q) + } + } + } + } + _ = f.db.Insert(FileTableQuestion, que) + f.mu.Unlock() + } + } + r := 0.0 + for _, v := range dupmap { + if v > r { + r = v + } + } + majorq.Sub = append(majorq.Sub, QuestionJSON{ + Name: queidstr, //TODO: fill sub points + }) + } + filequestions = append(filequestions, majorq) + lst.QuesC += len(majorq.Sub) + } + progress(90) + file.Questions, _ = json.Marshal(filequestions) + _, err = docf.Seek(0, io.SeekStart) + if err != nil { + return err + } + lst.Path = filebasepath + file.Class + ".docx" + lst.HasntAnalyzed = false + lst.Desc = fmt.Sprintf("%s%v%v%v%c卷", + file.Class, file.Year, file.Type.FirstSecond(), file.Type.MiddleFinal(), file.Type.AB(), + ) + dstf, err := os.Create(lst.Path) + if err != nil { + return err + } + defer dstf.Close() + _, err = io.Copy(dstf, docf) + if err != nil { + return err + } + progress(95) + f.mu.Lock() + if istemp { + err = f.db.Insert(FileTableTempFile, file) + lst.IsTemp = true + } else { + err = f.db.Insert(FileTableFile, file) + lst.IsTemp = false + } + _ = f.db.Insert(FileTableList, &lst) + f.mu.Unlock() + progress(100) + return err +} diff --git a/backend/global/file.go b/backend/global/file.go index 0338acd..eb85ccf 100644 --- a/backend/global/file.go +++ b/backend/global/file.go @@ -1,17 +1,9 @@ package global import ( - "bytes" - "crypto/md5" - "encoding/binary" - "encoding/hex" "encoding/json" "errors" - "fmt" - "image" - "io" "os" - "regexp" "strconv" "strings" "time" @@ -22,10 +14,6 @@ import ( _ "golang.org/x/image/webp" sql "github.com/FloatTech/sqlite" - "github.com/corona10/goimagehash" - base14 "github.com/fumiama/go-base16384" - "github.com/fumiama/go-docx" - "github.com/sirupsen/logrus" "github.com/fumiama/paper-manager/backend/utils" ) @@ -64,7 +52,9 @@ func init() { if err != nil { panic(err) } - err = FileDB.db.Create(FileTableQuestion, &Question{}) + err = FileDB.db.Create(FileTableQuestion, &Question{}, + "FOREIGN KEY(FileID) REFERENCES "+FileTableFile+"(ID)", + ) if err != nil { panic(err) } @@ -108,389 +98,6 @@ func (sy StudyYear) String() string { return strconv.Itoa(int(sy)) + "-" + strconv.Itoa(int(next)) + "学年" } -// AddFile from lst and copy it to analyzed path. -// The para reg must belong to a valid user -func (f *FileDatabase) AddFile(lstid int, reg *Regex, istemp bool, progress func(uint)) error { - user, err := UserDB.GetUserByID(reg.ID) - if err != nil { - return err - } - if !user.IsFileManager() && !istemp { - return ErrInvalidRole - } - progress(1) - f.mu.RLock() - lst, err := sql.Find[List](&f.db, FileTableList, "WHERE ID="+strconv.Itoa(lstid)) - f.mu.RUnlock() - if err != nil { - return err - } - if lst.Path == "" || strings.Contains(lst.Path, "..") { - return os.ErrNotExist - } - tempath := lst.Path - docf, err := os.Open(tempath) - if err != nil { - return err - } - defer docf.Close() - progress(2) - h := md5.New() - _, err = io.Copy(h, docf) - if err != nil { - return err - } - var buf [md5.Size]byte - id := int64(binary.LittleEndian.Uint64(h.Sum(buf[:0]))) - _, err = docf.Seek(0, io.SeekStart) - if err != nil { - return err - } - stat, err := docf.Stat() - if err != nil { - return err - } - sz := stat.Size() - progress(3) - doc, err := docx.Parse(docf, sz) - if err != nil { - return err - } - progress(5) - doc.Document.Body.DropDrawingOf("NilPicture") - majorre, err := regexp.Compile(reg.Major) - if err != nil { - return err - } - docs := doc.SplitByParagraph(docx.SplitDocxByPlainTextRegex(majorre)) - if len(docs) < 2 { - return ErrMajorSplitsTooShort - } - progress(9) - // filling File struct - file := &File{ - ID: id, - ListID: *lst.ID, - } - titlere, err := regexp.Compile(reg.Title) - if err != nil { - return err - } - classre, err := regexp.Compile(reg.Class) - if err != nil { - return err - } - opclre, err := regexp.Compile(reg.OpenCl) - if err != nil { - return err - } - datere, err := regexp.Compile(reg.Date) - if err != nil { - return err - } - timere, err := regexp.Compile(reg.Time) - if err != nil { - return err - } - ratere, err := regexp.Compile(reg.Rate) - if err != nil { - return err - } - progress(10) - for _, it := range docs[0].Document.Body.Items { - if p, ok := it.(*docx.Paragraph); ok { - text := p.String() - title := titlere.FindStringSubmatch(text) - if len(title) >= 5 { - years, semesters, mfs, abs := title[1], title[2], title[3], title[4] - y, err := strconv.Atoi(years) - if err != nil { - return err - } - file.Year = StudyYear(y) - if len(semesters) > 0 { - file.Type = file.Type.SetFirstSecond(semesters[0]) - } - file.Type = file.Type.SetMiddleFinal(mfs) - if len(abs) > 0 { - file.Type = file.Type.SetAB(abs[0]) - } - } - class := classre.FindStringSubmatch(text) - if len(class) >= 3 { - file.Class = class[2] - } - opcl := opclre.FindStringSubmatch(text) - if len(opcl) >= 2 { - file.Type = file.Type.SetOpenClose(opcl[1]) - } - date := datere.FindStringSubmatch(text) - if len(date) >= 4 { - y, m, d := date[1], date[2], date[3] - if y != "" && m != "" { - if d == "" { - d = "1" - } - yyyy, err := strconv.ParseUint(y, 10, 64) - if err == nil && yyyy > 1600 { - mm, err := strconv.ParseUint(m, 10, 64) - if err == nil && mm >= 1 && mm <= 12 { - dd, err := strconv.ParseUint(d, 10, 64) - if err == nil && dd >= 1 && dd <= 31 { - file.Date = uint32(yyyy*10000 + mm*100 + dd) - } - } - } - } - } - times := timere.FindStringSubmatch(text) - if len(times) >= 2 { - min, err := strconv.Atoi(times[1]) - if err == nil && min > 0 { - file.Time = time.Minute * time.Duration(min) - } - } - rate := ratere.FindStringSubmatch(text) - if len(rate) >= 3 { - file.Rate = rate[2] - } - } - } - progress(19) - if file.Class == "" || strings.Contains(file.Class, "..") || strings.ContainsAny(file.Class, `/\`) { - return ErrEmptyClass - } - filebasepath := "" - if istemp { - filebasepath = PaperFolder + "temp/" + strconv.Itoa(*user.ID) + "/" + file.Class + "/" - } else { - filebasepath = fmt.Sprintf( - PaperFolder+file.Class+"/%v/%v/%v/%c/", - file.Year, file.Type.FirstSecond(), file.Type.MiddleFinal(), file.Type.AB(), - ) - } - questionpath := filebasepath + "questions/" - err = os.MkdirAll(questionpath, 0755) - if err != nil { - return err - } - docs = docs[1:] - // parse questions - subre, err := regexp.Compile(reg.Sub) - if err != nil { - return err - } - filequestions := make([]QuestionJSON, 0, len(docs)) - lst.QuesC = 0 - progress(20) - p := uint(20) - delta := uint(70 / len(docs)) - if delta == 0 { - delta = 1 - } - for _, majordoc := range docs { - p += delta - if p > 90 { - p = 90 - } - progress(p) - majorq := QuestionJSON{} - for _, it := range majordoc.Document.Body.Items { - if p, ok := it.(*docx.Paragraph); ok { - text := p.String() - majorinfo := majorre.FindStringSubmatch(text) - if len(majorinfo) >= 6 { - name, points := majorinfo[2], majorinfo[5] - majorq.Name = name - majorq.Points, _ = strconv.Atoi(points) - } - } - } - subdocs := majordoc.SplitByParagraph(docx.SplitDocxByPlainTextRegex(subre)) - if len(subdocs) < 2 { - continue - } - subdocs = subdocs[1:] - majorq.Sub = make([]QuestionJSON, 0, len(subdocs)) - for _, subdoc := range subdocs { - sb := bytes.NewBuffer(make([]byte, 0, 4096)) - for _, it := range subdoc.Document.Body.Items { - sb.WriteString(fmt.Sprint(it)) - } - m := md5.Sum(sb.Bytes()) - que := &Question{ - ID: int64(binary.LittleEndian.Uint64(m[:8])), - Plain: base14.BytesToString(sb.Bytes()), - Images: func() []byte { - m := make(map[string]string) - _ = subdoc.RangeRelationships(func(r *docx.Relationship) error { - if r.Type != docx.REL_IMAGE { - return nil - } - if r.Target == "" { - return nil - } - i := strings.LastIndex(r.Target, "/") - if i < 0 { - return nil - } - name := r.Target[i+1:] - if name == "" { - return nil - } - md := subdoc.Media(name) - if md == nil { - return nil - } - img, _, err := image.Decode(bytes.NewReader(md.Data)) - if err != nil { - return nil - } - dh, err := goimagehash.DifferenceHash(img) - if err != nil { - return nil - } - var buf [8]byte - binary.LittleEndian.PutUint64(buf[:], dh.GetHash()) - m[name] = hex.EncodeToString(buf[:]) - return nil - }) - if len(m) == 0 { - return nil - } - data, err := json.Marshal(m) - if err != nil { - return nil - } - return data - }(), - Vector: func() []byte { - words := utils.Segmenter.Cut(base14.BytesToString(sb.Bytes()), true) - if len(words) == 0 { - return nil - } - v := make(map[string]uint8, len(words)*2) - for _, word := range words { - if word != "" && !strings.Contains("\n 。,、的是使,.()()1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ", word) { - if v[word] == 0 { // 二值化 - v[word] = 1 - } - } - } - data, err := json.Marshal(v) - if err != nil { - return nil - } - return data - }(), - } - var q Question - dupmap := make(map[string]float64, 64) - f.mu.RLock() - err = f.db.FindFor(FileTableQuestion, &q, "", func() error { - r, err := q.GetDuplicateRate(que) - if err != nil { - logrus.Warnln("[global.AddFile] GetDuplicateRate err:", err) - return err - } - if r < 0.5 { - return nil - } - var buf [8]byte - binary.LittleEndian.PutUint64(buf[:], uint64(q.ID)) - dupmap[hex.EncodeToString(buf[:])] = r - return nil - }) - f.mu.RUnlock() - if err == nil && len(dupmap) > 0 { - que.Dup, _ = json.Marshal(dupmap) - } - w := bytes.NewBuffer(make([]byte, 0, 65536)) - _, err = subdoc.WriteTo(w) - var buf [8]byte - binary.LittleEndian.PutUint64(buf[:], uint64(que.ID)) - queidstr := hex.EncodeToString(buf[:]) - if err == nil { - m5 := md5.Sum(w.Bytes()) - quepath := questionpath + hex.EncodeToString(m5[:]) + ".docx" - qf, err := os.Create(quepath) - if err == nil { - _, _ = io.Copy(qf, w) - _ = qf.Close() - } - que.Path = quepath - if istemp { - f.mu.Lock() - _ = f.db.Insert(FileTableTempQuestion, que) - f.mu.Unlock() - } else { - f.mu.Lock() - for k, v := range dupmap { - err = f.db.Find(FileTableQuestion, &q, "WHERE ID=0x"+k) - if err == nil { - thismap := make(map[string]float64, 64) - err := json.Unmarshal(q.Dup, &thismap) - if err == nil { - thismap[queidstr] = v - q.Dup, err = json.Marshal(thismap) - if err == nil { - _ = f.db.Insert(FileTableQuestion, &q) - } - } - } - } - _ = f.db.Insert(FileTableQuestion, que) - f.mu.Unlock() - } - } - r := 0.0 - for _, v := range dupmap { - if v > r { - r = v - } - } - majorq.Sub = append(majorq.Sub, QuestionJSON{ - Name: queidstr, //TODO: fill sub points - }) - } - filequestions = append(filequestions, majorq) - lst.QuesC += len(majorq.Sub) - } - progress(90) - file.Questions, _ = json.Marshal(filequestions) - _, err = docf.Seek(0, io.SeekStart) - if err != nil { - return err - } - lst.Path = filebasepath + file.Class + ".docx" - lst.HasntAnalyzed = false - lst.Desc = fmt.Sprintf("%s%v%v%v%c卷", - file.Class, file.Year, file.Type.FirstSecond(), file.Type.MiddleFinal(), file.Type.AB(), - ) - dstf, err := os.Create(lst.Path) - if err != nil { - return err - } - defer dstf.Close() - _, err = io.Copy(dstf, docf) - if err != nil { - return err - } - progress(95) - f.mu.Lock() - if istemp { - err = f.db.Insert(FileTableTempFile, file) - lst.IsTemp = true - } else { - err = f.db.Insert(FileTableFile, file) - lst.IsTemp = false - } - _ = f.db.Insert(FileTableList, &lst) - f.mu.Unlock() - progress(100) - return err -} - // DelFile by listid func (f *FileDatabase) DelFile(lstid, uid int, istemp bool) error { user, err := UserDB.GetUserByID(uid) diff --git a/backend/global/generate.go b/backend/global/generate.go new file mode 100644 index 0000000..3fe5bbf --- /dev/null +++ b/backend/global/generate.go @@ -0,0 +1,24 @@ +package global + +import "errors" + +var ( + ErrInvalidGenerateConfig = errors.New("invalid generate config") +) + +// GenerateConfig 试卷生成配置 +type GenerateConfig struct { + Distribution map[string]uint // Distribution is map[majorname]subcount + RateLimit float64 // RateLimit 重复率上限 + YearStart StudyYear // YearStart 起始年份(空则直到最旧) + YearEnd StudyYear // YearEnd 截止年份(空则直到最新) + TypeMask PaperType // TypeMask & File.Type != 0 则匹配 +} + +// GenerateFile 用一些限定条件生成新试卷, 云端不保存 +func (f *FileDatabase) GenerateFile(config *GenerateConfig) ([]byte, error) { + if config == nil || config.Distribution == nil { + return nil, ErrInvalidGenerateConfig + } + return nil, nil +} diff --git a/backend/global/papertype.go b/backend/global/papertype.go index fbbbfc0..437aa6c 100644 --- a/backend/global/papertype.go +++ b/backend/global/papertype.go @@ -79,7 +79,7 @@ func (pt PaperType) OpenClose() string { return "开卷" case 2: return "一页纸开卷" - case 3: + case 4: return "闭卷" default: return "闭卷" @@ -94,7 +94,7 @@ func (pt PaperType) SetOpenClose(x string) PaperType { case "一页纸开卷": n = 2 << 12 case "闭卷": - n = 3 << 12 + n = 4 << 12 } return pt | n } diff --git a/backend/global/question.go b/backend/global/question.go index 625d723..4ffe5c2 100644 --- a/backend/global/question.go +++ b/backend/global/question.go @@ -80,6 +80,7 @@ func (f *FileDatabase) DelQuestion(id int64, istemp bool) error { type Question struct { ID int64 // ID is the first 8 bytes of the Plain's md5 + FileID int64 // FileID is fk to File(ID) Path string // Path is the question's docx position Plain string // Plain is the plain text of the question (like markdown format) Images []byte // Images is json of the image dhash in XML, ex. ['rId1': '1234567890abcdef', ...] diff --git a/frontend/vben/src/locales/lang/zh-CN/routes/genfile.ts b/frontend/vben/src/locales/lang/zh-CN/routes/genfile.ts new file mode 100644 index 0000000..7bffa04 --- /dev/null +++ b/frontend/vben/src/locales/lang/zh-CN/routes/genfile.ts @@ -0,0 +1,3 @@ +export default { + name: '试卷生成', +} diff --git a/frontend/vben/src/router/menus/modules/genfile.ts b/frontend/vben/src/router/menus/modules/genfile.ts new file mode 100644 index 0000000..8799c0a --- /dev/null +++ b/frontend/vben/src/router/menus/modules/genfile.ts @@ -0,0 +1,17 @@ +import type { MenuModule } from '/@/router/types' +import { t } from '/@/hooks/web/useI18n' +const menu: MenuModule = { + orderNo: 40, + menu: { + name: t('routes.genfile.name'), + path: '/genfile', + + children: [ + { + path: 'index', + name: t('routes.genfile.name'), + }, + ], + }, +} +export default menu diff --git a/frontend/vben/src/router/menus/modules/templist.ts b/frontend/vben/src/router/menus/modules/templist.ts index fa52ec8..56c1b6c 100644 --- a/frontend/vben/src/router/menus/modules/templist.ts +++ b/frontend/vben/src/router/menus/modules/templist.ts @@ -1,7 +1,7 @@ import type { MenuModule } from '/@/router/types' import { t } from '/@/hooks/web/useI18n' const menu: MenuModule = { - orderNo: 20, + orderNo: 30, menu: { name: t('routes.templist.name'), path: '/templist', diff --git a/frontend/vben/src/router/routes/modules/genfile.ts b/frontend/vben/src/router/routes/modules/genfile.ts new file mode 100644 index 0000000..855d750 --- /dev/null +++ b/frontend/vben/src/router/routes/modules/genfile.ts @@ -0,0 +1,30 @@ +import type { AppRouteModule } from '/@/router/types' +import { LAYOUT } from '/@/router/constant' +import { t } from '/@/hooks/web/useI18n' + +const genfile: AppRouteModule = { + path: '/genfile', + name: 'GenFile', + component: LAYOUT, + redirect: '/genfile/index', + meta: { + hideChildrenInMenu: true, + icon: 'ion:balloon-outline', + title: t('routes.genfile.name'), + orderNo: 40, + }, + children: [ + { + path: 'index', + name: 'GenFilePage', + component: () => import('/@/views/page/genfile/index.vue'), + meta: { + title: t('routes.genfile.name'), + icon: 'ion:balloon-outline', + hideMenu: true, + }, + }, + ], +} + +export default genfile diff --git a/frontend/vben/src/router/routes/modules/templist.ts b/frontend/vben/src/router/routes/modules/templist.ts index 2279b6b..d7390ce 100644 --- a/frontend/vben/src/router/routes/modules/templist.ts +++ b/frontend/vben/src/router/routes/modules/templist.ts @@ -11,7 +11,7 @@ const templist: AppRouteModule = { hideChildrenInMenu: true, icon: 'ion:ios-analytics', title: t('routes.templist.name'), - orderNo: 20, + orderNo: 30, }, children: [ { diff --git a/frontend/vben/src/views/dashboard/regex/index.vue b/frontend/vben/src/views/dashboard/regex/index.vue index 667e2ae..7b640e7 100644 --- a/frontend/vben/src/views/dashboard/regex/index.vue +++ b/frontend/vben/src/views/dashboard/regex/index.vue @@ -25,15 +25,15 @@ const { createMessage } = useMessage() const [register, { validate, setProps }] = useForm({ labelCol: { - span: 8, + span: 5, }, wrapperCol: { - span: 15, + span: 16, }, schemas: schemas, actionColOptions: { - offset: 8, - span: 23, + offset: 3, + span: 16, }, submitButtonOptions: { text: '提交', diff --git a/frontend/vben/src/views/page/genfile/Step1.vue b/frontend/vben/src/views/page/genfile/Step1.vue new file mode 100644 index 0000000..cae25e8 --- /dev/null +++ b/frontend/vben/src/views/page/genfile/Step1.vue @@ -0,0 +1,103 @@ + + + diff --git a/frontend/vben/src/views/page/genfile/Step2.vue b/frontend/vben/src/views/page/genfile/Step2.vue new file mode 100644 index 0000000..9ce31b6 --- /dev/null +++ b/frontend/vben/src/views/page/genfile/Step2.vue @@ -0,0 +1,78 @@ + + + diff --git a/frontend/vben/src/views/page/genfile/Step3.vue b/frontend/vben/src/views/page/genfile/Step3.vue new file mode 100644 index 0000000..1b9da20 --- /dev/null +++ b/frontend/vben/src/views/page/genfile/Step3.vue @@ -0,0 +1,49 @@ + + + diff --git a/frontend/vben/src/views/page/genfile/data.tsx b/frontend/vben/src/views/page/genfile/data.tsx new file mode 100644 index 0000000..bf8ba5c --- /dev/null +++ b/frontend/vben/src/views/page/genfile/data.tsx @@ -0,0 +1,78 @@ +import { FormSchema } from '/@/components/Form' + +export const step1Schemas: FormSchema[] = [ + { + field: 'account', + component: 'Select', + label: '付款账户', + required: true, + defaultValue: '1', + componentProps: { + options: [ + { + label: 'anncwb@126.com', + value: '1', + }, + ], + }, + colProps: { + span: 24, + }, + }, + { + field: 'fac', + component: 'InputGroup', + label: '收款账户', + required: true, + defaultValue: 'test@example.com', + slot: 'fac', + colProps: { + span: 24, + }, + }, + { + field: 'pay', + component: 'Input', + label: '', + defaultValue: 'zfb', + show: false, + }, + { + field: 'payeeName', + component: 'Input', + label: '收款人姓名', + defaultValue: 'Vben', + required: true, + colProps: { + span: 24, + }, + }, + { + field: 'money', + component: 'Input', + label: '转账金额', + defaultValue: '500', + required: true, + renderComponentContent: () => { + return { + prefix: () => '¥', + } + }, + colProps: { + span: 24, + }, + }, +] + +export const step2Schemas: FormSchema[] = [ + { + field: 'pwd', + component: 'InputPassword', + label: '支付密码', + required: true, + defaultValue: '123456', + colProps: { + span: 24, + }, + }, +] diff --git a/frontend/vben/src/views/page/genfile/index.vue b/frontend/vben/src/views/page/genfile/index.vue new file mode 100644 index 0000000..def8486 --- /dev/null +++ b/frontend/vben/src/views/page/genfile/index.vue @@ -0,0 +1,96 @@ + + +