animeic il y a 2 ans
commit
225914c29f
74 fichiers modifiés avec 6107 ajouts et 0 suppressions
  1. 13 0
      Dockerfile
  2. 7 0
      build.bat
  3. 0 0
      build.sh
  4. 14 0
      cmd/config-linux.yaml
  5. 21 0
      cmd/main.go
  6. 14 0
      config-linux.yaml
  7. 12 0
      config.yaml
  8. 86 0
      config/config.go
  9. 14 0
      dao/dao-base.go
  10. 162 0
      dao/dao-class.go
  11. 574 0
      dao/dao-exam-record.go
  12. 457 0
      dao/dao-exam.go
  13. 30 0
      dao/dao-perm.go
  14. 124 0
      dao/dao-question-bank.go
  15. 43 0
      dao/dao-role.go
  16. 265 0
      dao/dao-subject.go
  17. 117 0
      dao/dao-term.go
  18. 311 0
      dao/dao-test-paper.go
  19. 920 0
      dao/dao-user.go
  20. 48 0
      docker-compose.yml
  21. 14 0
      entity/class.go
  22. 26 0
      entity/exam.go
  23. 29 0
      entity/exam_record.go
  24. 15 0
      entity/perm.go
  25. 12 0
      entity/perm_role.go
  26. 21 0
      entity/question_bank.go
  27. 11 0
      entity/role.go
  28. 52 0
      entity/subject.go
  29. 14 0
      entity/term.go
  30. 28 0
      entity/test_paper.go
  31. 23 0
      entity/user.go
  32. 12 0
      entity/user_role.go
  33. BIN
      file/判断题.xlsx
  34. BIN
      file/单选题.xlsx
  35. BIN
      file/填空题.xlsx
  36. BIN
      file/多选题.xlsx
  37. BIN
      file/成绩导出.xlsx
  38. BIN
      file/考生信息导出.xlsx
  39. BIN
      file/考生模板.zip
  40. BIN
      file/试题模板.zip
  41. 55 0
      go.mod
  42. 578 0
      go.sum
  43. BIN
      go_build_exam_system_cmd.exe
  44. 51 0
      middleware/auth/auth.go
  45. 53 0
      middleware/claims/claims.go
  46. 24 0
      middleware/exception/exception.go
  47. 18 0
      middleware/middleware.go
  48. 66 0
      middleware/perm/perm.go
  49. 1 0
      middleware/res/res.go
  50. 11 0
      my.cnf
  51. 73 0
      result/result.go
  52. 47 0
      router/router-auth.go
  53. 79 0
      router/router-class.go
  54. 201 0
      router/router-exam-record.go
  55. 95 0
      router/router-exam.go
  56. 91 0
      router/router-plus.go
  57. 79 0
      router/router-question-bank.go
  58. 194 0
      router/router-subject.go
  59. 78 0
      router/router-term.go
  60. 87 0
      router/router-test-paper.go
  61. 218 0
      router/router-user.go
  62. 29 0
      router/router.go
  63. 1 0
      service/service-subject.go
  64. 73 0
      service/service-user.go
  65. 153 0
      test.sql
  66. 102 0
      utils/utils.go
  67. 10 0
      vo/class_vo.go
  68. 54 0
      vo/exam_record_vo.go
  69. 19 0
      vo/exam_vo.go
  70. 9 0
      vo/qustion_bank_vo.go
  71. 15 0
      vo/subject_vo.go
  72. 10 0
      vo/term_vo.go
  73. 34 0
      vo/ters_paper_vo.go
  74. 10 0
      vo/user_vo.go

+ 13 - 0
Dockerfile

@@ -0,0 +1,13 @@
+FROM alpine
+
+
+RUN  mkdir -p /project/file
+ADD   ./file /project/file
+ADD  ./exam /project
+ADD ./config-linux.yaml /project
+WORKDIR /project
+
+
+EXPOSE 3000
+
+ENTRYPOINT ["/project/exam"]

+ 7 - 0
build.bat

@@ -0,0 +1,7 @@
+set GOARCH=amd64
+set GOOS=linux
+go build -o exam ./cmd/
+
+docker rmi registry.cn-chengdu.aliyuncs.com/infish/exam:1.0.8
+docker build -t registry.cn-chengdu.aliyuncs.com/infish/exam:1.0.8 .
+docker push registry.cn-chengdu.aliyuncs.com/infish/exam:1.0.8

+ 0 - 0
build.sh


+ 14 - 0
cmd/config-linux.yaml

@@ -0,0 +1,14 @@
+port: 3000
+
+database:
+  name: mysql
+  # host: root:zyhd2022@tcp(177.7.0.13:3306)/exam?charset=utf8mb4&parseTime=True
+  host: root:zyhd2022@tcp(124.71.139.24:3306)/exam?charset=utf8mb4&parseTime=True
+
+  maxOpenConns: 100
+  maxIdleConns: 20
+mq:
+  name: nats
+  # host: nats://177.7.0.14:4222
+  host: nats://127.0.0.1:14301
+

+ 21 - 0
cmd/main.go

@@ -0,0 +1,21 @@
+package main
+
+import (
+	"exam_system/config"
+	"exam_system/middleware"
+	"exam_system/router"
+	"fmt"
+	"github.com/gin-gonic/gin"
+)
+
+
+func main() {
+	engine := gin.Default()
+
+	middleware.Middleware(engine)
+
+	// 路由配置
+	router.RouterEntry(engine)
+
+	engine.Run(fmt.Sprintf(":%d",config.Conf.Port))
+}

+ 14 - 0
config-linux.yaml

@@ -0,0 +1,14 @@
+port: 3000
+
+database:
+  name: mysql
+  # host: root:zyhd2022@tcp(177.7.0.13:3306)/exam?charset=utf8mb4&parseTime=True
+  host: root:zyhd2022@tcp(124.71.139.24:3306)/exam?charset=utf8mb4&parseTime=True
+
+  maxOpenConns: 100
+  maxIdleConns: 20
+mq:
+  name: nats
+  # host: nats://177.7.0.14:4222
+  host: nats://127.0.0.1:14301
+

+ 12 - 0
config.yaml

@@ -0,0 +1,12 @@
+port: 3000
+
+database:
+  name: mysql
+  host: root:zyhd2022@tcp(124.71.139.24:3306)/exam?charset=utf8mb4&parseTime=True
+  maxOpenConns: 100
+  maxIdleConns: 20
+mq:
+  name: nats
+  host: nats://127.0.0.1:14301
+  # host: nats://127.0.0.1:4222
+

+ 86 - 0
config/config.go

@@ -0,0 +1,86 @@
+package config
+
+import (
+	"fmt"
+	_ "github.com/go-sql-driver/mysql"
+	"github.com/jmoiron/sqlx"
+	"github.com/nats-io/nats.go"
+	"github.com/spf13/viper"
+	"log"
+	"runtime"
+)
+
+type Config struct {
+	Port     int       `json:"port,omitempty"`
+	DataBase *DataBase `json:"database,omitempty"`
+	MQ       *MQ       `json:"mq,omitempty"`
+}
+
+type DataBase struct {
+	Name         string `json:"name,omitempty"`
+	Host         string `json:"host,omitempty"`
+	MaxOpenConns int    `json:"maxOpenConns,omitempty"`
+	MaxIdleConns int    `json:"maxIdleConns,omitempty"`
+}
+
+type MQ struct {
+	Name string `json:"name,omitempty"`
+	Host string `json:"host,omitempty"`
+}
+
+var Conf *Config
+var DB *sqlx.DB
+var JS nats.JetStreamContext
+
+func InitConf() {
+	if runtime.GOOS=="linux"{
+		viper.SetConfigFile("./config-linux.yaml")
+	}else{
+		viper.SetConfigFile("./config.yaml")
+	}
+
+	if err := viper.ReadInConfig(); err != nil {
+		panic("Read file error:" + err.Error())
+	}
+	var config Config
+	if err := viper.Unmarshal(&config); err != nil {
+		panic("File exchange error:" + err.Error())
+	}
+	Conf = &config
+}
+
+func InitDataBase() {
+
+	var err error
+	DB, err = sqlx.Connect(Conf.DataBase.Name, Conf.DataBase.Host)
+	if err != nil {
+		log.Fatalf("connect DB failed, err:%v\n", err)
+		return
+	}
+	DB.SetMaxOpenConns(Conf.DataBase.MaxOpenConns)
+	DB.SetMaxIdleConns(Conf.DataBase.MaxOpenConns)
+}
+
+func InitMQ() {
+	conn, err := nats.Connect(Conf.MQ.Host)
+	if err != nil {
+		fmt.Println(err)
+	}
+	js, _ := conn.JetStream(nats.PublishAsyncMaxPending(256))
+	js.AddStream(&nats.StreamConfig{
+		Name:     "EXAMS",
+		Subjects: []string{"EXAMS.*"},
+	})
+	JS = js
+}
+
+func init() {
+	// 1、初始化配置
+	InitConf()
+
+	// 2、初始化数据库
+	InitDataBase()
+
+	// 3、初始化MQ
+	InitMQ()
+}

+ 14 - 0
dao/dao-base.go

@@ -0,0 +1,14 @@
+package dao
+
+import (
+	"exam_system/config"
+	"fmt"
+)
+
+func FindTotal(tableName string) int{
+
+	var count int
+	sqlStr := fmt.Sprintf("select count(id) from %s where delete_at is null",tableName)
+	config.DB.Get(&count, sqlStr)
+	return  count
+}

+ 162 - 0
dao/dao-class.go

@@ -0,0 +1,162 @@
+package dao
+
+import (
+	"database/sql"
+	"exam_system/config"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"exam_system/vo"
+	"github.com/jmoiron/sqlx"
+	"time"
+)
+
+func AddClass(c *entity.Class) *result.Result {
+
+	tx, err := config.DB.Beginx()
+	if err != nil {
+		tx.Commit()
+		return result.UNKNOW_ERROR.SetData(err)
+	}
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+		// 判断是否存在term
+		sqlStr := "select id from term where id = ? and delete_at is null limit 1"
+		var termId int64
+		err = tx.Get(&termId, sqlStr, c.TermId)
+		if err != nil {
+			if err == sql.ErrNoRows {
+				return result.DATA_NOT_FOUND
+			}
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+
+		// 插入
+		sqlStr = "insert into class(name,term_id,create_at,update_at) values (?,?,?,?)"
+		res, err := config.DB.Exec(sqlStr, c.Name, c.TermId, time.Now(), time.Now())
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+
+		id, _ := res.LastInsertId()
+
+		return result.SuccessResult(id)
+	})
+
+}
+
+func ClassDetail(id string) *result.Result {
+	sqlStr := `SELECT
+	c.id,
+	c.name,
+	c.term_id,
+	c.create_at,
+	c.update_at,
+	t.name term_name
+FROM
+	class c,
+	term t 
+WHERE
+	c.id =? 
+	AND c.delete_at IS NULL 
+	AND c.term_id = t.id`
+
+	var c vo.ClassVo
+	err := config.DB.Get(&c, sqlStr, id)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return result.DATA_NOT_FOUND
+		}
+		return result.UNKNOW_ERROR.SetData(err.Error())
+	}
+
+	return result.SuccessResult(&c)
+}
+
+func ClassList(page, size int, sort string, query map[string]interface{}) *result.Result {
+
+	parm := make([]interface{}, 0)
+	sqlStr := "select c.* from class c,term t where c.term_id = t.id and c.delete_at is null"
+
+	if query["term_id"] != "" {
+		sqlStr += `and t.id=? `
+		parm = append(parm, query["term_id"])
+	}
+	sqlStr += `order by create_at `
+
+	if sort == "desc" {
+		sqlStr += "desc "
+	}
+	sqlStr += "limit ?,?"
+	parm = append(parm, utils.PostionSize(page, size),size)
+
+	var ts []*vo.ClassVo
+	err := config.DB.Select(&ts, sqlStr, parm...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 获取总数
+	sqlStr = "select count(c.id) from class c "
+	parm = make([]interface{}, 0)
+	if query["term_id"] != "" {
+		sqlStr += `,term t where c.term_id = t.id and t.id=? and c.delete_at is null`
+		parm = append(parm, query["term_id"])
+	}
+
+	var total int
+	err = config.DB.Get(&total, sqlStr, parm...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return result.SUCCESS.SetData(result.NewPage(page,len(ts),total,ts))
+}
+
+func UpdateClass(t *entity.Class) *result.Result {
+	// 判断是否存在term
+	tx, err := config.DB.Beginx()
+	if err != nil {
+		return result.UNKNOW_ERROR.SetData(err)
+	}
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+		sqlStr := "select id from term where id = ? and delete_at is null limit 1"
+		var termId int64
+		err = tx.Get(&termId, sqlStr, t.TermId)
+		if err != nil {
+			if err == sql.ErrNoRows {
+				return result.DATA_NOT_FOUND
+			}
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+
+		sqlStr = `update  class set name=?,term_id=?,update_at=? where id=? and delete_at is  null `
+		var res sql.Result
+		res, err = config.DB.Exec(sqlStr, t.Name, t.TermId, time.Now(), t.Id)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+		affected, _ := res.RowsAffected()
+
+		return result.SuccessResult(result.NewResultChange(affected))
+	})
+
+}
+
+func DeleteClasss(ids []string) *result.Result {
+
+	sqlStr := "update class c left join user u on c.id = u.class_id set c.delete_at=?,u.delete_at=? where c.id in (?)"
+
+	query, param, err := sqlx.In(sqlStr, time.Now(),time.Now(),ids)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	res, err := config.DB.Exec(query, param...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+	affected, _ := res.RowsAffected()
+
+
+	return result.SUCCESS.SetData(result.NewResultChange(affected))
+}

+ 574 - 0
dao/dao-exam-record.go

@@ -0,0 +1,574 @@
+package dao
+
+import (
+	"database/sql"
+	"encoding/json"
+	"exam_system/config"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"exam_system/vo"
+	"fmt"
+	"strconv"
+	"time"
+
+	"github.com/gin-gonic/gin"
+	"github.com/jmoiron/sqlx"
+	uuid "github.com/satori/go.uuid"
+	"github.com/xuri/excelize/v2"
+)
+
+func AddExamRecord(er *entity.ExamRecord) *result.Result {
+
+	// 2、插入
+	uuId := uuid.NewV4().String()
+
+	sqlStr := "update exam_record er set create_at=?,update_at=?,token=?,ip=inet_aton(?),status=? where er.id = ? and er.user_id=?"
+
+	_, err := config.DB.Exec(sqlStr, time.Now(), time.Now(), uuId, er.Ip, entity.TESTING, er.Id, er.UserId)
+
+	if err != nil {
+		return result.UNKNOW_ERROR.SetData(err)
+	}
+
+	return result.SuccessResult(gin.H{
+		"id":    er.Id,
+		"token": uuId,
+	})
+}
+
+func ExamRecordDetail(id, uid int) *result.Result {
+
+	// 1、考试详情
+	sqlStr := `select e.name,e.create_at,e.update_at,e.duration,e.start_at,e.end_at,e.publish_at,tp.score,tp.pass_score,er.score user_score,timestampdiff(minute,er.create_at,er.update_at) user_take_time
+from user u left join  exam_record er on u.id = er.user_id
+	LEFT JOIN exam e ON e.id = er.exam_id
+	LEFT JOIN test_paper tp ON e.tp_id = tp.id
+	LEFT JOIN exam_class ec ON e.id = ec.exam_id
+	LEFT JOIN class c ON ec.class_id = c.id
+where er.id = ? and u.id = ? and e.delete_at is null `
+
+	var t vo.ExamRecordVo1
+	err := config.DB.Get(&t, sqlStr, id, uid)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return result.DATA_NOT_FOUND
+		}
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	if t.PublishAt.After(time.Now()) {
+		t.UserScore = nil
+	}
+
+	// 题目答案, 用户答案
+	sqlStr = "select er.answer from exam_record er where er.id = ? and er.delete_at is null"
+	var answerStr string
+	err = config.DB.Get(&answerStr, sqlStr, id)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	var avs []*vo.AnswerVo
+	err = json.Unmarshal([]byte(answerStr), &avs)
+	if err != nil {
+		return result.FORMATE_ERROR
+	}
+
+	for i := 0; i < len(avs); i++ {
+		var av vo.AnswerVo
+		sqlStr = "select * from subject where id = ?"
+		err = config.DB.Get(&av, sqlStr, avs[i].Id)
+		if err != nil {
+			if err == sql.ErrNoRows {
+				continue
+			}
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		if t.PublishAt.After(time.Now()) {
+			av.Answer = ""
+		}
+		av.OwnAnswer = avs[i].OwnAnswer
+		avs[i] = &av
+	}
+
+	return result.SuccessResult(gin.H{
+		"exam_detail": &t,
+		"subject":     &avs,
+	})
+}
+
+func ExamRecordList(page, size int, query map[string]interface{}, uid int) *result.Result {
+	// 1、获取list
+	sqlStr := `select er.id,e.name,e.create_at,e.update_at,e.duration,e.start_at,e.end_at,e.publish_at,inet_ntoa(er.ip) ip,tp.score,tp.pass_score,er.score user_score,timestampdiff(minute,er.create_at,er.update_at) user_take_time
+from user u left join  exam_record er on u.id = er.user_id
+	LEFT JOIN exam e ON e.id = er.exam_id
+	LEFT JOIN test_paper tp ON e.tp_id = tp.id
+	LEFT JOIN exam_class ec ON e.id = ec.exam_id
+	LEFT JOIN class c ON ec.class_id = c.id
+where u.id = ? and e.delete_at is null and er.status!=0 `
+
+	params := []interface{}{uid}
+	if query["name"] != nil {
+		sqlStr += "and e.name like ? "
+		params = append(params, "%"+query["name"].(string)+"%")
+	}
+	sqlStr += "group by er.id "
+
+	if query["create_at"] != nil {
+		if query["create_at"] == "desc" {
+			sqlStr += "order by create_at desc "
+		} else {
+			sqlStr += "order by create_at asc "
+		}
+	} else if query["user_take_time"] != nil {
+		if query["user_take_time"] == "desc" {
+			sqlStr += "order by user_take_time desc "
+		} else {
+			sqlStr += "order by user_take_time asc "
+		}
+	} else if query["user_score"] != nil {
+		if query["user_score"] == "desc" {
+			sqlStr += "order by user_score desc "
+		} else {
+			sqlStr += "order by user_score asc "
+		}
+	} else {
+		sqlStr += "order by create_at desc "
+	}
+
+	sqlStr += "limit ?,?"
+	params = append(params, utils.PostionSize(page, size), size)
+
+	var evos []*vo.ExamRecordVo1
+	err := config.DB.Select(&evos, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 2、总数
+	sqlStr = `select count(distinct(er.id))
+from user u left join  exam_record er on u.id = er.user_id
+	LEFT JOIN exam e ON e.id = er.exam_id
+	LEFT JOIN test_paper tp ON e.tp_id = tp.id
+	LEFT JOIN exam_class ec ON e.id = ec.exam_id
+	LEFT JOIN class c ON ec.class_id = c.id
+where u.id = ? and e.delete_at is null and er.status!=0 `
+	params = []interface{}{uid}
+	if query["name"] != nil {
+		sqlStr += "and e.name like ? "
+		params = append(params, "%"+query["name"].(string)+"%")
+	}
+	sqlStr += "group by er.id"
+	for _, e := range evos {
+		if e.PublishAt.After(time.Now()) {
+			e.UserScore = nil
+		}
+	}
+
+	var total int
+	err = config.DB.Get(&total, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return result.SUCCESS.SetData(result.NewPage(page, len(evos), total, evos))
+}
+
+func UpdateExamRecord(erv *vo.ExamRecordVo) *result.Result {
+
+	tx, err := config.DB.Beginx()
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+		// 1、查询是否存在该记录
+		sqlStr := `select er.id, er.exam_id
+from exam_record er
+         left join exam e on e.id = er.exam_id
+where er.id = ?
+  and er.token = ?
+and er.update_at - er.create_at <= e.duration
+		and e.end_at < ?
+`
+
+		err = tx.Get(erv, sqlStr, erv.Id, erv.Token)
+		if err != nil {
+			if err == sql.ErrNoRows {
+				return result.DATA_NOT_FOUND.SetMsg("该场考试已过期")
+			}
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		sqlStr = "update exam_record set token = null,status = ? where id = ?"
+		var res sql.Result
+		res, err = tx.Exec(sqlStr, entity.TESTED, erv.Id)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 2、查询出所需id
+		sqlStr = "select c.sub_ids from chapter c left join exam e on  c.tp_id = e.tp_id where e.id=?"
+
+		var subIdStrs []string
+		err = tx.Select(&subIdStrs, sqlStr, erv.ExamId)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		subIds := make([]*vo.SubIdsVo, 0)
+		for _, subIdStr := range subIdStrs {
+			var subIdsVo []*vo.SubIdsVo
+			err = json.Unmarshal([]byte(subIdStr), &subIdsVo)
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+			subIds = append(subIds, subIdsVo...)
+		}
+
+		// 2.1 构造一个key为id的 map
+		subIdMap := make(map[int]int, 0)
+		for _, subId := range subIds {
+			subIdMap[*subId.Id] = *subId.Score
+		}
+
+		// 2、 批量查询id
+		toatalScore := 0
+		for _, answerVo := range erv.AnswerVo {
+			// 判断提交的试题是否在考试范围内
+			if _, ok := subIdMap[*answerVo.Id]; !ok {
+				return result.UNKNOW_ERROR.SetMsg("不存在该试题")
+			}
+
+			var score int
+			sqlStr = "select count(id) from subject where id=? and answer=?"
+			err = tx.Get(&score, sqlStr, answerVo.Id, utils.SubjectSort(answerVo.OwnAnswer))
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+			toatalScore += score * subIdMap[*answerVo.Id]
+			delete(subIdMap, *answerVo.Id)
+		}
+		if len(subIdMap) != 0 {
+			return result.UNKNOW_ERROR.SetMsg("试题提交不完整")
+		}
+
+		var anwserData []byte
+		anwserData, err = json.Marshal(erv.AnswerVo)
+
+		// 插入记录表
+		sqlStr = "update  exam_record set score=?,answer=?,update_at=? where id=? and delete_at is null "
+
+		res, err = tx.Exec(sqlStr, toatalScore, string(anwserData), time.Now(), erv.Id)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+		affected, _ := res.RowsAffected()
+
+		return result.SuccessResult(result.NewResultChange(affected))
+	})
+}
+
+func DeleteExamRecords(ids []string) *result.Result {
+	parm := []interface{}{time.Now()}
+	parmStr := ""
+	for k, id := range ids {
+		parm = append(parm, id)
+		if k == (len(ids) - 1) {
+			parmStr += "?"
+		} else {
+			parmStr += "?,"
+		}
+	}
+
+	sqlStr := fmt.Sprintf("update exam_record set delete_at=? where id in (%s)", parmStr)
+
+	res, err := config.DB.Exec(sqlStr, parm...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+	affected, _ := res.RowsAffected()
+
+	return result.SUCCESS.SetData(result.NewResultChange(affected))
+}
+
+func AdminExamRecordDetail(id int) *result.Result {
+
+	// 1、考试详情
+	//	sqlStr := `select e.name,e.create_at,e.update_at,e.duration,e.start_at,e.end_at,e.publish_at,tp.score,tp.pass_score,er.score user_score,timestampdiff(minute,er.create_at,er.update_at) user_take_time
+	//from exam_record er left join exam e on er.exam_id = e.id
+	//         left join test_paper tp on e.tp_id = tp.id
+	//         left join exam_class ec on e.id = ec.exam_id
+	//         left join class c on ec.class_id = c.id
+	//         left join user u on u.class_id = c.id
+	//		 where er.id=? and e.delete_at is null
+	//		 GROUP BY er.id`
+	//
+	//	var t vo.ExamRecordVo1
+	//	err := config.DB.Get(&t, sqlStr, id)
+	//	if err != nil {
+	//		if err == sql.ErrNoRows {
+	//			return result.DATA_NOT_FOUND
+	//		}
+	//		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	//	}
+
+	// 题目答案, 用户答案
+	sqlStr := "select er.answer from exam_record er where er.id = ? and er.delete_at is null"
+	var answerStr string
+	err := config.DB.Get(&answerStr, sqlStr, id)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	var avs []*vo.AnswerVo
+	err = json.Unmarshal([]byte(answerStr), &avs)
+	if err != nil {
+		return result.FORMATE_ERROR
+	}
+
+	for i := 0; i < len(avs); i++ {
+		var av vo.AnswerVo
+		sqlStr = "select * from subject where id = ?"
+		err = config.DB.Get(&av, sqlStr, avs[i].Id)
+		if err != nil {
+			if err.Error() == "sql: no rows in result set" {
+				continue
+			}
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		av.OwnAnswer = avs[i].OwnAnswer
+		if avs[i].OwnAnswer == "-1" {
+			av.OwnAnswer = ""
+		}
+
+		avs[i] = &av
+	}
+
+	return result.SuccessResult(gin.H{
+		//"exam_detail": &t,
+		"subject": &avs,
+	})
+}
+
+func AdminExamRecordList(page, size int, query map[string]interface{}, examId int) *result.Result {
+	// 1、获取list
+	sqlStr := `select er.id,
+       e.name,
+       e.create_at,
+       e.update_at,
+       e.duration,
+       e.start_at,
+       e.end_at,
+       e.publish_at,
+       tp.score,
+       tp.pass_score,
+       er.score                                          user_score,
+       u.username,
+       c.name classname,
+       timestampdiff(minute, er.create_at, er.update_at) user_take_time
+from exam_record er
+	LEFT JOIN exam e ON er.exam_id = e.id
+	LEFT JOIN test_paper tp ON e.tp_id = tp.id
+	LEFT JOIN user u on er.user_id = u.id
+	LEFT JOIN class c on u.class_id = c.id
+where e.id = ?
+  and e.delete_at is null `
+
+	params := []interface{}{examId}
+	if query["name"] != nil {
+		sqlStr += "and e.name like ? "
+		params = append(params, "%"+query["name"].(string)+"%")
+	}
+
+	if query["class_id"] != nil {
+		sqlStr += "and c.id = ? "
+		params = append(params, query["class_id"])
+	}
+
+	if query["create_at"] != nil {
+		if query["create_at"] == "desc" {
+			sqlStr += "order by create_at desc "
+		} else {
+			sqlStr += "order by create_at asc "
+		}
+	} else if query["user_take_time"] != nil {
+		if query["user_take_time"] == "desc" {
+			sqlStr += "order by user_take_time desc "
+		} else {
+			sqlStr += "order by user_take_time asc "
+		}
+	} else if query["user_score"] != nil {
+		if query["user_score"] == "desc" {
+			sqlStr += "order by user_score desc "
+		} else {
+			sqlStr += "order by user_score asc "
+		}
+	} else {
+		sqlStr += "order by create_at desc "
+	}
+
+	sqlStr += "limit ?,?"
+	params = append(params, utils.PostionSize(page, size), size)
+
+	var evos []*vo.ExamRecordVo1
+	err := config.DB.Select(&evos, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 2、总数
+	sqlStr = `select count(er.id)
+from exam_record er
+	LEFT JOIN exam e ON er.exam_id = e.id
+	LEFT JOIN test_paper tp ON e.tp_id = tp.id
+	LEFT JOIN user u on er.user_id = u.id
+	LEFT JOIN class c on u.class_id = c.id
+where e.id = ? and e.delete_at is null `
+	params = []interface{}{examId}
+	if query["name"] != nil {
+		sqlStr += "and e.name like ? "
+		params = append(params, "%"+query["name"].(string)+"%")
+	}
+	if query["class_id"] != nil {
+		sqlStr += "and c.id = ? "
+		params = append(params, query["class_id"])
+	}
+
+	var total int
+	err = config.DB.Get(&total, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return result.SUCCESS.SetData(result.NewPage(page, len(evos), total, evos))
+}
+
+func AdminDownloadExamRecord(file *excelize.File, id string) *result.Result {
+	// 1、获取list
+	sqlStr := `SELECT
+    e.name exam_name,
+	u.username,
+	CONCAT(t.name,c.name,'>') term_class,
+	u.username name,
+	u.sid,
+	er.create_at,
+	er.update_at,
+	inet_ntoa( er.ip ) ip,
+	er.score user_score,
+	timestampdiff( MINUTE, er.create_at, er.update_at ) user_take_time 
+FROM
+    exam e left join exam_record er on e.id = er.exam_id
+	LEFT JOIN user u  ON u.id = er.user_id 
+	LEFT JOIN class c ON u.class_id = c.id
+	LEFT JOIN term  t on c.term_id = t.id
+WHERE
+	u.delete_at IS NULL and e.id = ?
+`
+
+	var evos []*vo.ExamRecordVo2
+	err := config.DB.Select(&evos, sqlStr, id)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return result.DATA_NOT_FOUND
+		}
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	file.SetCellValue("Sheet1", "A1", evos[0].ExamName)
+	i := 4
+	for _, evo := range evos {
+
+		if evo.TermClass != nil {
+			file.SetCellValue("Sheet1", "A"+strconv.Itoa(i), *evo.TermClass)
+		}
+		file.SetCellValue("Sheet1", "B"+strconv.Itoa(i), evo.Name)
+		file.SetCellValue("Sheet1", "C"+strconv.Itoa(i), evo.Sid)
+		file.SetCellValue("Sheet1", "D"+strconv.Itoa(i), evo.CreateAt.Format("2006-01-02 15:04:05"))
+		file.SetCellValue("Sheet1", "E"+strconv.Itoa(i), evo.UpdateAt.Format("2006-01-02 15:04:05"))
+		if evo.UserScore != nil {
+			file.SetCellValue("Sheet1", "F"+strconv.Itoa(i), *evo.UserScore)
+		}
+		i++
+	}
+
+	return result.SUCCESS
+}
+
+func UpdateExamRecordTask(examId int) *result.Result {
+	tx, err := config.DB.Beginx()
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+		var ervs []*vo.ExamRecordVo
+		// 1、查询是否存在该记录
+		sqlStr := `select er.id, er.exam_id
+from exam_record er
+         left join exam e on e.id = er.exam_id
+where e.id = ? and e.end_at < ?
+`
+
+		err = tx.Select(&ervs, sqlStr, examId, time.Now())
+		if err != nil {
+			if err == sql.ErrNoRows {
+				return result.SUCCESS
+			}
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		sqlStr = "update exam_record set token = null,status = ? where id = ?"
+		var updateEr *sql.Stmt
+		updateEr, err = tx.Prepare(sqlStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		sqlStr = "update  exam_record set score=?,answer=?,update_at=? where id=? and delete_at is null "
+		var updateErScore *sql.Stmt
+		updateErScore, err = tx.Prepare(sqlStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		for _, erv := range ervs {
+
+			_, err = updateEr.Exec(entity.TIMEOUT, erv.Id)
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+
+			// 2、查询出所需id
+			sqlStr = "select c.sub_ids from chapter c left join exam e on  c.tp_id = e.tp_id where e.id=?"
+
+			var subIdStrs []string
+			err = tx.Select(&subIdStrs, sqlStr, erv.ExamId)
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+
+			subIds := make([]*vo.SubIdsVo, 0)
+			for _, subIdStr := range subIdStrs {
+				var subIdsVo []*vo.SubIdsVo
+				err = json.Unmarshal([]byte(subIdStr), &subIdsVo)
+				if err != nil {
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+				subIds = append(subIds, subIdsVo...)
+			}
+
+			// 2、 批量查询id
+			toatalScore := 0
+			avos := make([]*vo.AnswerVo, 0, len(subIds))
+			for _, subId := range subIds {
+				avo := vo.AnswerVo{Id: subId.Id, OwnAnswer: "-1"}
+				avos = append(avos, &avo)
+			}
+
+			var anwserData []byte
+			anwserData, err = json.Marshal(avos)
+
+			// 插入记录表
+			_, err = updateErScore.Exec(toatalScore, string(anwserData), time.Now(), erv.Id)
+			if err != nil {
+				return result.UNKNOW_ERROR.SetData(err)
+			}
+		}
+		return result.SUCCESS
+	})
+}

+ 457 - 0
dao/dao-exam.go

@@ -0,0 +1,457 @@
+package dao
+
+import (
+	"database/sql"
+	"encoding/json"
+	"exam_system/config"
+	"exam_system/entity"
+
+	"exam_system/result"
+	"exam_system/utils"
+	"exam_system/vo"
+	"github.com/jmoiron/sqlx"
+	"strings"
+	"time"
+)
+
+func ExamInfoList(page, size int, sort string, query map[string]interface{}, uid int) *result.Result {
+	// 1、获取list
+	sqlStr := `select er.id, e.name,e.status, e.start_at, e.end_at, e.duration, e.publish_at,e.tp_id,er.status state,tp.score,tp.pass_score
+from exam_record er left join user u on er.user_id = u.id 
+		left join exam e on e.id = er.exam_id
+         left join test_paper tp on e.tp_id = tp.id
+         left join exam_class ec on e.id = ec.exam_id
+         left join class c on ec.class_id = c.id
+where er.status =0 and u.id = ? `
+
+	params := []interface{}{uid}
+	if query["name"] != nil {
+		sqlStr += "and e.name like ? "
+		params = append(params, "%"+query["name"].(string)+"%")
+	}
+	// 0 进行中 1已结束
+	if query["status"] != nil {
+		if query["status"].(float64) == 0 {
+			sqlStr += "and e.end_at < ? "
+		} else {
+			sqlStr += "and e.end_at > ? "
+		}
+		params = append(params, time.Now())
+	}
+	sqlStr += "group by er.id "
+
+	if sort == "asc" {
+		sqlStr += "order by e.create_at asc "
+	} else {
+		sqlStr += "order by e.create_at desc "
+	}
+
+	sqlStr += "limit ?,?"
+	params = append(params, utils.PostionSize(page, size), size)
+
+	var evo []*vo.ExamVo1
+	err := config.DB.Select(&evo, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 2、总数
+	sqlStr = `select count(er.id)
+from exam_record er left join user u on er.user_id = u.id 
+		left join exam e on e.id = er.exam_id
+         left join test_paper tp on e.tp_id = tp.id
+         left join exam_class ec on e.id = ec.exam_id
+         left join class c on ec.class_id = c.id
+where er.status =0 and u.id = ? `
+	params = []interface{}{uid}
+	if query["name"] != nil {
+		sqlStr += "and e.name like ? "
+		params = append(params, "%"+query["name"].(string)+"%")
+	}
+	// 0 进行中 1已结束
+	if query["status"] != nil {
+		if query["status"].(float64) == 0 {
+			sqlStr += "and e.end_at < ? "
+		} else {
+			sqlStr += "and e.end_at > ? "
+		}
+		params = append(params, time.Now())
+	}
+
+	var total int
+	err = config.DB.Get(&total, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return result.SUCCESS.SetData(result.NewPage(page, len(evo), total, evo))
+}
+
+func ExamInfoList1(page, size int, sort string, query map[string]interface{}, uid int) *result.Result {
+	// 1、获取list
+	sqlStr := `select e.*,tp.score,tp.pass_score
+from exam e
+         left join test_paper tp on e.tp_id = tp.id
+         left join exam_class ec on e.id = ec.exam_id
+         left join class c on ec.class_id = c.id
+         left join user u on u.class_id = c.id
+where u.id = ? `
+
+	params := []interface{}{uid}
+	if query["name"] != nil {
+		sqlStr += "and e.name like ? "
+		params = append(params, "%"+query["name"].(string)+"%")
+	}
+	// 0 进行中 1已结束
+	if query["status"] != nil {
+		if query["status"].(float64) == 0 {
+			sqlStr += "and e.end_at < ? "
+		} else {
+			sqlStr += "and e.end_at > ? "
+		}
+		params = append(params, time.Now())
+	}
+	sqlStr += "group by e.id "
+
+	if sort == "asc" {
+		sqlStr += "order by e.create_at asc "
+	} else {
+		sqlStr += "order by e.create_at desc "
+	}
+
+	sqlStr += "limit ?,?"
+	params = append(params, utils.PostionSize(page, size), size)
+
+	var evo []*vo.ExamVo1
+	err := config.DB.Select(&evo, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 2、总数
+	sqlStr = `select count(e.id)
+from exam e
+         left join test_paper tp on e.tp_id = tp.id
+         left join exam_class ec on e.id = ec.exam_id
+         left join class c on ec.class_id = c.id
+         left join user u on u.class_id = c.id
+where u.id = ? `
+	params = []interface{}{uid}
+	if query["name"] != nil {
+		sqlStr += "and e.name like ? "
+		params = append(params, "%"+query["name"].(string)+"%")
+	}
+	// 0 进行中 1已结束
+	if query["status"] != nil {
+		if query["status"].(float64) == 0 {
+			sqlStr += "and e.end_at < ? "
+		} else {
+			sqlStr += "and e.end_at > ? "
+		}
+		params = append(params, time.Now())
+	}
+
+	var total int
+	err = config.DB.Get(&total, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return result.SUCCESS.SetData(result.NewPage(page, len(evo), total, evo))
+}
+
+func AddExam(e *entity.Exam) *result.Result {
+
+	tx, err := config.DB.Beginx()
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+		// 1、获取考生人数
+		classIds := strings.Split(e.ClassIds, ",")
+		var users []*entity.User
+		sqlStr := "select id from user where class_id in (?)"
+		var query string
+		var args []interface{}
+		query, args, err = sqlx.In(sqlStr, classIds)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+		query = tx.Rebind(query)
+		err = tx.Select(&users, query, args...)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 2、插入
+		sqlStr = "insert into exam(name,create_at,update_at,start_at,end_at,publish_at,duration,status,tp_id,user_count) values (?,?,?,?,?,?,?,?,?,?)"
+		var res sql.Result
+		res, err = tx.Exec(sqlStr, e.Name, time.Now(), time.Now(), e.StartAt, e.EndAt, e.PublishAt, e.Duration, e.Status, e.TpId, len(users))
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		examId, _ := res.LastInsertId()
+		e.Id = int(examId)
+
+		// 3、插入考试班级表
+		sqlStr = "insert into exam_class(exam_id,class_id,create_at,update_at) values (?,?,?,?)"
+		var stmt *sql.Stmt
+		stmt, err = tx.Prepare(sqlStr)
+		for _, classId := range classIds {
+			_, err = stmt.Exec(examId, classId, time.Now(), time.Now())
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+		}
+
+		// 4、插入考试记录表
+		sqlStr = "insert into exam_record(create_at, update_at, user_id, exam_id,status) values (?,?,?,?,?)"
+		stmt, err = tx.Prepare(sqlStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		for _, user := range users {
+			res, err = stmt.Exec(time.Now(), time.Now(), user.ID, examId, entity.NOT_TESTED)
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+		}
+
+		var data []byte
+		data, err = json.Marshal(&e)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		// 用来判断考试过期的
+		config.JS.PublishAsync("EXAMS.info", data)
+
+		return result.SuccessResult(examId)
+	})
+}
+
+func ExamDetail(id string) *result.Result {
+
+	// 1.查询考试
+	sqlStr := "select e.*,t.name tp_name,t.score from exam e left join test_paper t on e.tp_id=t.id where e.id=? and e.delete_at is null"
+
+	var e vo.ExamVo
+	err := config.DB.Get(&e, sqlStr, id)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return result.DATA_NOT_FOUND
+		}
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 2.查询班级
+	var classes []*entity.Class
+	sqlStr = `SELECT
+	c.*
+FROM
+	exam e
+	LEFT JOIN exam_class ec ON e.id = ec.exam_id
+	LEFT JOIN class c ON c.id = ec.class_id 
+WHERE
+	e.id = ? and c.delete_at is null
+GROUP BY
+	c.id
+	`
+	err = config.DB.Select(&classes, sqlStr, id)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+	e.Classes = classes
+
+	return result.SuccessResult(e)
+}
+
+func ExamList(page, size int, sort string, query map[string]interface{}) *result.Result {
+	sqlStr := "select e.*,t.name tp_name,t.score from exam e left join test_paper t on e.tp_id=t.id where e.delete_at is null "
+	params := make([]interface{}, 0)
+
+	if query["name"] != nil {
+		params = append(params, "%"+query["name"].(string)+"%")
+		sqlStr += "and e.name like ? "
+	}
+
+	if query["start_at"] != nil {
+		params = append(params, query["start_at"])
+		sqlStr += "and e.start_at >= ? "
+	}
+
+	if query["end_at"] != nil {
+		params = append(params, query["end_at"])
+		sqlStr += "and e.end_at <= ? "
+	}
+
+	sqlStr += "group by e.id order by e.create_at "
+	if sort == "asc" {
+		sqlStr += "asc "
+	} else {
+		sqlStr += "desc "
+	}
+	sqlStr += "limit ?,?"
+	params = append(params, utils.PostionSize(page, size), size)
+
+	var evs []*vo.ExamVo
+	err := config.DB.Select(&evs, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 2.查询班级
+	for _, ev := range evs {
+		var classes []*entity.Class
+		sqlStr = `SELECT
+	c.*
+FROM
+	exam e
+	LEFT JOIN exam_class ec ON e.id = ec.exam_id
+	LEFT JOIN class c ON c.id = ec.class_id 
+WHERE
+	e.id = ? and c.delete_at is null and c.id is not null
+GROUP BY
+	c.id 
+	`
+		err = config.DB.Select(&classes, sqlStr, ev.Id)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		ev.Classes = classes
+	}
+
+	// 总数
+	sqlStr = "select count(e.id) from exam e left join test_paper t on e.tp_id=t.id where e.delete_at is null "
+	params = make([]interface{}, 0)
+
+	if query["name"] != nil {
+		params = append(params, "%"+query["name"].(string)+"%")
+		sqlStr += "and e.name like ? "
+	}
+
+	if query["start_at"] != nil {
+		params = append(params, query["start_at"])
+		sqlStr += "and e.start_at = ? "
+	}
+
+	if query["end_at"] != nil {
+		params = append(params, query["end_at"])
+		sqlStr += "and e.end_at = ? "
+	}
+	var total int
+	err = config.DB.Get(&total, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return result.SUCCESS.SetData(result.NewPage(page, len(evs), total, evs))
+}
+
+func UpdateExam(e *entity.Exam) *result.Result {
+
+	tx, err := config.DB.Beginx()
+
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+		// 1、获取考生人数
+		classIds := strings.Split(e.ClassIds, ",")
+		var users []*entity.User
+		sqlStr := "select id,class_id from user where class_id in (?)"
+		var query string
+		var args []interface{}
+		query, args, err = sqlx.In(sqlStr, classIds)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err.Error())
+		}
+		query = config.DB.Rebind(query)
+		err = config.DB.Select(&users, query, args...)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 2、更新
+		sqlStr = "update  exam set name=?,update_at=?,start_at=?,end_at=?,publish_at=?,duration=?,status=?,tp_id=?,user_count=? where id=? and delete_at is null "
+
+		var res sql.Result
+		res, err = config.DB.Exec(sqlStr, e.Name, time.Now(), e.StartAt, e.EndAt, e.PublishAt, e.Duration, e.Status, e.TpId, len(users), e.Id)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+		affected, _ := res.RowsAffected()
+
+		// 3、删除不是该班级学生的考试记录(班级 用户)
+		sqlStr = `DELETE er FROM exam_record er,exam_class ec WHERE er.exam_id = ec.exam_id and ec.class_id not in (?) and er.exam_id=?`
+		query, args, err = sqlx.In(sqlStr, classIds, e.Id)
+		_, err = tx.Exec(query, args...)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+
+		// 4、插入考试班级表
+		// 4.1、 先删除所有关联
+		sqlStr = "delete from exam_class where exam_id = ?"
+		res, err = tx.Exec(sqlStr, e.Id)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+
+		// 4.2、 重新建立关联
+		sqlStr = "insert into exam_class(exam_id,class_id,create_at,update_at) values (?,?,?,?)"
+		var stmt *sql.Stmt
+		stmt, err = tx.Prepare(sqlStr)
+		classMap := map[string]byte{}
+		for _, classId := range classIds {
+			classMap[classId] = 0
+			_, err = stmt.Exec(e.Id, classId, time.Now(), time.Now())
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+		}
+
+		// 4、插入考试记录表,如果user的classid存在则(不管,或者插入),不存在删除
+		sqlStr = "insert into exam_record(create_at, update_at, user_id, exam_id,status) values (?,?,?,?,?) "
+		stmt, err = tx.Prepare(sqlStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		for _, user := range users {
+			sqlStr = "select id from exam_record er where user_id = ? limit 1"
+			var userId int64
+			err = tx.Get(&userId, sqlStr, user.ID)
+			if err != nil && err != sql.ErrNoRows {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+			if userId == 0 {
+				res, err = stmt.Exec(time.Now(), time.Now(), user.ID, e.Id, entity.NOT_TESTED)
+				if err != nil {
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+			}
+		}
+
+		var data []byte
+		data, err = json.Marshal(&e)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		// 用来判断考试过期的
+		config.JS.PublishAsync("EXAMS.info", data)
+
+		return result.SuccessResult(result.NewResultChange(affected))
+	})
+}
+
+func DeleteExams(ids []string) *result.Result {
+
+	sqlStr := "update exam set delete_at=? where id in (?)"
+
+	query, param, err := sqlx.In(sqlStr, time.Now(), ids)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	res, err := config.DB.Exec(query, param...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	affected, _ := res.RowsAffected()
+	return result.SUCCESS.SetData(result.NewResultChange(affected))
+}

+ 30 - 0
dao/dao-perm.go

@@ -0,0 +1,30 @@
+package dao
+
+import (
+	"exam_system/config"
+	"exam_system/entity"
+	"exam_system/result"
+	"github.com/jmoiron/sqlx"
+)
+
+func FindPermByRoles(roleIds []int) *result.Result {
+	sqlStr := `
+	SELECT
+	p.id,
+	p.path,
+	p.method,
+	p.name
+FROM
+	role r, perm_role pr , perm p
+	where r.id in (?) and  pr.role_id = r.id and  pr.perm_id = p.id
+	GROUP BY p.id;`
+
+	query, args, _ := sqlx.In(sqlStr, roleIds)
+	var r []*entity.Perm
+	query = config.DB.Rebind(query)
+	err := config.DB.Select(&r, query, args...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+	return result.SuccessResult(r)
+}

+ 124 - 0
dao/dao-question-bank.go

@@ -0,0 +1,124 @@
+package dao
+
+import (
+	"database/sql"
+	"exam_system/config"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"exam_system/vo"
+	"fmt"
+	"time"
+)
+
+func AddQuestionBank(t *entity.QuestionBank) *result.Result {
+
+	sqlStr := "insert into question_bank(name,status,description,create_at,update_at) values (?,?,?,?,?)"
+	res, err := config.DB.Exec(sqlStr, t.Name, t.Status, t.Description, time.Now(), time.Now())
+	if err != nil {
+		return result.UNKNOW_ERROR.SetData(err)
+	}
+
+	id, _ := res.LastInsertId()
+
+	return result.SuccessResult(id)
+}
+
+func QuestionBankDetail(id string) *result.Result {
+	sqlStr := "select q.*,count(s.id) sub_num from question_bank q left join subject s on q.id=s.qb_id where q.id=? and q.delete_at is null group by q.id"
+
+	var t vo.QuestionBankVo
+	err := config.DB.Get(&t, sqlStr, id)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return result.DATA_NOT_FOUND
+		}
+		return result.UNKNOW_ERROR.SetData(err.Error())
+	}
+
+	return result.SuccessResult(&t)
+}
+
+func QuestionBankList(page, size int, sort string, query map[string]interface{}) *result.Result {
+
+
+	// 1、查询列表
+	sqlStr := "select q.*,count( s.id ) sub_num  from question_bank q left join subject s on q.id=s.qb_id and s.delete_at is null where q.delete_at is null "
+
+	params := make([]interface{}, 0)
+	if query["status"] != nil {
+		sqlStr += "and q.status = ? "
+		params = append(params, query["status"])
+	}
+	if query["key"] != nil {
+		sqlStr += "and q.name like ? "
+		params = append(params, "%"+query["key"].(string)+"%")
+	}
+
+	sqlStr += "group by q.id  order by create_at "
+	if sort == "desc" {
+		sqlStr += "desc "
+	}
+	sqlStr += "limit ?,?"
+
+	params = append(params, utils.PostionSize(page,size), size)
+	var ts []*vo.QuestionBankVo
+	err := config.DB.Select(&ts, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 3、查询所有
+	totalStr := "select count(id) from question_bank where delete_at is null "
+	params = make([]interface{}, 0)
+	if query["status"] != nil {
+		totalStr += "and status = ? "
+		params = append(params, query["status"])
+	}
+	if query["key"] != nil {
+		totalStr += "and name like ? "
+		params = append(params, "%"+query["key"].(string))
+	}
+	var total int
+	err = config.DB.Get(&total, totalStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return result.SUCCESS.SetData(result.NewPage(page,len(ts),total,ts))
+}
+
+func UpdateQuestionBank(q *entity.QuestionBank) *result.Result {
+	sqlStr := "update  question_bank set name=?,description=?,status=?,update_at=? where id=? and delete_at is  null "
+
+	res, err := config.DB.Exec(sqlStr, q.Name, q.Description, q.Status, time.Now(), q.Id)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetData(err)
+	}
+	affected, _ := res.RowsAffected()
+
+	return result.SuccessResult(result.NewResultChange(affected))
+}
+
+func DeleteQuestionBanks(ids []string) *result.Result {
+	parm := []interface{}{time.Now()}
+	parmStr := ""
+	for k, id := range ids {
+		parm = append(parm, id)
+		if k == (len(ids) - 1) {
+			parmStr += "?"
+		} else {
+			parmStr += "?,"
+		}
+	}
+
+	sqlStr := fmt.Sprintf("update question_bank set delete_at=? where id in (%s)", parmStr)
+
+	res, err := config.DB.Exec(sqlStr, parm...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+	affected, _ := res.RowsAffected()
+
+	return result.SUCCESS.SetData(result.NewResultChange(affected))
+}

+ 43 - 0
dao/dao-role.go

@@ -0,0 +1,43 @@
+package dao
+
+import (
+	"exam_system/config"
+	"exam_system/entity"
+	"fmt"
+)
+
+func FindRoleByUserId(id int) []*entity.Role {
+	sqlStr := `
+	SELECT
+	r.id,
+	r.name
+FROM
+	user u
+	LEFT JOIN user_role ur ON ur.user_id = u.id
+	LEFT JOIN role r ON ur.role_id = r.id
+	where u.id=?
+	GROUP BY r.id;
+`
+
+	var r []*entity.Role
+
+	err := config.DB.Select(&r, sqlStr, id)
+	if err != nil {
+		fmt.Printf("get failed, err:%v\n", err)
+		return nil
+	}
+	return r
+}
+
+func FindRoleByName(roleName string) *entity.Role {
+	sqlStr := "select id from role where name=? limit 1"
+
+	var r entity.Role
+	err := config.DB.Get(&r, sqlStr, roleName)
+	if err != nil {
+		fmt.Printf("get failed, err:%v\n", err)
+		return nil
+	}
+
+	return &r
+}

+ 265 - 0
dao/dao-subject.go

@@ -0,0 +1,265 @@
+package dao
+
+import (
+	"database/sql"
+	"exam_system/config"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"exam_system/vo"
+	"fmt"
+	"github.com/jmoiron/sqlx"
+	"github.com/xuri/excelize/v2"
+	"mime/multipart"
+	"strconv"
+	"strings"
+	"time"
+)
+
+func AddSubject(s *entity.Subject) *result.Result {
+
+	sqlStr := "insert into subject(question,type,opt_a,opt_b,opt_c,opt_d,answer,analysis,status,qb_id,create_at,update_at) values (?,?,?,?,?,?,?,?,?,?,?,?)"
+	res, err := config.DB.Exec(sqlStr, s.Question, s.Type, s.OptA, s.OptB, s.OptC, s.OptD, utils.SubjectSort(s.Answer), s.Analysis, s.Status, s.QbId, time.Now(), time.Now())
+	if err != nil {
+		return result.UNKNOW_ERROR.SetData(err)
+	}
+
+	id, _ := res.LastInsertId()
+
+	return result.SuccessResult(id)
+}
+
+func SubjectDetail(id string) *result.Result {
+	sqlStr := "select * from subject where id=? and delete_at is null"
+
+	var t entity.Subject
+	err := config.DB.Get(&t, sqlStr, id)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return result.DATA_NOT_FOUND
+		}
+		return result.UNKNOW_ERROR.SetData(err.Error())
+	}
+
+	return result.SuccessResult(&t)
+}
+
+func SubjectList(page, size int, sort string, query map[string]interface{}) *result.Result {
+
+	// 1、获取列表
+	parms := make([]interface{}, 0)
+	sqlStr := "select * from subject where delete_at is null "
+
+	if query["key"] != nil {
+		sqlStr += "and question like ? "
+		parms = append(parms, "%"+query["key"].(string))
+	}
+	if query["qb_id"] != nil {
+		sqlStr += "and qb_id = ? "
+		parms = append(parms, query["qb_id"])
+	}
+	if query["status"] != nil {
+		sqlStr += "and status=? "
+		parms = append(parms, query["status"])
+	}
+	if query["type"]!=nil{
+		sqlStr += "and type=? "
+		parms = append(parms, query["type"])
+	}
+
+	sqlStr += "order by create_at "
+
+	if sort == "desc" {
+		sqlStr += "desc "
+	} else {
+		sqlStr += "asc "
+	}
+	sqlStr += "limit ?,?"
+	parms = append(parms, utils.PostionSize(page, size), size)
+
+	var ts []*entity.Subject
+	err := config.DB.Select(&ts, sqlStr, parms...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 2、获取元素个数
+	sqlStr = "select count(id) from subject where delete_at is null "
+	parms = make([]interface{}, 0)
+	if query["key"] != nil {
+		sqlStr += "and question like ? "
+		parms = append(parms, "%"+query["key"].(string))
+	}
+	if query["qb_id"] != nil {
+		sqlStr += "and qb_id = ? "
+		parms = append(parms, query["qb_id"])
+	}
+	if query["status"] != nil {
+		sqlStr += "and status=? "
+		parms = append(parms, query["status"])
+	}
+	if query["type"]!=nil{
+		sqlStr += "and type=? "
+		parms = append(parms, query["type"])
+	}
+
+	var total int
+	err = config.DB.Get(&total, sqlStr, parms...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return result.SUCCESS.SetData(result.NewPage(page, len(ts), total, ts))
+}
+
+func UpdateSubject(s *entity.Subject) *result.Result {
+	sqlStr := "update  subject set question=?,type=?,opt_a=?,opt_b=?,opt_c=?,opt_d=?,answer=?,analysis=?,status=?,qb_id=?,update_at=? where id=? and delete_at is null "
+
+	res, err := config.DB.Exec(sqlStr, s.Question, s.Type, s.OptA, s.OptB, s.OptC, s.OptD, utils.SubjectSort(s.Answer), s.Analysis, s.Status, s.QbId, time.Now(), s.Id)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetData(err)
+	}
+	affected, _ := res.RowsAffected()
+
+	return result.SuccessResult(result.NewResultChange(affected))
+}
+
+func DeleteSubjects(ids []string) *result.Result {
+	parm := []interface{}{time.Now()}
+	parmStr := ""
+	for k, id := range ids {
+		parm = append(parm, id)
+		if k == (len(ids) - 1) {
+			parmStr += "?"
+		} else {
+			parmStr += "?,"
+		}
+	}
+
+	sqlStr := fmt.Sprintf("update subject set delete_at=? where id in (%s)", parmStr)
+
+	res, err := config.DB.Exec(sqlStr, parm...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+	affected, _ := res.RowsAffected()
+
+	return result.SUCCESS.SetData(result.NewResultChange(affected))
+}
+
+func AddSubjectBatch(file multipart.File, id string) *result.Result {
+
+	xlsFile, err := excelize.OpenReader(file)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	typeName, err := xlsFile.GetCellValue("Sheet", "A1")
+	typeName = strings.TrimSpace(typeName)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 1.设置插入类型
+	tp := 0
+	switch typeName {
+	case "单选题":
+		tp = entity.SINGEL_CHOICE
+	case "填空题":
+		tp = entity.Completion
+	case "判断题":
+		tp = entity.Judgement
+	case "多选题":
+		tp = entity.MULTIPLE_CHOICE
+	default:
+		return result.UNKNOW_ERROR.SetMsg("excel格式错误")
+	}
+
+	tx, err := config.DB.Beginx()
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+		// 1.sql准备
+		sqlStr := "insert into subject(question,type,opt_a,opt_b,opt_c,opt_d,answer,analysis,status,qb_id,create_at,update_at) values (?,?,?,?,?,?,?,?,?,?,?,?)"
+		var stmt *sql.Stmt
+		stmt, err = config.DB.Prepare(sqlStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		var count int64
+		i := 3
+		for {
+			question, _ := xlsFile.GetCellValue("Sheet", "A"+strconv.Itoa(i))
+			var optA, optB, optC, optD, answer string
+			if typeName == "判断题" {
+				answer, _ = xlsFile.GetCellValue("Sheet", "B"+strconv.Itoa(i))
+			} else {
+				optA, _ = xlsFile.GetCellValue("Sheet", "B"+strconv.Itoa(i))
+				optB, _ = xlsFile.GetCellValue("Sheet", "C"+strconv.Itoa(i))
+				optC, _ = xlsFile.GetCellValue("Sheet", "D"+strconv.Itoa(i))
+				optD, _ = xlsFile.GetCellValue("Sheet", "E"+strconv.Itoa(i))
+				answer, _ = xlsFile.GetCellValue("Sheet", "F"+strconv.Itoa(i))
+				answer = utils.SubjectFormat(answer)
+			}
+
+			if question == "" {
+				break
+			}
+
+			var res sql.Result
+			res, err = stmt.Exec(question, tp, optA, optB, optC, optD, answer, "", entity.USE, id, time.Now(), time.Now())
+			if err != nil {
+				continue
+			}
+			affected, _ := res.RowsAffected()
+			count += affected
+			i++
+		}
+
+		return result.SuccessResult(result.NewResultChange(count))
+	})
+
+}
+
+func DownloadSubject(file *excelize.File, id, tp int) *result.Result {
+
+	sqlStr := "select s.*,q.name qb_name from subject s left join question_bank q on s.qb_id = q.id where q.id = ? and s.type =? and s.delete_at is null"
+	var subs []*vo.SubjectVo
+	err := config.DB.Select(&subs, sqlStr, id, tp)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 题型
+	name, err := entity.GetSubjectTypeName(tp)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	if len(subs)==0{
+		return result.SUCCESS.SetData( "没有" + name)
+	}
+
+	i := 3
+
+	for _, sub := range subs {
+		file.SetCellValue("Sheet", "A"+strconv.Itoa(i), sub.Id)
+		file.SetCellValue("Sheet", "B"+strconv.Itoa(i), sub.Question)
+		if tp == entity.Judgement {
+			file.SetCellValue("Sheet", "C"+strconv.Itoa(i), utils.SubjectParse(sub.Answer))
+		} else {
+			file.SetCellValue("Sheet", "C"+strconv.Itoa(i), sub.OptA)
+			file.SetCellValue("Sheet", "D"+strconv.Itoa(i), sub.OptB)
+			file.SetCellValue("Sheet", "E"+strconv.Itoa(i), sub.OptC)
+			file.SetCellValue("Sheet", "F"+strconv.Itoa(i), sub.OptD)
+			sub.Answer = utils.SubjectParse(sub.Answer)
+			file.SetCellValue("Sheet", "G"+strconv.Itoa(i), utils.SubjectParse(sub.Answer))
+		}
+		i++
+	}
+
+	return result.SUCCESS.SetData(subs[0].QbName + "_" + name)
+}

+ 117 - 0
dao/dao-term.go

@@ -0,0 +1,117 @@
+package dao
+
+import (
+	"database/sql"
+	"exam_system/config"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"exam_system/vo"
+	"github.com/jmoiron/sqlx"
+	"time"
+)
+
+func AddTerm(t *entity.Term) *result.Result {
+
+	sqlStr := "insert into term(name,create_at,update_at) values (?,?,?)"
+	res, err := config.DB.Exec(sqlStr, t.Name, time.Now(), time.Now())
+	if err != nil {
+		return result.UNKNOW_ERROR.SetData(err)
+	}
+
+	id, _ := res.LastInsertId()
+
+	return result.SuccessResult(id)
+}
+
+func TermDetail(id string) *result.Result {
+	sqlStr := "select * from term where id=?"
+
+	var t entity.Term
+	err := config.DB.Get(&t, sqlStr, id)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return result.DATA_NOT_FOUND
+		}
+		return result.UNKNOW_ERROR.SetData(err.Error())
+	}
+
+	return result.SuccessResult(&t)
+}
+
+func TermList(page, size int, sort string, query map[string]interface{}) *result.Result {
+	sqlStr := `SELECT
+	t.*,
+	count( u.id ) user_num 
+FROM
+	term t
+	LEFT JOIN class c ON t.id = c.term_id
+	LEFT JOIN user u ON c.id = u.class_id and u.delete_at is null
+WHERE
+	t.delete_at IS NULL
+GROUP BY
+	t.id 
+ORDER BY
+	t.create_at `
+	if sort == "asc" {
+		sqlStr += "asc "
+	}else{
+		sqlStr += "desc "
+	}
+	sqlStr += "limit ?,?"
+
+	var tv []*vo.TermVo
+	err := config.DB.Select(&tv, sqlStr, utils.PostionSize(page, size), size)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+	for _, t := range tv {
+		var c []*vo.ClassVo
+		sqlStr = "select c.*,count(u.id) user_num from class c left join user u on c.id=u.class_id and u.delete_at is null where c.delete_at is null and c.term_id=? group by c.id"
+
+		err = config.DB.Select(&c, sqlStr, t.Id)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		t.Classes = c
+	}
+
+	var count int
+	sqlStr = "select count(id) from term where delete_at is null"
+	err = config.DB.Get(&count, sqlStr)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+	return result.SUCCESS.SetData(result.NewPage(page,len(tv),count,tv))
+}
+
+func UpdateTerm(t *entity.Term) *result.Result {
+	sqlStr := "update  term set name=?,update_at=? where id=? and delete_at is  null "
+
+	res, err := config.DB.Exec(sqlStr, t.Name, time.Now(), t.Id)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetData(err)
+	}
+	affected, _ := res.RowsAffected()
+
+	return result.SuccessResult(result.NewResultChange(affected))
+}
+
+func DeleteTerms(ids []string) *result.Result {
+
+	sqlStr := "update term t left join class  c on t.id = c.term_id left join  user u on c.id = u.class_id  set t.delete_at=?,c.delete_at=?,u.delete_at=? where t.id in (?)"
+
+	query, param, err := sqlx.In(sqlStr, time.Now(), time.Now(), time.Now(),ids)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	res, err := config.DB.Exec(query, param...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+	affected, _ := res.RowsAffected()
+
+
+	return result.SUCCESS.SetData(result.NewResultChange(affected))
+}

+ 311 - 0
dao/dao-test-paper.go

@@ -0,0 +1,311 @@
+package dao
+
+import (
+	"database/sql"
+	"encoding/json"
+	"exam_system/config"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"exam_system/vo"
+	"fmt"
+	"github.com/jmoiron/sqlx"
+	"time"
+)
+
+func AddTestPaper(t *vo.TestPaperVo) *result.Result {
+
+	tx, err := config.DB.Beginx()
+
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+
+		// 1.插入试卷
+		sqlStr := "insert into test_paper(name,create_at,update_at,status,score,pass_score) values (?,?,?,?,?,?)"
+		var res sql.Result
+		res, err = tx.Exec(sqlStr, t.Name, time.Now(), time.Now(), entity.USE,t.Score,t.PassScore)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+		id, _ := res.LastInsertId()
+
+
+		var score int64
+		for _, chapter := range t.Chapters {
+
+			var subData []byte
+			subData, err = json.Marshal(chapter.SubList)
+			// 2.插入章节
+			sqlStr = "insert into chapter(name,description,sub_ids,tp_id,`index`) values (?,?,?,?,?)"
+			// sublist试题
+			var res2 sql.Result
+			res2, err = tx.Exec(sqlStr, chapter.Name, chapter.Description, string(subData), id,chapter.Index)
+			if err != nil {
+				return result.UNKNOW_ERROR.SetData(err)
+			}
+			affected, _ := res2.RowsAffected()
+			score+=affected
+		}
+
+		return result.SuccessResult(id)
+	})
+
+}
+
+func TestPaperDetail(id string) *result.Result {
+	// 1、查询试卷
+	sqlStr := "select * from test_paper where id=? and delete_at is null"
+
+	var t vo.TestPaperVo
+	err := config.DB.Get(&t, sqlStr, id)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return result.DATA_NOT_FOUND
+		}
+		return result.UNKNOW_ERROR.SetData(err.Error())
+	}
+
+	// 2、查询相关章节
+	sqlStr = "select * from chapter where tp_id =? order by `index`"
+	var chapters []*vo.ChapterVo
+	err = config.DB.Select(&chapters, sqlStr, t.Id)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 3、获取相关试题,chapter
+	for _, chap := range chapters {
+		var sidsvo []*vo.SubIdsVo
+		err = json.Unmarshal([]byte(chap.SubIds), &sidsvo)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		var subList []*vo.SubjectVo
+		for _, sidvo := range sidsvo {
+			var sub vo.SubjectVo
+			sqlStr = "select * from subject where id = ?"
+			err = config.DB.Get(&sub, sqlStr, sidvo.Id)
+			if err != nil && err.Error()!="sql: no rows in result set"{
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+
+			sub.Score = sidvo.Score
+			subList = append(subList,&sub)
+		}
+
+		chap.SubList = subList
+	}
+	t.Chapters = chapters
+
+	return result.SuccessResult(t)
+}
+
+func TestPaperDetail2(id string) *result.Result {
+	// 1、查询试卷
+	sqlStr := "select * from test_paper where id=? and delete_at is null"
+
+	var t vo.TestPaperVo
+	err := config.DB.Get(&t, sqlStr, id)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return result.DATA_NOT_FOUND
+		}
+		return result.UNKNOW_ERROR.SetData(err.Error())
+	}
+
+	// 2、查询相关章节
+	sqlStr = "select * from chapter where tp_id =? order by `index`"
+	var chapters []*vo.ChapterVo
+	err = config.DB.Select(&chapters, sqlStr, t.Id)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 3、获取相关试题,chapter
+	for _, chap := range chapters {
+		var sidsvo []*vo.SubIdsVo
+		err = json.Unmarshal([]byte(chap.SubIds), &sidsvo)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		var subList []*vo.SubjectVo
+		for _, sidvo := range sidsvo {
+			var sub vo.SubjectVo
+			sqlStr = "select id, create_at, update_at, delete_at, type, opt_a, opt_b, opt_c, opt_d, analysis, status, question, qb_id from subject where id = ?"
+			err = config.DB.Get(&sub, sqlStr, sidvo.Id)
+			if err != nil && err.Error()!="sql: no rows in result set"{
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+
+			sub.Score = sidvo.Score
+			subList = append(subList,&sub)
+		}
+
+		chap.SubList = subList
+	}
+	t.Chapters = chapters
+
+	return result.SuccessResult(t)
+}
+
+func TestPaperList(page, size int, sort string, query map[string]interface{}) *result.Result {
+
+	// 1、获取paper
+	sqlStr := "select * from test_paper where delete_at is null "
+
+	params := make([]interface{},0)
+	if query["name"]!=nil{
+		sqlStr+="and name like ? "
+		params = append(params, "%"+query["name"].(string)+"%")
+	}
+
+	if query["status"]!=nil{
+		sqlStr+="and status = ? "
+		params = append(params, fmt.Sprintf("%1.0f", query["status"].(float64)))
+	}
+
+	sqlStr+="order by create_at "
+
+	if sort == "asc" {
+		sqlStr += "asc "
+	} else {
+		sqlStr += "desc "
+	}
+	sqlStr += "limit ?,?"
+	params = append(params,utils.PostionSize(page, size),size)
+
+	var tp []*vo.TestPaperVo
+	err := config.DB.Select(&tp, sqlStr, params...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	// 2、获取章节
+	for _, t := range tp {
+		// 2、查询相关章节
+		chapterStr := "select * from chapter where tp_id =?"
+		var chapters []*vo.ChapterVo
+		err = config.DB.Select(&chapters, chapterStr, t.Id)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+
+		// 3、获取相关试题,chapter
+		for _, chap := range chapters {
+			var sidsvo []*vo.SubIdsVo
+			err = json.Unmarshal([]byte(chap.SubIds), &sidsvo)
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+
+			var subList []*vo.SubjectVo
+			for _, sidvo := range sidsvo {
+				var sub vo.SubjectVo
+				sqlStr = "select * from subject where id = ?"
+				err = config.DB.Get(&sub, sqlStr, sidvo.Id)
+				if err != nil && err.Error()!="sql: no rows in result set"{
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+
+				sub.Score = sidvo.Score
+				subList = append(subList,&sub)
+			}
+
+			chap.SubList = subList
+		}
+		t.Chapters = chapters
+
+	}
+
+	// 获取总数
+	sqlStr = "select count(id) from test_paper where delete_at is null "
+
+	var total int
+	params = make([]interface{},0)
+	if query["name"]!=nil{
+		sqlStr+="and name like ? "
+		params = append(params, "%"+query["name"].(string)+"%")
+	}
+	if query["status"]!=nil{
+		sqlStr+="and status = ? "
+		params = append(params, fmt.Sprintf("%1.0f", query["status"].(float64)))
+	}
+	err = config.DB.Get(&total, sqlStr,params...)
+	if err!=nil{
+		return result.UNKNOW_ERROR.SetData(err.Error())
+	}
+
+	return result.SUCCESS.SetData(result.NewPage(page,len(tp),total,tp))
+}
+
+func UpdateTestPaper(t *vo.TestPaperVo) *result.Result {
+
+
+
+	tx, err := config.DB.Beginx()
+	if err!=nil{
+		return result.UNKNOW_ERROR.SetData(err)
+	}
+	return utils.Transation(tx,err, func(tx *sqlx.Tx) *result.Result {
+
+
+		sqlStr := "update  test_paper set name=?,update_at=?,status=?,score=?,pass_score=? where id=? and delete_at is null "
+
+		var res sql.Result
+		res, err = config.DB.Exec(sqlStr, t.Name, time.Now(),t.Status,t.Score,t.PassScore, t.Id)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+		affected, _ := res.RowsAffected()
+
+		sqlStr = "delete from chapter where tp_id = ?"
+		res, err = tx.Exec(sqlStr, t.Id)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetData(err)
+		}
+
+		for _, chapter := range t.Chapters {
+
+			sqlStr = "insert into chapter(name,description,sub_ids,tp_id) values (?,?,?,?)"
+
+			var subData []byte
+			subData, err = json.Marshal(chapter.SubList)
+
+			// sublist试题
+			res, err = tx.Exec(sqlStr, chapter.Name, chapter.Description, subData, t.Id)
+			if err != nil {
+				return result.UNKNOW_ERROR.SetData(err)
+			}
+
+			affected2, _ := res.RowsAffected()
+
+			affected+=affected2
+		}
+
+		return result.SuccessResult(result.NewResultChange(affected))
+	})
+}
+
+func DeleteTestPapers(ids []string) *result.Result {
+	parm := []interface{}{time.Now()}
+	parmStr := ""
+	for k, id := range ids {
+		parm = append(parm, id)
+		if k == (len(ids) - 1) {
+			parmStr += "?"
+		} else {
+			parmStr += "?,"
+		}
+	}
+
+	sqlStr := fmt.Sprintf("update test_paper set delete_at=? where id in (%s)", parmStr)
+
+	res, err := config.DB.Exec(sqlStr, parm...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+	affected, _ := res.RowsAffected()
+
+	return result.SUCCESS.SetData(result.NewResultChange(affected))
+}

+ 920 - 0
dao/dao-user.go

@@ -0,0 +1,920 @@
+package dao
+
+import (
+	"database/sql"
+	"exam_system/config"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"exam_system/vo"
+	"fmt"
+	"github.com/jmoiron/sqlx"
+	"github.com/xuri/excelize/v2"
+	"golang.org/x/crypto/bcrypt"
+	"log"
+	"mime/multipart"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// FindUserbySid 是否存在
+func FindUserbySid(sid string) *result.Result {
+	sqlStr := `SELECT
+	u.id,
+	u.username,
+	u.sid,
+	u.create_at,
+	u.update_at,
+	u.password,
+	u.status,
+	group_concat( DISTINCT(r.name) ) role_name,
+	t.name term_name,
+	c.name class_name
+FROM
+	user u
+	LEFT JOIN class c ON u.class_id = c.id
+	LEFT JOIN term t ON c.term_id = t.id
+	LEFT JOIN user_role ur ON u.id = ur.user_id
+	LEFT JOIN role r ON ur.role_id = r.id 
+WHERE
+	u.sid =? and u.delete_at is null
+	GROUP BY u.id`
+
+	var u vo.UserVo
+	err := config.DB.Get(&u, sqlStr, sid)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return result.DATA_NOT_FOUND
+		}
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return result.SuccessResult(u)
+}
+
+// FindUserbyId 通过Id 查询
+func FindUserbyId(id int) *result.Result {
+	sqlStr := `SELECT
+	u.id,
+	u.username,
+	u.sid,
+	u.create_at,
+	u.update_at,
+	group_concat( DISTINCT(r.NAME) ) role_name,
+	t.name term_name,
+	c.name class_name
+FROM
+	user u
+	LEFT JOIN class c ON u.class_id = c.id
+	LEFT JOIN term t ON c.term_id = t.id
+	LEFT JOIN user_role ur ON u.id = ur.user_id
+	LEFT JOIN role r ON ur.role_id = r.id 
+WHERE
+	u.id =? and u.delete_at is null
+	GROUP BY u.id`
+
+	var user vo.UserVo
+	err := config.DB.Get(&user, sqlStr, id)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return result.SUCCESS.SetData(user)
+}
+
+// InsertUser 插入
+func InsertUser(user *entity.User, userType string) *result.Result {
+
+	tx, err := config.DB.Beginx() // 开启事务
+	if err != nil {
+		log.Printf("begin trans failed, err:%v\n", err)
+		return result.UNKNOW_ERROR
+	}
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+		// 1.先查询用户是否存在
+		sqlStr := "select id,username,password,sid,class_id from user where sid=? and delete_at is null"
+		var u entity.User
+		err = tx.Get(&u, sqlStr, user.Sid)
+		if err != nil && err.Error() != "sql: no rows in result set" {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		if u.ID != 0 {
+			return result.USER_IS_EXISTED
+		}
+
+		// 2、插入用户表
+		sqlStr = "insert into user(username, password,sid,create_at,update_at,status,class_id) values (?,?,?,?,?,?,?)"
+
+		var ret sql.Result
+		ret, err = tx.Exec(sqlStr, user.Username, user.Password, user.Sid, time.Now(), time.Now(), user.Status, user.ClassId)
+		if err != nil {
+			tx.Rollback()
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		id, _ := ret.LastInsertId()
+
+		// 3、通过名字获取角色
+
+		sqlStr = "select id from role where name=? limit 1"
+
+		var r entity.Role
+		err = tx.Get(&r, sqlStr, userType)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+
+
+		// 4、插入用户角色表
+		sqlStr = "insert into user_role(user_id, role_id,create_at,update_at) values (?,?,?,?)"
+		_, err = tx.Exec(sqlStr, id, r.Id, time.Now(), time.Now())
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 5、查询班级还未开始的考试
+		sqlStr = "select e.id from exam_class ec left join exam e on e.id = ec.exam_id where ec.class_id=? and e.end_at>=?"
+		var count []int64
+		err = tx.Select(&count, sqlStr, user.ClassId,time.Now())
+		if err != nil && err!=sql.ErrNoRows{
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 6、添加考试记录
+		sqlStr = "insert into exam_record(create_at, update_at, user_id, exam_id,status) values (?,?,?,?,?)"
+		prepare, _ := tx.Prepare(sqlStr)
+
+		for _, examId := range count {
+			_, err = prepare.Exec( time.Now(), time.Now(),id, examId,entity.NOT_TESTED)
+			if err != nil && err!=sql.ErrNoRows{
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+		}
+
+		// 7、更新考试人数
+		sqlStr = "update exam set user_count = user_count+1 where id in (?)"
+		var updateExamStr string
+		var param []interface{}
+		updateExamStr, param, err = sqlx.In(sqlStr,count)
+		_, err = tx.Exec(updateExamStr, param...)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		return result.SUCCESS.SetData(id)
+	})
+}
+
+// UpdateUser 根据id
+func UpdateUser(user *entity.User, userType string) *result.Result {
+
+	tx, err := config.DB.Beginx() // 开启事务
+	if err != nil {
+		log.Printf("begin trans failed, err:%v\n", err)
+		return result.UNKNOW_ERROR
+	}
+
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+		// 1.先查询用户
+		sqlStr := "select id,username,password,sid,class_id from user where id=?"
+
+		var u entity.User
+		err = tx.Get(&u, sqlStr, user.ID)
+		if err != nil && err.Error() != "sql: no rows in result set" {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 2、更新用户表(管理员插入默认为正常)
+		parms := make([]interface{}, 0)
+		sqlStr = `update user set username = ?, `
+		parms = append(parms, user.Username)
+
+		if user.Password != "" {
+			hash, _ := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
+			user.Password = string(hash)
+			parms = append(parms, user.Password)
+			sqlStr += `password = ?, `
+		}
+		if user.Sid != "" && user.Sid != u.Sid {
+			parms = append(parms, user.Sid)
+			sqlStr += `sid = ?, `
+		}
+		sqlStr += "class_id = ?,update_at=?,status=? where id = ?"
+		parms = append(parms, user.ClassId)
+		parms = append(parms, time.Now())
+		parms = append(parms, user.Status)
+		parms = append(parms, user.ID)
+
+		var ret sql.Result
+		ret, err = tx.Exec(sqlStr, parms...)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		affected, _ := ret.RowsAffected()
+
+		// 3、获取角色
+		var r entity.Role
+		sqlStr = "select id from role where name=? limit 1"
+		err = tx.Get(&r, sqlStr, userType)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 4、更新用户角色表
+		sqlStr = "select id from user_role where user_id=?"
+		var id int
+		var ret2 sql.Result
+		_ = tx.Get(&id, sqlStr, u.ID)
+		if id == 0 {
+			sqlStr = "insert into user_role(user_id, role_id,create_at,update_at) values (?,?,?,?)"
+			ret2, err = tx.Exec(sqlStr, u.ID, r.Id, time.Now(), time.Now())
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+		} else {
+			sqlStr = "update user_role set  role_id=?  ,update_at=? where user_id=?"
+			ret2, err = tx.Exec(sqlStr, r.Id, time.Now(), user.ID)
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+		}
+
+		affected2, _ := ret2.RowsAffected()
+		return result.SUCCESS.SetData(result.NewResultChange(affected + affected2))
+	})
+}
+
+// DeleteUsers  删除
+func DeleteUsers(ids []string) *result.Result {
+
+	parm := []interface{}{time.Now()}
+	parmStr := ""
+	for k, id := range ids {
+		parm = append(parm, id)
+		if k == (len(ids) - 1) {
+			parmStr += "?"
+		} else {
+			parmStr += "?,"
+		}
+	}
+
+	sqlStr := fmt.Sprintf("update user set delete_at=? where id in (%s)", parmStr)
+
+	res, err := config.DB.Exec(sqlStr, parm...)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return result.SUCCESS.SetData(res)
+}
+
+func FindUserList(page, size int, sort string, query map[string]interface{}) *result.Result {
+
+	tx, err := config.DB.Beginx() // 开启事务
+	if err != nil {
+		log.Printf("begin trans failed, err:%v\n", err)
+		return result.UNKNOW_ERROR
+	}
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+		sqlStr := `
+SELECT
+	u.id,
+	u.username,
+	u.sid,
+	group_concat(DISTINCT ( r.name )) role_name,
+	u.create_at,
+	u.update_at,
+	t.name term_name,
+	c.name class_name
+FROM
+	user u
+	LEFT JOIN class c ON u.class_id = c.id
+	LEFT JOIN term t ON c.term_id = t.id
+	LEFT JOIN user_role ur ON u.id = ur.user_id
+	LEFT JOIN role r ON ur.role_id = r.id 
+WHERE
+	u.delete_at IS NULL `
+
+		params := make([]interface{}, 0)
+		if query["username"] != nil {
+			sqlStr += "and u.username like ? "
+			params = append(params, "%"+query["username"].(string)+"%")
+		}
+
+		if query["sid"] != nil {
+			sqlStr += "and u.sid = ? "
+			params = append(params, query["sid"])
+		}
+
+		if query["class_id"] != nil {
+			sqlStr += "and u.class_id=? "
+			params = append(params, query["class_id"])
+		}
+
+		if query["role"] != nil {
+			sqlStr += "and r.name = ? "
+			params = append(params, query["role"])
+		}
+
+		sqlStr += `GROUP BY u.id `
+		if sort == "asc" {
+			sqlStr += `Order By u.create_at asc
+`
+		} else {
+			sqlStr += `Order By u.create_at desc
+`
+		}
+		sqlStr += `LIMIT ?,?`
+
+		params = append(params, utils.PostionSize(page, size), size)
+
+		var users []vo.UserVo
+		err = tx.Select(&users, sqlStr, params...)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		sqlStr = `SELECT
+	count(u.id)
+FROM
+	user u
+	LEFT JOIN class c ON u.class_id = c.id
+	LEFT JOIN term t ON c.term_id = t.id
+	LEFT JOIN user_role ur ON u.id = ur.user_id
+	LEFT JOIN role r ON ur.role_id = r.id 
+WHERE
+	u.delete_at IS NULL `
+		params = make([]interface{}, 0)
+		if query["username"] != nil {
+			sqlStr += "and u.username like ? "
+			params = append(params, "%"+query["username"].(string)+"%")
+		}
+
+		if query["sid"] != nil {
+			sqlStr += "and u.sid = ? "
+			params = append(params, query["sid"].(string))
+		}
+
+		if query["class_id"] != nil {
+			sqlStr += "and u.class_id=? "
+			params = append(params, query["class_id"])
+		}
+
+		if query["role"] != nil {
+			sqlStr += "and r.name = ? "
+			params = append(params, query["role"])
+		}
+
+		total := 0
+		err = tx.Get(&total, sqlStr, params...)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		return result.SuccessResult(result.NewPage(page, len(users), total, users))
+	})
+}
+
+// InsertBatchUserByClassId 导入xlsx
+func InsertBatchUserByClassId(file multipart.File, classId int) *result.Result {
+
+	xlsFile, err := excelize.OpenReader(file)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	tx, err := config.DB.Beginx() // 开启事务
+	if err != nil {
+		log.Printf("begin trans failed, err:%v\n", err)
+		return result.UNKNOW_ERROR
+	}
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+
+		var total int64 = 0
+
+		// 1.查询用户是否已存在
+		userStr := "select id,sid from user where sid = ? and delete_at is null limit 1"
+
+		// 2.插入用户
+		userAddStr := "insert into user(username, password,sid,create_at,update_at,status,class_id) values (?,?,?,?,?,?,?)"
+
+		var userAddStmt *sql.Stmt
+		userAddStmt, err = tx.Prepare(userAddStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 3.获取角色列表
+		rolesStr := "select id,name from role"
+		var roles []*entity.Role
+		err = tx.Select(&roles, rolesStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 4、插入用户角色表
+		roleAddStr := "insert into user_role(user_id, role_id,create_at,update_at) values (?,?,?,?)"
+		var roleAddStmt *sql.Stmt
+		roleAddStmt, err = tx.Prepare(roleAddStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 5、更新用户
+		userUpdateStr := "update user set username=?,password=?,sid=?,update_at=?,status=?,class_id=? where id = ?"
+		var userUpdateStmt *sql.Stmt
+		userUpdateStmt, err = tx.Prepare(userUpdateStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 6、更新用户角色
+		roleUpdateStr := "update user_role set  role_id=?  ,update_at=? where user_id=?"
+		var roleUpdateStmt *sql.Stmt
+		roleUpdateStmt, err = tx.Prepare(roleUpdateStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		i := 2
+		for {
+			sid, _ := xlsFile.GetCellValue("Sheet1", "A"+strconv.Itoa(i))
+			username, _ := xlsFile.GetCellValue("Sheet1", "B"+strconv.Itoa(i))
+			roleName, _ := xlsFile.GetCellValue("Sheet1", "D"+strconv.Itoa(i))
+			password, _ := xlsFile.GetCellValue("Sheet1", "E"+strconv.Itoa(i))
+			if sid == "" {
+				break
+			}
+			i++
+			hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+			password = string(hash)
+
+			// 1、查询用户是否存在
+			var user entity.User
+			err = tx.Get(&user, userStr, sid)
+			// 存在插入,不存在更新
+			var ret sql.Result
+			if err != nil {
+				if err == sql.ErrNoRows {
+					// 2、插入用户
+					ret, err = userAddStmt.Exec(username, password, sid, time.Now(), time.Now(), entity.NORMAL, classId)
+					if err != nil {
+						return result.UNKNOW_ERROR.SetMsg(err.Error())
+					}
+					id, _ := ret.LastInsertId()
+					affected, _ := ret.RowsAffected()
+					total += affected
+
+					// 3、通过名字获取角色
+					var role entity.Role
+					for _, r := range roles {
+						if r.Name == roleName {
+							role = *r
+							break
+						}
+					}
+
+					// 4、插入角色
+					_, err = roleAddStmt.Exec(id, role.Id, time.Now(), time.Now())
+					if err != nil {
+						return result.UNKNOW_ERROR.SetMsg(err.Error())
+					}
+				} else {
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+			} else {
+				// 1、更新用户
+				ret, err = userUpdateStmt.Exec(username, password, sid, time.Now(), entity.NORMAL, classId, user.ID)
+				if err != nil {
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+				affected, _ := ret.RowsAffected()
+				total += affected
+
+				// 2、获取角色
+				var role entity.Role
+				for _, r := range roles {
+					if r.Name == roleName {
+						role = *r
+						break
+					}
+				}
+				// 3、更新角色
+				ret, err = roleUpdateStmt.Exec(role.Id, time.Now(), user.ID)
+				if err != nil {
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+			}
+
+		}
+
+		res := result.NewResultChange(total)
+		res.RowsFailed = int64(i-2) - total
+
+		return result.SuccessResult(res)
+	})
+
+}
+
+// InsertBatchUserByTermId 导入xlsx
+func InsertBatchUserByTermId(file multipart.File, termId, classId int) *result.Result {
+
+	xlsFile, err := excelize.OpenReader(file)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	tx, err := config.DB.Beginx() // 开启事务
+	if err != nil {
+		log.Printf("begin trans failed, err:%v\n", err)
+		return result.UNKNOW_ERROR
+	}
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+
+		var total int64 = 0
+
+		// 1.查询班级
+		classStr := "SELECT c.id,c.term_id FROM class c,term t WHERE t.name=? and  c.name = ?  and c.term_id = t.id AND c.delete_at IS NULL limit 1"
+
+		// 1.查询用户是否已存在
+		userStr := "select id,sid from user where sid = ? and delete_at is null limit 1"
+
+		// 2.插入用户
+		userAddStr := "insert into user(username, password,sid,create_at,update_at,status,class_id) values (?,?,?,?,?,?,?)"
+
+		var userAddStmt *sql.Stmt
+		userAddStmt, err = tx.Prepare(userAddStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 3.获取角色列表
+		rolesStr := "select id,name from role"
+		var roles []*entity.Role
+		err = tx.Select(&roles, rolesStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 4、插入用户角色表
+		roleAddStr := "insert into user_role(user_id, role_id,create_at,update_at) values (?,?,?,?)"
+		var roleAddStmt *sql.Stmt
+		roleAddStmt, err = tx.Prepare(roleAddStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 5、更新用户
+		userUpdateStr := "update user set username=?,password=?,sid=?,update_at=?,status=?,class_id=? where id = ?"
+		var userUpdateStmt *sql.Stmt
+		userUpdateStmt, err = tx.Prepare(userUpdateStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 6、更新用户角色
+		roleUpdateStr := "update user_role set  role_id=?  ,update_at=? where user_id=?"
+		var roleUpdateStmt *sql.Stmt
+		roleUpdateStmt, err = tx.Prepare(roleUpdateStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+		i := 2
+		for {
+			sid, _ := xlsFile.GetCellValue("Sheet1", "A"+strconv.Itoa(i))
+			username, _ := xlsFile.GetCellValue("Sheet1", "B"+strconv.Itoa(i))
+			termClassName, _ := xlsFile.GetCellValue("Sheet1", "C"+strconv.Itoa(i))
+			roleName, _ := xlsFile.GetCellValue("Sheet1", "D"+strconv.Itoa(i))
+			password, _ := xlsFile.GetCellValue("Sheet1", "E"+strconv.Itoa(i))
+			if sid == "" {
+				break
+			}
+			i++
+			hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+			password = string(hash)
+
+			// 1.查询班级
+			var class entity.Class
+			termClass := strings.Split(termClassName, ">")
+
+
+			if len(termClass)==2{
+				fmt.Println(classStr)
+				err = tx.Get(&class, classStr, termClass[0], termClass[1])
+				if err != nil {
+						if err == sql.ErrNoRows {
+							err = nil
+							continue
+						}
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+			}
+			fmt.Println()
+			if roleName == "student" && class.TermId != termId || (classId != 0 && class.Id !=nil &&  *class.Id==classId) {
+				continue
+			}
+
+			// 2、查询用户是否存在
+			var user entity.User
+			err = tx.Get(&user, userStr, sid)
+			// 存在插入,不存在更新
+			var ret sql.Result
+			if err != nil {
+				if err == sql.ErrNoRows {
+					// 2、插入用户
+					ret, err = userAddStmt.Exec(username, password, sid, time.Now(), time.Now(), entity.NORMAL, class.Id)
+					if err != nil {
+						return result.UNKNOW_ERROR.SetMsg(err.Error())
+					}
+					id, _ := ret.LastInsertId()
+					user.ID = int(id)
+					affected, _ := ret.RowsAffected()
+					total += affected
+
+					// 3、通过名字获取角色
+					var role entity.Role
+					for _, r := range roles {
+						if r.Name == roleName {
+							role = *r
+							break
+						}
+					}
+
+					// 4、插入角色
+					_, err = roleAddStmt.Exec(id, role.Id, time.Now(), time.Now())
+					if err != nil {
+						return result.UNKNOW_ERROR.SetMsg(err.Error())
+					}
+				} else {
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+			} else {
+				// 1、更新用户
+				ret, err = userUpdateStmt.Exec(username, password, sid, time.Now(), entity.NORMAL, class.Id, user.ID)
+				if err != nil {
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+				affected, _ := ret.RowsAffected()
+				total += affected
+
+				// 2、获取角色
+				var role entity.Role
+				for _, r := range roles {
+					if r.Name == roleName {
+						role = *r
+						break
+					}
+				}
+				// 3、更新角色
+				ret, err = roleUpdateStmt.Exec(role.Id, time.Now(), user.ID)
+				if err != nil {
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+			}
+
+			// 更新考试
+			// 5、查询班级还未开始的考试
+			sqlStr := "select e.id from exam_class ec left join exam e on e.id = ec.exam_id where ec.class_id=? and e.end_at>=?"
+			var count []int64
+			err = tx.Select(&count, sqlStr, user.ClassId,time.Now())
+			if err != nil && err!=sql.ErrNoRows{
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+
+			// 6、添加考试记录
+			sqlStr = "insert into exam_record(create_at, update_at, user_id, exam_id,status) values (?,?,?,?,?)"
+			prepare, _ := tx.Prepare(sqlStr)
+
+			for _, examId := range count {
+				_, err = prepare.Exec( time.Now(), time.Now(),user.ID, examId,entity.NOT_TESTED)
+				if err != nil && err!=sql.ErrNoRows{
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+			}
+
+			// 7、更新考试人数
+			sqlStr = "update exam set user_count = user_count+1 where id in (?)"
+			var updateExamStr string
+			var param []interface{}
+			updateExamStr, param, err = sqlx.In(sqlStr,count)
+			_, err = tx.Exec(updateExamStr, param...)
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+		}
+
+		res := result.NewResultChange(total)
+		res.RowsFailed = int64(i-2) - total
+		return result.SuccessResult(res)
+	})
+
+}
+
+// InsertBatchUser 导入xlsx(excel读内存)
+func InsertBatchUser(file multipart.File) *result.Result {
+
+	xlsFile, err := excelize.OpenReader(file)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	tx, err := config.DB.Beginx() // 开启事务
+	if err != nil {
+		log.Printf("begin trans failed, err:%v\n", err)
+		return result.UNKNOW_ERROR
+	}
+	return utils.Transation(tx, err, func(tx *sqlx.Tx) *result.Result {
+		i := 2
+		var total int64 = 0
+		// 1.查询班级
+		classStr := "SELECT c.id FROM class c,term t WHERE t.name=? and  c.name = ?  and c.term_id = t.id AND c.delete_at IS NULL limit 1"
+
+		// 2.查询用户是否已存在
+		userStr := "select id,sid from user where sid = ? and delete_at is null limit 1"
+
+		// 3.插入用户
+		userAddStr := "insert into user(username, password,sid,create_at,update_at,status,class_id) values (?,?,?,?,?,?,?)"
+
+		var userAddStmt *sql.Stmt
+		userAddStmt, err = tx.Prepare(userAddStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 3.获取角色列表
+		rolesStr := "select id,name from role"
+		var roles []*entity.Role
+		err = tx.Select(&roles, rolesStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 4、插入用户角色表
+		roleAddStr := "insert into user_role(user_id, role_id,create_at,update_at) values (?,?,?,?)"
+		var roleAddStmt *sql.Stmt
+		roleAddStmt, err = tx.Prepare(roleAddStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 5、更新用户
+		userUpdateStr := "update user set username=?,password=?,sid=?,update_at=?,status=?,class_id=? where id = ?"
+		var userUpdateStmt *sql.Stmt
+		userUpdateStmt, err = tx.Prepare(userUpdateStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		// 6、更新用户角色
+		roleUpdateStr := "update user_role set  role_id=?  ,update_at=? where user_id=?"
+		var roleUpdateStmt *sql.Stmt
+		roleUpdateStmt, err = tx.Prepare(roleUpdateStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		var users []*vo.UserVo
+		for {
+			sid, _ := xlsFile.GetCellValue("Sheet1", "A"+strconv.Itoa(i))
+			username, _ := xlsFile.GetCellValue("Sheet1", "B"+strconv.Itoa(i))
+			className, _ := xlsFile.GetCellValue("Sheet1", "C"+strconv.Itoa(i))
+			roleName, _ := xlsFile.GetCellValue("Sheet1", "D"+strconv.Itoa(i))
+			password, _ := xlsFile.GetCellValue("Sheet1", "E"+strconv.Itoa(i))
+			if sid == "" {
+				break
+			}
+
+			user := entity.User{Sid: sid, Username: username, Password: password}
+			users = append(users, &vo.UserVo{User: user, RoleName: &roleName, ClassName: &className})
+			i++
+		}
+
+		for _, v := range users {
+			// 1、查询学期班级
+			termClass := strings.Split(*v.ClassName, ">")
+			var class entity.Class
+			if len(termClass)==2{
+				err = tx.Get(&class, classStr, termClass[0], termClass[1])
+				if err != nil {
+					if err == sql.ErrNoRows {
+						err = nil
+						continue
+					}
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+			}
+
+
+			hash, _ := bcrypt.GenerateFromPassword([]byte(v.Password), bcrypt.DefaultCost)
+			v.Password = string(hash)
+
+			// 2、查询用户是否存在
+			var user entity.User
+			err = tx.Get(&user, userStr, v.Sid)
+			// 存在插入,不存在更新
+			var ret sql.Result
+			if err != nil {
+				if err == sql.ErrNoRows {
+					// 2、插入用户
+					ret, err = userAddStmt.Exec(v.Username, v.Password, v.Sid, time.Now(), time.Now(), entity.NORMAL, class.Id)
+					if err != nil {
+						return result.UNKNOW_ERROR.SetMsg(err.Error())
+					}
+					id, _ := ret.LastInsertId()
+					affected, _ := ret.RowsAffected()
+					total += affected
+
+					// 3、通过名字获取角色
+					var role entity.Role
+					for _, r := range roles {
+						if r.Name == *v.RoleName {
+							role = *r
+							break
+						}
+					}
+
+					// 4、插入角色
+					_, err = roleAddStmt.Exec(id, role.Id, time.Now(), time.Now())
+					if err != nil {
+						return result.UNKNOW_ERROR.SetMsg(err.Error())
+					}
+				} else {
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+			} else {
+				// 2、更新用户
+				ret, err = userUpdateStmt.Exec(v.Username, v.Password, v.Sid, time.Now(), entity.NORMAL, class.Id, user.ID)
+				if err != nil {
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+				affected, _ := ret.RowsAffected()
+				total += affected
+
+				// 获取角色
+				var role entity.Role
+				for _, r := range roles {
+					if r.Name == *v.RoleName {
+						role = *r
+						break
+					}
+				}
+				// 3、更新角色
+				ret, err = roleUpdateStmt.Exec(role.Id, time.Now(), user.ID)
+				if err != nil {
+					return result.UNKNOW_ERROR.SetMsg(err.Error())
+				}
+			}
+		}
+
+		res := result.NewResultChange(total)
+		res.RowsFailed = int64(i-2) - total
+
+		return result.SuccessResult(res)
+	})
+
+}
+
+// ImportUserInfo 导入学生信息
+func DownloadUserInfo(file *excelize.File) *result.Result {
+
+	sqlStr := `	select
+	u.id,
+	u.username,
+	u.password,
+	u.sid,
+	u.create_at,
+	u.update_at,
+	group_concat( DISTINCT(r.NAME) ) role_name,
+	t.name term_name,
+	c.name class_name
+FROM
+	user u
+	LEFT JOIN class c ON u.class_id = c.id
+	LEFT JOIN term t ON c.term_id = t.id
+	LEFT JOIN user_role ur ON u.id = ur.user_id
+	LEFT JOIN role r ON ur.role_id = r.id 
+WHERE
+	 u.delete_at is null
+	GROUP BY u.id`
+
+	var users []*vo.UserVo
+	err := config.DB.Select(&users, sqlStr)
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	i := 2
+	for _, user := range users {
+		file.SetCellValue("Sheet1", "A"+strconv.Itoa(i), user.Sid)
+		file.SetCellValue("Sheet1", "B"+strconv.Itoa(i), user.Username)
+		if user.ClassName != nil && user.TermName != nil {
+			file.SetCellValue("Sheet1", "C"+strconv.Itoa(i), (*user.TermName + ">" + *user.ClassName))
+		}
+		if user.RoleName != nil {
+			file.SetCellValue("Sheet1", "D"+strconv.Itoa(i), *user.RoleName)
+		}
+		file.SetCellValue("Sheet1", "E"+strconv.Itoa(i), user.Password)
+		i++
+	}
+	return result.SUCCESS
+}

+ 48 - 0
docker-compose.yml

@@ -0,0 +1,48 @@
+version: '3'
+networks:
+  network:
+    ipam:
+      driver: default
+      config:
+        - subnet: '177.7.0.0/16'
+
+services:
+  exam:
+    restart: always
+    image: registry.cn-chengdu.aliyuncs.com/infish/exam:1.0.0
+    container_name: exam
+    ports:
+      - "3001:3000"
+    networks:
+      network:
+        # 在network网络下的容器内部的Ipv4地址
+        ipv4_address: 177.7.0.12
+  mysql:
+    restart: always
+    image: mysql:8.0
+    container_name: mysql_exam
+    environment:
+      - MYSQL_ROOT_PASSWORD=zyhd2022
+      - MYSQL_DATABASE=exam
+      - TZ=Asia/Shanghai
+    volumes:
+      - ./datadir:/var/lib/mysql
+      - ./conf/my.cnf:/etc/my.cnf
+      - ./olddata/:/docker-entrypoint-initdb.d
+    ports:
+      - 3306:3306
+    networks:
+      network:
+        # 在network网络下的容器内部的Ipv4地址
+        ipv4_address: 177.7.0.13
+  nats:
+    restart: always
+    image: nats:latest
+    container_name: nats_exam
+    ports:
+      -  4223:4222
+    command: "--js --http_port 8222"
+    networks:
+      network:
+        # 在network网络下的容器内部的Ipv4地址
+        ipv4_address: 177.7.0.14

+ 14 - 0
entity/class.go

@@ -0,0 +1,14 @@
+package entity
+
+import (
+	"time"
+)
+
+type Class struct {
+	Id       *int        `json:"id,omitempty" db:"id,omitempty"`
+	Name     string     `json:"name,omitempty" db:"name,omitempty"`
+	TermId   int        `json:"term_id,omitempty" db:"term_id,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+}

+ 26 - 0
entity/exam.go

@@ -0,0 +1,26 @@
+package entity
+
+import (
+	"time"
+)
+
+// Exam 考试
+type Exam struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	Name     string     `json:"name,omitempty" db:"name,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+	Status   int        `json:"status,omitempty" db:"status,omitempty"`
+	// 考试开始结束时间
+	StartAt  time.Time	`json:"start_at,omitempty" db:"start_at,omitempty"`
+	EndAt    time.Time  `json:"end_at,omitempty" db:"end_at,omitempty"`
+	PublishAt    time.Time  `json:"publish_at,omitempty" db:"publish_at,omitempty"`
+	// 考试时长
+	Duration int        `json:"duration,omitempty" db:"duration,omitempty"`
+	// 要考试的班级组
+	ClassIds string		`json:"class_ids,omitempty" db:"class_ids,omitempty"`
+	// 试卷id
+	TpId     int		`json:"tp_id,omitempty" db:"tp_id,omitempty"`
+	UserCount     *int		`json:"user_count,omitempty" db:"user_count,omitempty"`
+}

+ 29 - 0
entity/exam_record.go

@@ -0,0 +1,29 @@
+package entity
+
+import (
+	"time"
+)
+
+var (
+	NOT_TESTED = 0
+	TESTING    = 1
+	TESTED     = 2
+	TIMEOUT    = 3
+)
+
+// 考试记录管理
+type ExamRecord struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+	Score    *int       `json:"score,omitempty" db:"score,omitempty"`
+	UserId   int        `json:"user_id,omitempty" db:"user_id,omitempty"`
+	ExamId   int        `json:"exam_id,omitempty" db:"exam_id,omitempty"`
+	Answer   *string    `json:"answer,omitempty" db:"answer,omitempty"`
+	// 记录考试状态,防止用户恶意修改考试记录
+	Token string `json:"token,omitempty" db:"token,omitempty"`
+	Ip    string `json:"ip,omitempty" db:"ip,omitempty"`
+	// 0未考试 1已考试 2超时未交卷
+	Status *int `json:"status,omitempty" db:"status,omitempty"`
+}

+ 15 - 0
entity/perm.go

@@ -0,0 +1,15 @@
+package entity
+
+import "time"
+
+type Perm struct {
+	Id   int    `json:"id,omitempty" db:"id,omitempty"`
+	Path string `json:"path,omitempty" db:"path,omitempty"`
+	// 请求方式
+	Method string `json:"method,omitempty" db:"method,,omitempty"`
+	// 名字
+	Name     string     `json:"name,omitempty" db:"name,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+}

+ 12 - 0
entity/perm_role.go

@@ -0,0 +1,12 @@
+package entity
+
+import "time"
+
+type PermRole struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	PermId   int        `json:"permId,omitempty" db:"permId,omitempty"`
+	RoleId   int        `json:"roleId,omitempty" db:"roleId,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+}

+ 21 - 0
entity/question_bank.go

@@ -0,0 +1,21 @@
+package entity
+
+import (
+	"time"
+)
+
+const (
+	UNUSE = iota
+	USE
+)
+
+// QuestionBank 题库
+type QuestionBank struct {
+	Id          int        `json:"id,omitempty" db:"id,omitempty"`
+	Name        string     `json:"name,omitempty" db:"name,omitempty"`
+	CreateAt    time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt    time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt    *time.Time `json:"-" db:"delete_at,omitempty"`
+	Status      *int        `json:"status,omitempty" db:"status,omitempty"` // 0禁用	1正常
+	Description string     `json:"description,omitempty" db:"description,omitempty"`
+}

+ 11 - 0
entity/role.go

@@ -0,0 +1,11 @@
+package entity
+
+import "time"
+
+type Role struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	Name     string     `json:"name,omitempty" db:"name,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+}

+ 52 - 0
entity/subject.go

@@ -0,0 +1,52 @@
+package entity
+
+import (
+	"errors"
+	"time"
+)
+
+const (
+	SINGEL_CHOICE = iota
+	Completion
+	MULTIPLE_CHOICE
+	Judgement
+)
+
+// Subject 试题
+type Subject struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+	// 0 禁用 1使用
+	Status *int `json:"status,omitempty" db:"status,omitempty"`
+	// 问题
+	Question string `json:"question,omitempty" db:"question"`
+	// 0 单选题 1填空题 2多选题 3判断题
+	Type *int   `json:"type,omitempty" db:"type,omitempty"`
+	OptA string `json:"opt_a,omitempty" db:"opt_a,omitempty"`
+	OptB string `json:"opt_b,omitempty" db:"opt_b,omitempty"`
+	OptC string `json:"opt_c,omitempty" db:"opt_c,omitempty"`
+	OptD string `json:"opt_d,omitempty" db:"opt_d,omitempty"`
+	// 回答 (ABCD), 单选、填空 多选 判断
+	Answer string `json:"answer,omitempty" db:"answer,omitempty"`
+	// 解析
+	Analysis string `json:"analysis,omitempty" db:"analysis,omitempty"`
+	// 题库id
+	QbId int `json:"qb_id,omitempty" db:"qb_id,omitempty"`
+}
+
+func GetSubjectTypeName(tp int) (string, error) {
+	switch tp {
+	case SINGEL_CHOICE:
+		return "单选题", nil
+	case Completion:
+		return "填空题", nil
+	case MULTIPLE_CHOICE:
+		return "判断题", nil
+	case Judgement:
+		return "多选题", nil
+	default:
+		return "", errors.New("excel格式错误")
+	}
+}

+ 14 - 0
entity/term.go

@@ -0,0 +1,14 @@
+package entity
+
+import (
+	"time"
+)
+
+// Term 学期
+type Term struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	Name     string     `json:"name,omitempty" db:"name,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+}

+ 28 - 0
entity/test_paper.go

@@ -0,0 +1,28 @@
+package entity
+
+import (
+	"time"
+)
+
+// TestPaper 试卷
+type TestPaper struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	Name     string     `json:"name,omitempty" db:"name,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+	Score    *int         `json:"score,omitempty" db:"score,omitempty"`
+	Status   *int        `json:"status,omitempty" db:"status,omitempty"`
+	// 及格
+	PassScore *int		`json:"pass_score,omitempty" db:"pass_score,omitempty"`
+}
+
+
+type Chapter struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	Name     string     `json:"name,omitempty" db:"name,omitempty"`
+	Description string	`json:"description,omitempty" db:"description,omitempty"`
+	// 题目id编号
+	SubList		string	`json:"sub_list,omitempty" db:"sub_list,omitempty"`
+	TpId	int 		`json:"tp_id,omitempty" db:"tp_id,omitempty"`
+}

+ 23 - 0
entity/user.go

@@ -0,0 +1,23 @@
+package entity
+
+import "time"
+
+const (
+	UNDER_REVIEW = iota
+	NORMAL
+	DISABLED
+)
+
+type User struct {
+	ID       int    `json:"id,omitempty" db:"id,omitempty"`
+	Username string `json:"username,omitempty" db:"username,omitempty"`
+	Password string `json:"password,omitempty" db:"password,omitempty"`
+	// 学号
+	Sid      string     `json:"sid,omitempty" db:"sid,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+	// 状态 0审核中 1正常 2禁用
+	Status  *int  `json:"status,omitempty" db:"status,omitempty"`
+	ClassId *int `json:"class_id,omitempty" db:"class_id,omitempty"`
+}

+ 12 - 0
entity/user_role.go

@@ -0,0 +1,12 @@
+package entity
+
+import "time"
+
+type UserRole struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	UserId   int        `json:"userId,omitempty" db:"userId,omitempty"`
+	RoleId   int        `json:"roleId,omitempty" db:"roleId,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+}

BIN
file/判断题.xlsx


BIN
file/单选题.xlsx


BIN
file/填空题.xlsx


BIN
file/多选题.xlsx


BIN
file/成绩导出.xlsx


BIN
file/考生信息导出.xlsx


BIN
file/考生模板.zip


BIN
file/试题模板.zip


+ 55 - 0
go.mod

@@ -0,0 +1,55 @@
+module exam_system
+
+go 1.18
+
+require (
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible
+	github.com/gin-gonic/gin v1.8.0
+	github.com/go-sql-driver/mysql v1.6.0
+	github.com/jmoiron/sqlx v1.3.5
+	github.com/nats-io/nats.go v1.19.0
+	github.com/satori/go.uuid v1.2.0
+	github.com/spf13/viper v1.12.0
+	github.com/xuri/excelize/v2 v2.6.0
+	golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
+)
+
+require (
+	github.com/fsnotify/fsnotify v1.5.4 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-playground/locales v0.14.0 // indirect
+	github.com/go-playground/universal-translator v0.18.0 // indirect
+	github.com/go-playground/validator/v10 v10.10.0 // indirect
+	github.com/goccy/go-json v0.9.7 // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/leodido/go-urn v1.2.1 // indirect
+	github.com/magiconair/properties v1.8.6 // indirect
+	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
+	github.com/nats-io/nats-server/v2 v2.9.6 // indirect
+	github.com/nats-io/nkeys v0.3.0 // indirect
+	github.com/nats-io/nuid v1.0.1 // indirect
+	github.com/pelletier/go-toml v1.9.5 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.2 // indirect
+	github.com/richardlehane/mscfb v1.0.4 // indirect
+	github.com/richardlehane/msoleps v1.0.3 // indirect
+	github.com/spf13/afero v1.8.2 // indirect
+	github.com/spf13/cast v1.5.0 // indirect
+	github.com/spf13/jwalterweatherman v1.1.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/subosito/gotenv v1.4.0 // indirect
+	github.com/ugorji/go/codec v1.2.7 // indirect
+	github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect
+	github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect
+	golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect
+	golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
+	golang.org/x/text v0.3.7 // indirect
+	google.golang.org/protobuf v1.28.0 // indirect
+	gopkg.in/ini.v1 v1.66.6 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 578 - 0
go.sum

@@ -0,0 +1,578 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
+github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.8.0 h1:4WFH5yycBMA3za5Hnl425yd9ymdw1XPm4666oab+hv4=
+github.com/gin-gonic/gin v1.8.0/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
+github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
+github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
+github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
+github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
+github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
+github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
+github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
+github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI=
+github.com/nats-io/nats-server/v2 v2.9.6 h1:RTtK+rv/4CcliOuqGsy58g7MuWkBaWmF5TUNwuUo9Uw=
+github.com/nats-io/nats-server/v2 v2.9.6/go.mod h1:AB6hAnGZDlYfqb7CTAm66ZKMZy9DpfierY1/PbpvI2g=
+github.com/nats-io/nats.go v1.19.0 h1:H6j8aBnTQFoVrTGB6Xjd903UMdE7jz6DS4YkmAqgZ9Q=
+github.com/nats-io/nats.go v1.19.0/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA=
+github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
+github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
+github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
+github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw=
+github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
+github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
+github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
+github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
+github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
+github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
+github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs=
+github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo=
+github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
+github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
+github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
+github.com/xuri/efp v0.0.0-20220407160117-ad0f7a785be8/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c=
+github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.6.0 h1:m/aXAzSAqxgt74Nfd+sNzpzVKhTGl7+S9nbG4A57mF4=
+github.com/xuri/excelize/v2 v2.6.0/go.mod h1:Q1YetlHesXEKwGFfeJn7PfEZz2IvHb6wdOeYjBxVcVs=
+github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M=
+github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
+golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
+golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
+golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
+golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
+gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

BIN
go_build_exam_system_cmd.exe


+ 51 - 0
middleware/auth/auth.go

@@ -0,0 +1,51 @@
+package auth
+
+import (
+	"exam_system/middleware/claims"
+	result "exam_system/result"
+	"net/http"
+	"strings"
+
+	"github.com/gin-gonic/gin"
+)
+
+func Auth() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// auth开头跳过
+		if strings.HasPrefix(c.Request.RequestURI, "/auth") {
+			c.Next()
+		} else {
+			// 授权
+			tokenValidate(c)
+		}
+	}
+}
+
+func tokenValidate(c *gin.Context) {
+	authHeader := c.Request.Header.Get("Authorization")
+	if authHeader == "" {
+		c.JSON(http.StatusUnauthorized, result.UNAUTHORIZED)
+		c.Abort()
+		return
+	}
+	// 按空格分割
+	parts := strings.SplitN(authHeader, " ", 2)
+	if !(len(parts) == 2 && parts[0] == "Bearer") {
+		c.JSON(http.StatusUnauthorized, result.UNAUTHORIZED)
+		c.Abort()
+		return
+	}
+	// parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它
+	mc, err := claims.ParseToken(parts[1])
+	if err != nil {
+		c.JSON(http.StatusUnauthorized, result.UNAUTHORIZED)
+		c.Abort()
+		return
+	}
+	// 将当前请求的username信息保存到请求的上下文c上
+	c.Set("id", mc.Id)
+	c.Set("sid", mc.Sid)
+	c.Set("username", mc.Username)
+	c.Set("role", mc.Role)
+	c.Next() // 后续的处理函数可以用过c.Get("username")来获取当前请求的用户信
+}

+ 53 - 0
middleware/claims/claims.go

@@ -0,0 +1,53 @@
+package claims
+
+import (
+	"errors"
+	"github.com/dgrijalva/jwt-go"
+	"time"
+)
+
+type MyClaims struct {
+	Id       int      `json:"id"`
+	Username string   `json:"username"`
+	Sid      string   `json:"sid"`
+	Role     []string `json:"role"`
+	jwt.StandardClaims
+}
+
+const TokenExpireDuration = time.Hour * 24
+
+var MySecret = []byte("fasdge[wkejk@$gagli")
+
+// GenToken 生成JWT
+func GenToken(id int, username, sid string, roleName []string) (string, error) {
+	// 创建一个我们自己的声明
+	c := MyClaims{
+		Id:       id,
+		Username: username, // 自定义字段
+		Sid:      sid,
+		Role:     roleName,
+		StandardClaims: jwt.StandardClaims{
+			ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间
+			Issuer:    "my-project",                               // 签发人
+		},
+	}
+	// 使用指定的签名方法创建签名对象
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
+	// 使用指定的secret签名并获得完整的编码后的字符串token
+	return token.SignedString(MySecret)
+}
+
+// ParseToken 解析JWT
+func ParseToken(tokenString string) (*MyClaims, error) {
+	// 解析token
+	token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (i interface{}, err error) {
+		return MySecret, nil
+	})
+	if err != nil {
+		return nil, err
+	}
+	if claims, ok := token.Claims.(*MyClaims); ok && token.Valid { // 校验token
+		return claims, nil
+	}
+	return nil, errors.New("invalid token")
+}

+ 24 - 0
middleware/exception/exception.go

@@ -0,0 +1,24 @@
+package exception
+
+import (
+	"exam_system/result"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"runtime/debug"
+)
+
+func Recover() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		defer func() {
+			if r := recover(); r != nil {
+				fmt.Errorf("panic %v\n", r)
+				debug.PrintStack()
+				c.JSON(http.StatusInternalServerError, result.UNKNOW_ERROR.SetMsg(r.(error).Error()))
+				c.Abort()
+			}
+		}()
+		c.Next()
+	}
+
+}

+ 18 - 0
middleware/middleware.go

@@ -0,0 +1,18 @@
+package middleware
+
+import (
+	"exam_system/middleware/auth"
+	"exam_system/middleware/exception"
+	"exam_system/middleware/perm"
+	"github.com/gin-gonic/gin"
+)
+
+func Middleware(engine *gin.Engine) {
+	// 异常
+	engine.Use(exception.Recover())
+	// 鉴权
+	engine.Use(auth.Auth())
+	// 权限
+	engine.Use(perm.Perm())
+
+}

+ 66 - 0
middleware/perm/perm.go

@@ -0,0 +1,66 @@
+package perm
+
+import (
+	"exam_system/dao"
+	"exam_system/entity"
+	result "exam_system/result"
+	"exam_system/vo"
+	"net/http"
+	"regexp"
+	"strings"
+
+	"github.com/gin-gonic/gin"
+)
+
+func Perm() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		if strings.HasPrefix(c.Request.RequestURI, "/auth") {
+			c.Next()
+			return
+		}
+		id := c.GetInt("id")
+		if id == 0 {
+			c.JSON(http.StatusForbidden, result.NO_PERMISSION)
+			c.Abort()
+			return
+		}
+
+		// 查user
+		res := dao.FindUserbyId(id)
+		if res.Data == nil {
+			c.JSON(http.StatusForbidden, result.USER_IS_NOT_EXISTED)
+			c.Abort()
+			return
+		}
+		userVo := res.Data.(vo.UserVo)
+
+		// TODO 后续role和perm可以保存到redis里面
+		// 查role列表
+		roles := dao.FindRoleByUserId(userVo.ID)
+		roleIds := make([]int, len(roles))
+		for i, v := range roles {
+			roleIds[i] = v.Id
+		}
+
+		// 查perm列表
+		res = dao.FindPermByRoles(roleIds)
+		if res.Code != result.SUCCESS.Code {
+			c.JSON(http.StatusForbidden, res)
+			c.Abort()
+			return
+		}
+		permissions := res.Data.([]*entity.Perm)
+
+		for _, permission := range permissions {
+			match, _ := regexp.MatchString(permission.Path, c.Request.RequestURI)
+			if match && strings.ToUpper(permission.Method) == c.Request.Method {
+				c.Next()
+				return
+			}
+		}
+
+		c.JSON(http.StatusForbidden, result.NO_PERMISSION)
+		c.Abort()
+		return
+	}
+}

+ 1 - 0
middleware/res/res.go

@@ -0,0 +1 @@
+package res

+ 11 - 0
my.cnf

@@ -0,0 +1,11 @@
+[mysqld]
+user=mysql
+default-storage-engine=INNODB
+character-set-client-handshake=FALSE
+character-set-server=utf8mb4
+collation-server=utf8mb4_unicode_ci
+init_connect='SET NAMES utf8mb4'
+[client]
+default-character-set=utf8mb4
+[mysql]
+default-character-set=utf8mb4

+ 73 - 0
result/result.go

@@ -0,0 +1,73 @@
+package result
+
+type Result struct {
+	Code int32       `json:"code"`
+	Msg  string      `json:"msg"`
+	Data interface{} `json:"data"`
+}
+
+func (errcode *Result) Error() string {
+	return errcode.Msg
+}
+
+func NewResult(Code int32, Msg string, Data interface{}) *Result {
+	return &Result{Code, Msg, Data}
+}
+
+func SuccessResult(Data interface{}) *Result {
+	return &Result{2000, "操作成功", Data}
+}
+
+func (r Result) SetMsg(msg string) *Result {
+	r.Msg = msg
+	return &r
+}
+
+func (r Result) SetData(data interface{}) *Result {
+	r.Data = data
+	return &r
+}
+
+type Page struct {
+	Page  int         `json:"page" db:"page"`
+	Size  int         `json:"size,omitempty" db:"size,omitempty"`
+	Total int         `json:"total,omitempty" db:"total,omitempty"`
+	Data  interface{} `json:"data,omitempty" db:"data,omitempty"`
+}
+
+func NewPage(page, size, total int, data interface{}) *Page {
+	return &Page{
+		page,
+		size,
+		total,
+		data,
+	}
+}
+
+
+// ResultChange 影响行数
+type ResultChange struct {
+	RowsAffected int64 `json:"rowsAffected"`
+	RowsFailed   int64 `json:"rowsFailed"`
+}
+
+func NewResultChange(row int64) ResultChange {
+	return ResultChange{row,0}
+}
+
+var (
+	SUCCESS             = NewResult(2000, "操作成功", nil)
+	UNAUTHORIZED        = NewResult(4001, "未授权", nil)
+	FORMATE_ERROR       = NewResult(4002, "格式转换异常", nil)
+	NO_PERMISSION       = NewResult(4003, "无权限", nil)
+	DATA_NOT_FOUND      = NewResult(4004, "不存在该数据", nil)
+	DATA_NO_CHANGE      = NewResult(4005, "数据未发生改变", nil)
+	PASSWORD_ERROR      = NewResult(4006, "密码错误", nil)
+	PARAM_ERROR         = NewResult(4007, "参数错误", nil)
+	USER_IS_EXISTED     = NewResult(4008, "用户已存在", nil)
+	USER_IS_NOT_EXISTED = NewResult(4009, "用户不存在", nil)
+	USER_PENDING_REVIEW = NewResult(4010, "用户待审核", nil)
+	ROLE_IS_NOT_EXISTED = NewResult(4011, "角色不存在", nil)
+
+	UNKNOW_ERROR = NewResult(5000, "未知异常", nil)
+)

+ 47 - 0
router/router-auth.go

@@ -0,0 +1,47 @@
+package router
+
+import (
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/service"
+	"exam_system/vo"
+	"github.com/gin-gonic/gin"
+)
+
+func Auth(router *RouterPlus) {
+
+	authGroup := router.Group("/auth")
+	{
+		// 注册
+		//authGroup.POST("/register", Register)
+		// 登录
+		authGroup.POST("/login", Login)
+	}
+
+}
+func Register(context *gin.Context) *result.Result {
+	var body struct {
+		entity.User
+		UserType string `json:"user_type,omitempty"`
+	}
+	if err := context.ShouldBindJSON(&body); err != nil {
+		return result.PASSWORD_ERROR
+	}
+	if body.Username == "" || body.Password == "" || body.Sid == "" {
+		return result.PASSWORD_ERROR
+	}
+	status :=entity.UNDER_REVIEW
+	body.Status = &status
+	return service.AddUser(&body.User, body.UserType)
+}
+
+func Login(context *gin.Context) *result.Result {
+	var user vo.UserVo
+	if err := context.ShouldBindJSON(&user); err != nil {
+		return result.PASSWORD_ERROR
+	}
+	if user.RoleName == nil {
+		return result.PARAM_ERROR
+	}
+	return service.Login(&user)
+}

+ 79 - 0
router/router-class.go

@@ -0,0 +1,79 @@
+package router
+
+import (
+	"exam_system/dao"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"github.com/gin-gonic/gin"
+	"strings"
+)
+
+func Class(router *RouterPlus) {
+
+	r := router.Group("/admin")
+	{
+
+		// 添加班级
+		r.POST("/class", AdminAddClass)
+
+		// 获取班级
+		r.GET("/class/:id", AdminGetClass)
+
+		// 获取班级列表(通过termid)
+		r.GET("/class/list", AdminClassList)
+
+		// 修改班级
+		r.PUT("/class", AdminUpdateClass)
+
+		// 删除班级
+		r.DELETE("/class/:ids", AdminDeleteClass)
+	}
+
+}
+
+func AdminAddClass(c *gin.Context) *result.Result {
+	var class entity.Class
+	err := c.ShouldBindJSON(&class)
+	if err != nil || class.TermId == 0 || class.Name == "" {
+		return result.PARAM_ERROR
+	}
+
+	return dao.AddClass(&class)
+}
+
+func AdminGetClass(c *gin.Context) *result.Result {
+	id := c.Param("id")
+	if id == "" {
+		return result.UNKNOW_ERROR
+	}
+	return dao.ClassDetail(id)
+}
+
+func AdminClassList(c *gin.Context) *result.Result {
+	page, size, sort, query, err := utils.Page(c)
+	if err != nil {
+		return result.PARAM_ERROR
+	}
+	return dao.ClassList(page, size, sort, query)
+}
+
+func AdminUpdateClass(c *gin.Context) *result.Result {
+	var class entity.Class
+	err := c.ShouldBindJSON(&class)
+	if err != nil || class.TermId == 0 || class.Name == "" {
+		return result.PARAM_ERROR
+	}
+	return dao.UpdateClass(&class)
+}
+
+func AdminDeleteClass(c *gin.Context) *result.Result {
+	idStr := c.Param("ids")
+	if idStr == "" {
+		return result.PARAM_ERROR
+	}
+
+	ids := strings.Split(idStr, ",")
+
+	return dao.DeleteClasss(ids)
+}

+ 201 - 0
router/router-exam-record.go

@@ -0,0 +1,201 @@
+package router
+
+import (
+	"encoding/json"
+	"exam_system/config"
+	"exam_system/dao"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"exam_system/vo"
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/gin-gonic/gin"
+	"github.com/nats-io/nats.go"
+	"github.com/xuri/excelize/v2"
+)
+
+func ExamRecord(router *RouterPlus) {
+
+	// 添加考试记录管理(开始考试)
+	router.POST("/examRecord", AddExamRecord)
+	// 提交答案
+	router.PUT("/examRecord", UpdateExamRecord)
+	// 获取考试记录列表
+	router.GET("/examRecord/list", ExamRecordList)
+	// 获取考试记录
+	router.GET("/examRecord/:id", GetExamRecord)
+	// 导出考试记录
+	//router.router.GET("/examRecord/download",)
+
+	r := router.Group("/admin")
+	{
+		// 获取考试记录管理
+		r.GET("/examRecord/:id", AdminGetExamRecord)
+
+		// 获取考试记录管理列表
+		r.GET("/examRecord/list", AdminExamRecordList)
+
+		// 删除考试记录管理
+		// r.DELETE("/examRecord/:ids", AdminDeleteExamRecord)
+
+		// 导出考试记录
+		r.routerGroup.GET("/examRecord/download/:id", AdminDownloadExamRecord)
+
+	}
+
+	go UpdateExamRecordTask()
+
+}
+func UpdateExamRecordTask() {
+	config.JS.AddConsumer("EXAMS", &nats.ConsumerConfig{
+		Durable: "MONITOR",
+	})
+	sub, err := config.JS.PullSubscribe("EXAMS.info", "MONITOR")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	for {
+		time.Sleep(time.Second)
+		// 分批从stream拉取消息一次10个
+		msgs, err := sub.Fetch(100)
+		if err != nil && err != nats.ErrTimeout {
+			fmt.Println(err)
+		}
+		for _, msg := range msgs {
+
+			var e entity.Exam
+			err = json.Unmarshal(msg.Data, &e)
+			if err != nil && err != nats.ErrTimeout {
+				fmt.Println(err)
+			}
+
+			if e.EndAt.Before(time.Now()) {
+				// 更新为
+				res := dao.UpdateExamRecordTask(e.Id)
+				if res.Code == result.SUCCESS.Code {
+					err = msg.Ack()
+					if err != nil {
+						fmt.Println(err)
+					}
+				}
+			}
+
+		}
+	}
+}
+
+func AddExamRecord(c *gin.Context) *result.Result {
+	var examRecord entity.ExamRecord
+	err := c.ShouldBindJSON(&examRecord)
+	id, exists := c.Get("id")
+	if !exists {
+		return result.UNAUTHORIZED
+	}
+	examRecord.UserId = id.(int)
+	if err != nil || examRecord.Id == 0 {
+		return result.PARAM_ERROR
+	}
+	examRecord.Ip = strings.Split(c.Request.RemoteAddr, ":")[0]
+
+	return dao.AddExamRecord(&examRecord)
+}
+
+func ExamRecordList(c *gin.Context) *result.Result {
+	page, size, _, query, err := utils.Page(c)
+	if err != nil {
+		return result.PARAM_ERROR
+	}
+	uid, exists := c.Get("id")
+	if !exists {
+		return result.UNAUTHORIZED
+	}
+	return dao.ExamRecordList(page, size, query, uid.(int))
+}
+
+func GetExamRecord(c *gin.Context) *result.Result {
+	idStr := c.Param("id")
+	if idStr == "" {
+		return result.UNKNOW_ERROR
+	}
+	id, err := strconv.Atoi(idStr)
+	if err != nil {
+		return result.UNKNOW_ERROR
+	}
+
+	uid, exists := c.Get("id")
+	if !exists {
+		return result.UNAUTHORIZED
+	}
+	return dao.ExamRecordDetail(id, uid.(int))
+}
+
+func UpdateExamRecord(c *gin.Context) *result.Result {
+	var examRecord vo.ExamRecordVo
+	err := c.ShouldBindJSON(&examRecord)
+	if err != nil || examRecord.Id == 0 || examRecord.AnswerVo == nil || examRecord.Token == "" {
+		return result.PARAM_ERROR
+	}
+	return dao.UpdateExamRecord(&examRecord)
+}
+
+func AdminGetExamRecord(c *gin.Context) *result.Result {
+	idStr := c.Param("id")
+	if idStr == "" {
+		return result.UNKNOW_ERROR
+	}
+	id, err := strconv.Atoi(idStr)
+	if err != nil {
+		return result.UNKNOW_ERROR
+	}
+
+	return dao.AdminExamRecordDetail(id)
+}
+
+func AdminExamRecordList(c *gin.Context) *result.Result {
+	page, size, _, query, err := utils.Page(c)
+	examIdStr := c.Query("exam_id")
+	if err != nil {
+		return result.PARAM_ERROR
+	}
+
+	examId, _ := strconv.Atoi(examIdStr)
+	return dao.AdminExamRecordList(page, size, query, examId)
+}
+
+func AdminDeleteExamRecord(c *gin.Context) *result.Result {
+	idStr := c.Param("ids")
+	if idStr == "" {
+		return result.PARAM_ERROR
+	}
+
+	ids := strings.Split(idStr, ",")
+
+	return dao.DeleteExamRecords(ids)
+}
+
+func AdminDownloadExamRecord(c *gin.Context) {
+
+	idStr := c.Param("id")
+
+	file, err := excelize.OpenFile(utils.ScorePath)
+	if err != nil {
+		c.JSON(http.StatusOK, result.UNKNOW_ERROR.SetMsg(err.Error()))
+	}
+
+	res := dao.AdminDownloadExamRecord(file, idStr)
+	if res.Code != result.SUCCESS.Code {
+		c.JSON(http.StatusOK, res)
+	}
+
+	c.Header("Content-Type", "application/octet-stream")
+	c.Header("Content-Disposition", "attachment; filename=考生信息导出.xlsx")
+	c.Header("Content-Transfer-Encoding", "binary")
+
+	_ = file.Write(c.Writer)
+}

+ 95 - 0
router/router-exam.go

@@ -0,0 +1,95 @@
+package router
+
+import (
+	"exam_system/dao"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"github.com/gin-gonic/gin"
+	"strings"
+)
+
+func Exam(router *RouterPlus) {
+
+	// 查看个人考试列表
+	router.GET("/exam/list",ExamInfoList)
+
+	r := router.Group("/admin")
+	{
+
+		// 添加考试
+		r.POST("/exam", AdminAddExam)
+
+		// 获取考试
+		r.GET("/exam/:id", AdminGetExam)
+
+		// 获取考试列表
+		r.GET("/exam/list", AdminExamList)
+
+		// 修改考试
+		r.PUT("/exam", AdminUpdateExam)
+
+		// 删除考试
+		r.DELETE("/exam/:ids", AdminDeleteExam)
+	}
+
+}
+
+
+func ExamInfoList(c *gin.Context)*result.Result{
+	page, size, sort, query, err := utils.Page(c)
+	if err!=nil{
+		return result.PARAM_ERROR
+	}
+	uid, exists := c.Get("id")
+	if !exists{
+		return result.UNAUTHORIZED
+	}
+	return dao.ExamInfoList(page,size,sort,query,uid.(int))
+}
+
+func AdminAddExam(c *gin.Context) *result.Result {
+	var exam entity.Exam
+	err := c.ShouldBindJSON(&exam)
+	if err != nil || exam.StartAt.IsZero() || exam.EndAt.IsZero() || exam.PublishAt.IsZero() || exam.Duration == 0 || exam.ClassIds == "" || exam.TpId == 0 {
+		return result.PARAM_ERROR
+	}
+
+	return dao.AddExam(&exam)
+}
+
+func AdminGetExam(c *gin.Context) *result.Result {
+	id := c.Param("id")
+	if id == "" {
+		return result.UNKNOW_ERROR
+	}
+	return dao.ExamDetail(id)
+}
+
+func AdminExamList(c *gin.Context) *result.Result {
+	page, size, sort, query, err := utils.Page(c)
+	if err != nil {
+		return result.PARAM_ERROR
+	}
+	return dao.ExamList(page, size, sort, query)
+}
+
+func AdminUpdateExam(c *gin.Context) *result.Result {
+	var exam entity.Exam
+	err := c.ShouldBindJSON(&exam)
+	if err != nil || exam.Id == 0 || exam.StartAt.IsZero() || exam.PublishAt.IsZero() || exam.EndAt.IsZero() || exam.Duration == 0 || exam.ClassIds == "" || exam.TpId == 0 {
+		return result.PARAM_ERROR
+	}
+	return dao.UpdateExam(&exam)
+}
+
+func AdminDeleteExam(c *gin.Context) *result.Result {
+	idStr := c.Param("ids")
+	if idStr == "" {
+		return result.PARAM_ERROR
+	}
+
+	ids := strings.Split(idStr, ",")
+
+	return dao.DeleteExams(ids)
+}

+ 91 - 0
router/router-plus.go

@@ -0,0 +1,91 @@
+package router
+
+import (
+	"exam_system/result"
+	"github.com/gin-gonic/gin"
+
+	"net/http"
+)
+
+/**
+* @creator: xuwuruoshui
+* @date: 2022-06-08 22:27:02
+* @content: router封装
+ */
+
+type RouterPlus struct {
+	router *gin.Engine
+}
+
+func NewRouterPlus(engine *gin.Engine) *RouterPlus {
+	return &RouterPlus{
+		router: engine,
+	}
+}
+
+type RouterHandler func(ctx *gin.Context) *result.Result
+
+func (r *RouterPlus) GET(path string, handler RouterHandler, handlers ...gin.HandlerFunc) {
+	postHanlder := PostHandler(handler)
+	handlers = append(handlers, postHanlder)
+	r.router.GET(path, handlers...)
+}
+
+func (r *RouterPlus) POST(path string, handler RouterHandler, handlers ...gin.HandlerFunc) {
+	postHanlder := PostHandler(handler)
+	handlers = append(handlers, postHanlder)
+	r.router.POST(path, handlers...)
+}
+
+func (r *RouterPlus) PUT(path string, handler RouterHandler, handlers ...gin.HandlerFunc) {
+	postHanlder := PostHandler(handler)
+	handlers = append(handlers, postHanlder)
+	r.router.PUT(path, handlers...)
+}
+
+func (r *RouterPlus) DELETE(path string, handler RouterHandler, handlers ...gin.HandlerFunc) {
+	postHanlder := PostHandler(handler)
+	handlers = append(handlers, postHanlder)
+	r.router.DELETE(path, handlers...)
+}
+
+func (r *RouterPlus) Group(path string, handlers ...gin.HandlerFunc) *RouterGroupPlus {
+	return &RouterGroupPlus{
+		routerGroup: r.router.Group(path, handlers...),
+	}
+}
+
+type RouterGroupPlus struct {
+	routerGroup *gin.RouterGroup
+}
+
+func (r *RouterGroupPlus) GET(path string, handler RouterHandler, handlers ...gin.HandlerFunc) {
+	postHanlder := PostHandler(handler)
+	handlers = append(handlers, postHanlder)
+	r.routerGroup.GET(path, handlers...)
+}
+
+func (r *RouterGroupPlus) POST(path string, handler RouterHandler, handlers ...gin.HandlerFunc) {
+	postHanlder := PostHandler(handler)
+	handlers = append(handlers, postHanlder)
+	r.routerGroup.POST(path, handlers...)
+}
+
+func (r *RouterGroupPlus) PUT(path string, handler RouterHandler, handlers ...gin.HandlerFunc) {
+	postHanlder := PostHandler(handler)
+	handlers = append(handlers, postHanlder)
+	r.routerGroup.PUT(path, handlers...)
+}
+
+func (r *RouterGroupPlus) DELETE(path string, handler RouterHandler, handlers ...gin.HandlerFunc) {
+	postHanlder := PostHandler(handler)
+	handlers = append(handlers, postHanlder)
+	r.routerGroup.DELETE(path, handlers...)
+}
+
+// PostHandler 默认返回200
+func PostHandler(handler RouterHandler) gin.HandlerFunc {
+	return func(ctx *gin.Context) {
+		ctx.JSON(http.StatusOK, handler(ctx))
+	}
+}

+ 79 - 0
router/router-question-bank.go

@@ -0,0 +1,79 @@
+package router
+
+import (
+	"exam_system/dao"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"github.com/gin-gonic/gin"
+	"strings"
+)
+
+func QuestionBank(router *RouterPlus) {
+
+	r := router.Group("/admin")
+	{
+
+		// 添加题库
+		r.POST("/questionBank", AdminAddQuestionBank)
+
+		// 获取题库
+		r.GET("/questionBank/:id", AdminGetQuestionBank)
+
+		// 获取题库列表
+		r.GET("/questionBank/list", AdminQuestionBankList)
+
+		// 修改题库
+		r.PUT("/questionBank", AdminUpdateQuestionBank)
+
+		// 删除题库
+		r.DELETE("/questionBank/:ids", AdminDeleteQuestionBank)
+	}
+
+}
+
+func AdminAddQuestionBank(c *gin.Context) *result.Result {
+	var questionBank entity.QuestionBank
+	err := c.ShouldBindJSON(&questionBank)
+	if err != nil || questionBank.Name == "" {
+		return result.PARAM_ERROR
+	}
+
+	return dao.AddQuestionBank(&questionBank)
+}
+
+func AdminGetQuestionBank(c *gin.Context) *result.Result {
+	id := c.Param("id")
+	if id == "" {
+		return result.UNKNOW_ERROR
+	}
+	return dao.QuestionBankDetail(id)
+}
+
+func AdminQuestionBankList(c *gin.Context) *result.Result {
+	page, size, sort, query, err := utils.Page(c)
+	if err != nil {
+		return result.PARAM_ERROR
+	}
+	return dao.QuestionBankList(page, size, sort, query)
+}
+
+func AdminUpdateQuestionBank(c *gin.Context) *result.Result {
+	var questionBank entity.QuestionBank
+	err := c.ShouldBindJSON(&questionBank)
+	if err != nil || questionBank.Id == 0 || questionBank.Name == "" {
+		return result.PARAM_ERROR
+	}
+	return dao.UpdateQuestionBank(&questionBank)
+}
+
+func AdminDeleteQuestionBank(c *gin.Context) *result.Result {
+	idStr := c.Param("ids")
+	if idStr == "" {
+		return result.PARAM_ERROR
+	}
+
+	ids := strings.Split(idStr, ",")
+
+	return dao.DeleteQuestionBanks(ids)
+}

+ 194 - 0
router/router-subject.go

@@ -0,0 +1,194 @@
+package router
+
+import (
+	"exam_system/dao"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"github.com/xuri/excelize/v2"
+	"net/http"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+func Subject(router *RouterPlus) {
+
+	r := router.Group("/admin")
+	{
+
+		// 添加试题
+		r.POST("/subject", AdminAddSubject)
+
+		// 获取试题
+		r.GET("/subject/:id", AdminGetSubject)
+
+		// 获取试题列表
+		r.GET("/subject/list", AdminSubjectList)
+
+		// 修改试题
+		r.PUT("/subject", AdminUpdateSubject)
+
+		// 删除试题
+		r.DELETE("/subject/:ids", AdminDeleteSubject)
+
+		// 上传试题
+		r.POST("/subject/upload/:id", AdminUploadSuject)
+
+		// 下载试题
+		r.routerGroup.GET("/subject/:id/:type", AdminDownloadSubject)
+
+		// 下载试题模板
+		r.routerGroup.GET("/subject/template", AdminSubjectTemplate)
+
+	}
+
+}
+
+func AdminAddSubject(c *gin.Context) *result.Result {
+	var subject entity.Subject
+	err := c.ShouldBindJSON(&subject)
+	if err != nil || !CheckSubject(&subject) {
+		return result.PARAM_ERROR
+	}
+
+	return dao.AddSubject(&subject)
+}
+
+func AdminGetSubject(c *gin.Context) *result.Result {
+	id := c.Param("id")
+	if id == "" {
+		return result.UNKNOW_ERROR
+	}
+	return dao.SubjectDetail(id)
+}
+
+func AdminSubjectList(c *gin.Context) *result.Result {
+	page, size, sort, query, err := utils.Page(c)
+
+	if err != nil {
+		return result.PARAM_ERROR
+	}
+	return dao.SubjectList(page, size, sort, query)
+}
+
+func AdminUpdateSubject(c *gin.Context) *result.Result {
+	var subject entity.Subject
+	err := c.ShouldBindJSON(&subject)
+	if err != nil || subject.Id == 0 || subject.Type == nil {
+		return result.PARAM_ERROR
+	}
+	return dao.UpdateSubject(&subject)
+}
+
+func AdminDeleteSubject(c *gin.Context) *result.Result {
+
+	idStr := c.Param("ids")
+	if idStr == "" {
+		return result.PARAM_ERROR
+	}
+
+	ids := strings.Split(idStr, ",")
+
+	return dao.DeleteSubjects(ids)
+}
+
+func CheckSubject(s *entity.Subject) bool {
+
+	if s.Type == nil || s.QbId == 0 || s.Question == "" {
+		return false
+	}
+
+	s.Answer = strings.TrimSpace(s.Answer)
+
+	if *s.Type == 0 || *s.Type == 1 {
+		match, _ := regexp.MatchString("^[1-4]?$", s.Answer)
+		if s.OptA == "" || s.OptB == "" || s.OptC == "" || s.OptD == "" || !match {
+			return false
+		}
+	}
+
+	if *s.Type == 2 {
+		if s.OptA == "" || s.OptB == "" || s.OptC == "" || s.OptD == "" {
+			return false
+		}
+		match1, _ := regexp.MatchString("^[1-4]{0,4}$", s.Answer)
+		match2, _ := regexp.MatchString(`([\s\S])[\s\S]*?\1`, s.Answer)
+		if !match1 || match2 {
+			return false
+		}
+	}
+
+	if *s.Type == 3 {
+		match, _ := regexp.MatchString("^[1-2]?$", s.Answer)
+		if !match {
+			return false
+		}
+	}
+	return true
+}
+
+func AdminUploadSuject(c *gin.Context) *result.Result {
+
+	id := c.Param("id")
+	fh, _ := c.FormFile("file")
+	file, err := fh.Open()
+	if err != nil {
+		return result.UNKNOW_ERROR.SetMsg(err.Error())
+	}
+
+	return dao.AddSubjectBatch(file, id)
+}
+
+func AdminSubjectTemplate(c *gin.Context) {
+
+	c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", "试题模板.zip"))
+	c.Writer.Header().Set("Content-Type", "application/zip")
+	c.File(utils.SubTemplatePath)
+}
+
+func AdminDownloadSubject(c *gin.Context) {
+	idStr := c.Param("id")
+	tpStr := c.Param("type")
+
+	id, err := strconv.Atoi(idStr)
+	if err != nil {
+		c.JSON(http.StatusOK, result.UNKNOW_ERROR.SetMsg(err.Error()))
+	}
+	tp, err := strconv.Atoi(tpStr)
+	if err != nil {
+		c.JSON(http.StatusOK, result.UNKNOW_ERROR.SetMsg(err.Error()))
+	}
+
+	var path string
+	switch tp {
+	case entity.SINGEL_CHOICE:
+		path = utils.SingelChoicePath
+	case entity.Judgement:
+		path = utils.JudgementPath
+	case entity.MULTIPLE_CHOICE:
+		path = utils.MultipleChoicePath
+	case entity.Completion:
+		path = utils.CompletionPath
+	}
+
+	file, err := excelize.OpenFile(path)
+	if err != nil {
+		c.JSON(http.StatusOK, result.UNKNOW_ERROR.SetMsg(err.Error()))
+	}
+
+	res := dao.DownloadSubject(file, id, tp)
+	if res.Code != result.SUCCESS.Code {
+		file.Close()
+		c.JSON(http.StatusOK, res)
+	}else{
+		c.Header("Content-Type", "application/octet-stream")
+		c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s.xlsx", res.Data.(string)))
+		c.Header("Content-Transfer-Encoding", "binary")
+		_ = file.Write(c.Writer)
+	}
+
+
+}

+ 78 - 0
router/router-term.go

@@ -0,0 +1,78 @@
+package router
+
+import (
+	"exam_system/dao"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/utils"
+	"github.com/gin-gonic/gin"
+	"strings"
+)
+
+func Term(router *RouterPlus) {
+
+	r := router.Group("/admin")
+	{
+		// 添加学期
+		r.POST("/term", AdminAddTerm)
+
+		// 获取学期
+		r.GET("/term/:id", AdminGetTerm)
+
+		// 获取学期列表
+		r.GET("/term/list", AdminTermList)
+
+		// 修改学期
+		r.PUT("/term", AdminUpdateTerm)
+
+		// 删除学期
+		r.DELETE("/term/:ids", AdminDeleteTerm)
+	}
+
+}
+
+func AdminAddTerm(c *gin.Context) *result.Result {
+	var term entity.Term
+	err := c.ShouldBindJSON(&term)
+	if err != nil || term.Name == "" {
+		return result.PARAM_ERROR
+	}
+
+	return dao.AddTerm(&term)
+}
+
+func AdminGetTerm(c *gin.Context) *result.Result {
+	id := c.Param("id")
+	if id == "" {
+		return result.UNKNOW_ERROR
+	}
+	return dao.TermDetail(id)
+}
+
+func AdminTermList(c *gin.Context) *result.Result {
+	page, size, sort, query, err := utils.Page(c)
+	if err != nil {
+		return result.PARAM_ERROR
+	}
+	return dao.TermList(page, size, sort, query)
+}
+
+func AdminUpdateTerm(c *gin.Context) *result.Result {
+	var term entity.Term
+	err := c.ShouldBindJSON(&term)
+	if err != nil || term.Id == 0 {
+		return result.PARAM_ERROR
+	}
+	return dao.UpdateTerm(&term)
+}
+
+func AdminDeleteTerm(c *gin.Context) *result.Result {
+	idStr := c.Param("ids")
+	if idStr == "" {
+		return result.PARAM_ERROR
+	}
+
+	ids := strings.Split(idStr, ",")
+
+	return dao.DeleteTerms(ids)
+}

+ 87 - 0
router/router-test-paper.go

@@ -0,0 +1,87 @@
+package router
+
+import (
+	"exam_system/dao"
+	"exam_system/result"
+	"exam_system/utils"
+	"exam_system/vo"
+	"github.com/gin-gonic/gin"
+	"strings"
+)
+
+func TestPaper(router *RouterPlus) {
+
+	router.GET("/testPaper/:id", GetTestPaper)
+	r := router.Group("/admin")
+	{
+		// 添加试卷
+		r.POST("/testPaper", AdminAddTestPaper)
+
+		// 获取试卷
+		r.GET("/testPaper/:id", AdminGetTestPaper)
+
+		// 获取试卷列表
+		r.GET("/testPaper/list", AdminTestPaperList)
+
+		// 修改试卷
+		r.PUT("/testPaper", AdminUpdateTestPaper)
+
+		// 删除试卷
+		r.DELETE("/testPaper/:ids", AdminDeleteTestPaper)
+	}
+
+}
+
+func GetTestPaper(c *gin.Context) *result.Result {
+	id := c.Param("id")
+	if id == "" {
+		return result.UNKNOW_ERROR
+	}
+	return dao.TestPaperDetail2(id)
+}
+
+func AdminAddTestPaper(c *gin.Context) *result.Result {
+	var testPaper vo.TestPaperVo
+	err := c.ShouldBindJSON(&testPaper)
+	if err != nil || testPaper.Name=="" || len(testPaper.Chapters)<1{
+		return result.PARAM_ERROR
+	}
+
+	return dao.AddTestPaper(&testPaper)
+}
+
+func AdminGetTestPaper(c *gin.Context) *result.Result {
+	id := c.Param("id")
+	if id == "" {
+		return result.UNKNOW_ERROR
+	}
+	return dao.TestPaperDetail(id)
+}
+
+func AdminTestPaperList(c *gin.Context) *result.Result {
+	page, size, sort, query, err := utils.Page(c)
+	if err != nil {
+		return result.PARAM_ERROR
+	}
+	return dao.TestPaperList(page, size, sort, query)
+}
+
+func AdminUpdateTestPaper(c *gin.Context) *result.Result {
+	var testPaper vo.TestPaperVo
+	err := c.ShouldBindJSON(&testPaper)
+	if err != nil || testPaper.Id == 0 || len(testPaper.Chapters)<1{
+		return result.PARAM_ERROR
+	}
+	return dao.UpdateTestPaper(&testPaper)
+}
+
+func AdminDeleteTestPaper(c *gin.Context) *result.Result {
+	idStr := c.Param("ids")
+	if idStr == "" {
+		return result.PARAM_ERROR
+	}
+
+	ids := strings.Split(idStr, ",")
+
+	return dao.DeleteTestPapers(ids)
+}

+ 218 - 0
router/router-user.go

@@ -0,0 +1,218 @@
+package router
+
+import (
+	"exam_system/dao"
+	"exam_system/entity"
+	"exam_system/result"
+	"exam_system/service"
+	"exam_system/utils"
+	"exam_system/vo"
+	"github.com/gin-gonic/gin"
+	"github.com/xuri/excelize/v2"
+	"net/http"
+	"strconv"
+	"strings"
+)
+
+func User(router *RouterPlus) {
+
+	// 用户个人(管理员,用户)
+	router.GET("/user", UserInfo)
+	router.PUT("/user", UpdateUser)
+
+	r := router.Group("/admin")
+	{
+		// 用户详情
+		r.GET("/user/:sid", AdminUserDetail)
+
+		// 用户列表
+		r.GET("/user/list", AdminUserList)
+
+		// 添加用户
+		r.POST("/user", AdminAddUser)
+
+		// 修改用户信息
+		r.PUT("/user", AdminUpdateUser)
+
+		// 删除用户
+		r.DELETE("/user/:ids", AdminDeleteUser)
+
+		// 导入用户
+		r.POST("/user/upload", AdminUserImport)
+
+		// 导出用户
+		r.routerGroup.GET("/user/download", AdminDownload)
+
+		// 模板下载
+		r.routerGroup.GET("/user/template", AdminUserTemplate)
+	}
+
+}
+
+// 用户操作
+func UserInfo(c *gin.Context) *result.Result {
+	id, exists := c.Get("id")
+	if !exists {
+
+		return result.USER_IS_NOT_EXISTED
+	}
+	return dao.FindUserbyId(id.(int))
+}
+
+func UpdateUser(c *gin.Context) *result.Result {
+	var body struct {
+		entity.User
+		UserType string `json:"user_type,omitempty"`
+	}
+	if err := c.ShouldBindJSON(&body); err != nil {
+		return result.PASSWORD_ERROR
+	}
+
+	status := entity.UNDER_REVIEW
+	body.Status = &status
+	roleNames := c.GetStringSlice("role")
+	for _, roleName := range roleNames {
+		if roleName == "admin" {
+			status = entity.NORMAL
+			body.Status = &status
+			break
+		}
+	}
+
+	id := c.GetInt("id")
+	body.ID = id
+
+	return dao.UpdateUser(&body.User, body.UserType)
+}
+
+// 管理员操作
+
+func AdminUserDetail(c *gin.Context) *result.Result {
+	sid := c.Param("sid")
+	if sid == "" {
+		return result.PARAM_ERROR
+	}
+	res := dao.FindUserbySid(sid)
+	if res.Code != result.SUCCESS.Code {
+		return res
+	}
+	userVo := res.Data.(vo.UserVo)
+	userVo.Password = ""
+
+	return res.SetData(userVo)
+}
+
+func AdminUserList(c *gin.Context) *result.Result {
+	page, size, sort, query, err := utils.Page(c)
+	if err != nil {
+		return result.PARAM_ERROR
+	}
+	return dao.FindUserList(page, size, sort, query)
+}
+
+func AdminAddUser(c *gin.Context) *result.Result {
+	var body struct {
+		entity.User
+		UserType string `json:"user_type,omitempty"`
+	}
+	if err := c.ShouldBindJSON(&body); err != nil {
+		return result.PARAM_ERROR
+	}
+	if body.Username == "" || body.Password == "" || body.Sid == "" {
+		return result.PARAM_ERROR
+	}
+	status := entity.NORMAL
+	body.Status = &status
+	return service.AddUser(&body.User, body.UserType)
+}
+
+func AdminUpdateUser(c *gin.Context) *result.Result {
+	var body struct {
+		entity.User
+		UserType string `json:"user_type,omitempty"`
+	}
+	if err := c.ShouldBindJSON(&body); err != nil {
+		return result.PARAM_ERROR
+	}
+	if body.ID == 0 {
+		return result.PARAM_ERROR
+	}
+
+	status := entity.NORMAL
+	body.Status = &status
+
+	return dao.UpdateUser(&body.User, body.UserType)
+}
+
+func AdminDeleteUser(c *gin.Context) *result.Result {
+	idStr := c.Param("ids")
+	if idStr == "" {
+		return result.PARAM_ERROR
+	}
+
+	ids := strings.Split(idStr, ",")
+
+	return dao.DeleteUsers(ids)
+}
+
+func AdminUserImport(c *gin.Context) *result.Result {
+
+	fh, _ := c.FormFile("file")
+	termIdStr := c.PostForm("term_id")
+	classIdStr := c.PostForm("class_id")
+	file, err := fh.Open()
+
+	if termIdStr != "" {
+		var termId int
+		termId, err = strconv.Atoi(termIdStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		var classId int
+		if classIdStr != "" {
+			classId, err = strconv.Atoi(classIdStr)
+			if err != nil {
+				return result.UNKNOW_ERROR.SetMsg(err.Error())
+			}
+		}
+		return dao.InsertBatchUserByTermId(file, termId, classId)
+	}
+
+	if classIdStr != "" {
+		var classId int
+		classId, err = strconv.Atoi(classIdStr)
+		if err != nil {
+			return result.UNKNOW_ERROR.SetMsg(err.Error())
+		}
+
+		return dao.InsertBatchUserByClassId(file, classId)
+	}
+
+	return dao.InsertBatchUser(file)
+}
+
+func AdminDownload(c *gin.Context) {
+	file, err := excelize.OpenFile(utils.StudentPath)
+	if err != nil {
+		c.JSON(http.StatusOK, result.UNKNOW_ERROR.SetMsg(err.Error()))
+	}
+
+	res := dao.DownloadUserInfo(file)
+	if res.Code != result.SUCCESS.Code {
+		c.JSON(http.StatusOK, res)
+	}
+
+	c.Header("Content-Type", "application/octet-stream")
+	c.Header("Content-Disposition", "attachment; filename=考生信息导出.xlsx")
+	c.Header("Content-Transfer-Encoding", "binary")
+
+	_ = file.Write(c.Writer)
+}
+
+func AdminUserTemplate(c *gin.Context) {
+
+	c.Writer.Header().Add("Content-Disposition", "attachment; filename=试题模板.zip")
+	c.Writer.Header().Set("Content-Type", "application/zip")
+	c.File(utils.StuTemplatePath)
+}

+ 29 - 0
router/router.go

@@ -0,0 +1,29 @@
+package router
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func RouterEntry(engine *gin.Engine) {
+
+	routerPlus := NewRouterPlus(engine)
+
+	// 授权
+	Auth(routerPlus)
+	// 用户模块
+	User(routerPlus)
+	// 学期
+	Term(routerPlus)
+	// 班级
+	Class(routerPlus)
+	// 题库
+	QuestionBank(routerPlus)
+	// 试题
+	Subject(routerPlus)
+	// 试卷
+	TestPaper(routerPlus)
+	// 考试
+	Exam(routerPlus)
+	// 考试记录
+	ExamRecord(routerPlus)
+}

+ 1 - 0
service/service-subject.go

@@ -0,0 +1 @@
+package service

+ 73 - 0
service/service-user.go

@@ -0,0 +1,73 @@
+package service
+
+import (
+	"exam_system/dao"
+	"exam_system/entity"
+	"exam_system/middleware/claims"
+	"exam_system/result"
+	"exam_system/vo"
+	"github.com/gin-gonic/gin"
+	"golang.org/x/crypto/bcrypt"
+	"strings"
+)
+
+func Login(u *vo.UserVo) *result.Result {
+	// 1、查询
+	res := dao.FindUserbySid(u.Sid)
+	if res.Data == nil {
+		return res
+	}
+
+	// 2、查询角色
+	userVo := res.Data.(vo.UserVo)
+	roleNames := strings.Split(*userVo.RoleName, ",")
+
+	// 3、解密
+	err := bcrypt.CompareHashAndPassword([]byte(userVo.Password), []byte(u.Password))
+	if err != nil {
+		return result.PASSWORD_ERROR
+	}
+
+	// 4、判断角色是否存在
+	roleExist := false
+	for _, roleName := range roleNames {
+		if roleName == *u.RoleName {
+			roleExist = true
+			break
+		}
+	}
+	if !roleExist {
+		return result.USER_IS_NOT_EXISTED
+	}
+
+	if *userVo.Status != entity.NORMAL {
+		return result.USER_PENDING_REVIEW
+	}
+
+	// 4、生产token
+	token, err := claims.GenToken(userVo.ID, userVo.Username, userVo.Sid, roleNames)
+	if err != nil {
+		return result.UNKNOW_ERROR
+	}
+	userVo.Password = ""
+
+	return result.SUCCESS.SetData(gin.H{
+		"token": token,
+		"user":  userVo,
+	})
+}
+
+// AddUser 管理员,用户公用
+func AddUser(u *entity.User, userTypes string) *result.Result {
+	// 1、加密
+	hash, _ := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
+	u.Password = string(hash)
+
+	// 2、插入用户表
+	res := dao.InsertUser(u, userTypes)
+	if res != result.SUCCESS {
+		return res
+	}
+
+	return res
+}

+ 153 - 0
test.sql

@@ -0,0 +1,153 @@
+/*
+ Navicat Premium Data Transfer
+
+ Source Server         : user
+ Source Server Type    : MySQL
+ Source Server Version : 80028
+ Source Host           : 120.78.159.42:3306
+ Source Schema         : user
+
+ Target Server Type    : MySQL
+ Target Server Version : 80028
+ File Encoding         : 65001
+
+ Date: 12/04/2022 22:28:08
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for perm
+-- ----------------------------
+DROP TABLE IF EXISTS `perm`;
+CREATE TABLE `perm`  (
+  `id` int(0) NOT NULL AUTO_INCREMENT,
+  `path` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+  `create_at` timestamp(0) NULL DEFAULT NULL,
+  `update_at` timestamp(0) NULL DEFAULT NULL,
+  `delete_at` timestamp(0) NULL DEFAULT NULL,
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of perm
+-- ----------------------------
+INSERT INTO `perm` VALUES (1, '/user', '2022-04-12 21:01:34', '2022-04-12 21:01:37', NULL);
+INSERT INTO `perm` VALUES (2, '/test2', '2022-04-12 21:01:46', '2022-04-12 21:01:47', NULL);
+
+-- ----------------------------
+-- Table structure for perm_role
+-- ----------------------------
+DROP TABLE IF EXISTS `perm_role`;
+CREATE TABLE `perm_role`  (
+  `id` int(0) NOT NULL AUTO_INCREMENT,
+  `perm_id` int(0) NOT NULL,
+  `role_id` int(0) NOT NULL,
+  `create_at` timestamp(0) NOT NULL,
+  `update_at` timestamp(0) NOT NULL,
+  `delete_at` timestamp(0) NULL DEFAULT NULL,
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of perm_role
+-- ----------------------------
+INSERT INTO `perm_role` VALUES (1, 1, 1, '2022-04-12 21:02:14', '2022-04-12 21:02:16', NULL);
+INSERT INTO `perm_role` VALUES (2, 1, 2, '2022-04-12 21:05:23', '2022-04-12 21:05:25', NULL);
+INSERT INTO `perm_role` VALUES (3, 2, 2, '2022-04-12 21:06:50', '2022-04-12 21:06:52', NULL);
+
+-- ----------------------------
+-- Table structure for role
+-- ----------------------------
+DROP TABLE IF EXISTS `role`;
+CREATE TABLE `role`  (
+  `id` int(0) NOT NULL AUTO_INCREMENT,
+  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+  `create_at` timestamp(0) NULL DEFAULT NULL,
+  `update_at` timestamp(0) NULL DEFAULT NULL,
+  `delete_at` timestamp(0) NULL DEFAULT NULL,
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE INDEX `name_unique`(`name`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of role
+-- ----------------------------
+INSERT INTO `role` VALUES (1, 'normal', '2022-04-12 21:01:15', '2022-04-12 21:03:36', NULL);
+INSERT INTO `role` VALUES (2, 'admin', '2022-04-12 21:03:31', '2022-04-12 21:04:22', NULL);
+
+-- ----------------------------
+-- Table structure for user
+-- ----------------------------
+DROP TABLE IF EXISTS `user`;
+CREATE TABLE `user`  (
+  `id` int(0) NOT NULL AUTO_INCREMENT,
+  `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+  `password` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+  `create_at` timestamp(0) NULL DEFAULT NULL,
+  `update_at` timestamp(0) NULL DEFAULT NULL,
+  `delete_at` timestamp(0) NULL DEFAULT NULL,
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE INDEX `username`(`username`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of user
+-- ----------------------------
+INSERT INTO `user` VALUES (1, 'root', 'root', '2022-04-12 20:58:50', '2022-04-12 20:58:52', NULL);
+INSERT INTO `user` VALUES (2, 'aaa', 'aaa', '2022-04-12 21:03:06', '2022-04-12 21:03:08', NULL);
+
+-- ----------------------------
+-- Table structure for user_role
+-- ----------------------------
+DROP TABLE IF EXISTS `user_role`;
+CREATE TABLE `user_role`  (
+  `id` int(0) NOT NULL AUTO_INCREMENT,
+  `user_id` int(0) NOT NULL,
+  `role_id` int(0) NOT NULL,
+  `create_at` timestamp(0) NOT NULL,
+  `update_at` timestamp(0) NOT NULL,
+  `delete_at` timestamp(0) NULL DEFAULT NULL,
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of user_role
+-- ----------------------------
+INSERT INTO `user_role` VALUES (1, 1, 1, '2022-04-12 21:02:04', '2022-04-12 21:02:06', NULL);
+INSERT INTO `user_role` VALUES (2, 1, 2, '2022-04-12 21:07:36', '2022-04-12 21:07:37', NULL);
+INSERT INTO `user_role` VALUES (3, 2, 1, '2022-04-12 21:08:27', '2022-04-12 21:08:30', NULL);
+
+-- ----------------------------
+-- Procedure structure for insert_tb_item
+-- ----------------------------
+DROP PROCEDURE IF EXISTS `insert_tb_item`;
+delimiter ;;
+CREATE PROCEDURE `insert_tb_item`(num int)
+begin
+while num <= 10000000 do
+insert into tb_item values(num,concat('',num,''),round(RAND() * 100000,2),FLOOR(RAND() * 100000),FLOOR(RAND() * 10),'1','5435343235','2019-04-20 22:37:15','2019-04-20 22:37:15');
+set num = num + 1;
+end while;
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Procedure structure for test_add
+-- ----------------------------
+DROP PROCEDURE IF EXISTS `test_add`;
+delimiter ;;
+CREATE PROCEDURE `test_add`(n int)
+BEGIN
+DECLARE num int DEFAULT(0);
+WHILE num<=n DO
+SET num = num+1;
+insert into test(`id`,`name`,`email`) VALUES(num,num,num);
+END WHILE;
+END
+;;
+delimiter ;
+
+SET FOREIGN_KEY_CHECKS = 1;

+ 102 - 0
utils/utils.go

@@ -0,0 +1,102 @@
+package utils
+
+import (
+	"encoding/json"
+	"exam_system/result"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"github.com/jmoiron/sqlx"
+	"sort"
+	"strconv"
+	"strings"
+)
+
+const (
+	StudentPath        = "./file/考生信息导出.xlsx"
+	ScorePath          = "./file/成绩导出.xlsx"
+	SubTemplatePath    = "./file/试题模板.zip"
+	StuTemplatePath    = "./file/考生模板.zip"
+	SingelChoicePath   = "./file/单选题.xlsx"
+	CompletionPath     = "./file/填空题.xlsx"
+	MultipleChoicePath = "./file/多选题.xlsx"
+	JudgementPath      = "./file/判断题.xlsx"
+)
+
+// Page 分页解析
+func Page(c *gin.Context) (page, size int, sort string, query map[string]interface{}, err error) {
+	sizeStr := c.Query("size")
+	pageStr := c.Query("page")
+	queryStr := c.Query("query")
+	sort = c.Query("sort")
+
+	size, err = strconv.Atoi(sizeStr)
+	if err != nil {
+		return
+	}
+	page, err = strconv.Atoi(pageStr)
+	if err != nil {
+		return
+	}
+
+	query = make(map[string]interface{})
+	if queryStr != "" {
+		err = json.Unmarshal([]byte(queryStr), &query)
+		if err != nil {
+			return
+		}
+	}
+
+	return
+}
+
+func PostionSize(page, size int) int {
+	position := (page - 1) * size
+	return position
+}
+
+// Transation 事务
+func Transation(tx *sqlx.Tx, err error, fn func(tx *sqlx.Tx) *result.Result) *result.Result {
+	defer func() {
+		if p := recover(); p != nil {
+			tx.Rollback()
+			panic(p) // re-throw panic after Rollback
+		} else if err != nil {
+			fmt.Println("rollback")
+			tx.Rollback() // err is non-nil; don't change it
+		} else {
+			err = tx.Commit() // err is nil; if Commit returns error update err
+			fmt.Println("commit")
+		}
+	}()
+	return fn(tx)
+}
+
+// SubjectFormat A转0
+func SubjectFormat(answer string) string {
+
+	answer = strings.ToUpper(answer)
+	answer = strings.Replace(answer, "A", "1", 1)
+	answer = strings.Replace(answer, "B", "2", 1)
+	answer = strings.Replace(answer, "C", "3", 1)
+	answer = strings.Replace(answer, "D", "4", 1)
+	// 排序
+	return SubjectSort(answer)
+}
+
+func SubjectSort(answer string) string{
+	slice := strings.Split(answer, "")
+	sort.Strings(slice)
+	answer = strings.Join(slice,"")
+	return answer
+}
+
+// SubjectParse 0转A
+func SubjectParse(answer string)string{
+
+	answer = strings.Replace(answer, "1", "A", 1)
+	answer = strings.Replace(answer, "2","B" , 1)
+	answer = strings.Replace(answer, "3", "C", 1)
+	answer = strings.Replace(answer, "4", "D", 1)
+
+	return answer
+}

+ 10 - 0
vo/class_vo.go

@@ -0,0 +1,10 @@
+package vo
+
+import "exam_system/entity"
+
+type ClassVo struct {
+	*entity.Class
+	TermName string `json:"term_name,omitempty" db:"term_name,omitempty"`
+	// 班级人数
+	UserNum *int `json:"user_num,omitempty" db:"user_num,omitempty"`
+}

+ 54 - 0
vo/exam_record_vo.go

@@ -0,0 +1,54 @@
+package vo
+
+import (
+	"exam_system/entity"
+	"time"
+)
+
+// ExamRecordVo 提交考试时使用
+type ExamRecordVo struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+	Score   *int 		`json:"score,omitempty" db:"score,omitempty"`
+	UserId  int			`json:"user_id,omitempty" db:"user_id,omitempty"`
+	ExamId int 			`json:"exam_id,omitempty" db:"exam_id,omitempty"`
+	// 记录考试状态,防止用户恶意修改考试记录
+	Token string 		`json:"token,omitempty" db:"token,omitempty"`
+	AnswerVo []*AnswerVo `json:"answer,omitempty"`
+}
+
+
+type AnswerVo struct {
+	Id *int	`json:"id,omitempty" db:"id,omitempty"`
+	// 个人答案
+	OwnAnswer string `json:"own_answer,omitempty" db:"own_answer,omitempty"`
+	*entity.Subject
+}
+
+// ExamRecordVo1 查询列表时使用
+type ExamRecordVo1 struct {
+
+	*entity.ExamRecord
+	*ExamVo1
+	Score *int `json:"score,omitempty" db:"score,omitempty"`
+	Username *string `json:"username,omitempty" db:"username,omitempty"`
+	UserScore *int `json:"user_score,omitempty" db:"user_score,omitempty"`
+	UserTakeTime *int `json:"user_take_time,omitempty" db:"user_take_time,omitempty"`
+	ClassName *string `json:"classname,omitempty" db:"classname,omitempty"`
+}
+
+// ExamRecordVo2 导出报表时使用
+type ExamRecordVo2 struct {
+	ExamName string `json:"exam_name,omitempty" db:"exam_name,omitempty"`
+	Username string `json:"username,omitempty" db:"username,omitempty"`
+	TermClass *string `json:"term_class,omitempty" db:"term_class,omitempty"`
+	Name string `json:"name,omitempty" db:"name,omitempty"`
+	Sid string `json:"sid,omitempty" db:"sid,omitempty"`
+	CreateAt time.Time `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time `json:"update_at,omitempty" db:"update_at,omitempty"`
+	Ip       *string		`json:"ip,omitempty" db:"ip,omitempty"`
+	UserScore *int		`json:"user_score,omitempty" db:"user_score,omitempty"`
+	UserTakeTime *int 	`json:"user_take_time,omitempty" db:"user_take_time,omitempty"`
+}

+ 19 - 0
vo/exam_vo.go

@@ -0,0 +1,19 @@
+package vo
+
+import "exam_system/entity"
+
+type ExamVo struct {
+	*entity.Exam
+	// 试卷名称
+	TpName string `json:"tp_name,omitempty" db:"tp_name,omitempty"`
+	Score  *int `json:"score,omitempty" db:"score,omitempty"`
+	Classes []*entity.Class `json:"classes,omitempty" db:"classes,omitempty"`
+}
+
+
+type ExamVo1 struct {
+	*entity.Exam
+	State *int `json:"state,omitempty" db:"state,omitempty"`
+	Score *int `json:"score,omitempty" db:"score,omitempty"`
+	PassScore *int `json:"pass_score,omitempty" db:"pass_score,omitempty"`
+}

+ 9 - 0
vo/qustion_bank_vo.go

@@ -0,0 +1,9 @@
+package vo
+
+import "exam_system/entity"
+
+type QuestionBankVo struct {
+	*entity.QuestionBank
+	// 题库下试题数量
+	SubNum int `json:"sub_num,omitempty" db:"sub_num,omitempty"`
+}

+ 15 - 0
vo/subject_vo.go

@@ -0,0 +1,15 @@
+package vo
+
+import "exam_system/entity"
+
+type SubjectVo struct {
+	*entity.Subject
+	QbName string `json:"qb_name,omitempty" db:"qb_name,omitempty"`
+	Score *int `json:"score,omitempty"`
+}
+
+
+type SubIdsVo struct {
+	Id    *int `json:"id,omitempty"`
+	Score *int `json:"score,omitempty"`
+}

+ 10 - 0
vo/term_vo.go

@@ -0,0 +1,10 @@
+package vo
+
+import "exam_system/entity"
+
+type TermVo struct {
+	*entity.Term
+	Classes []*ClassVo `json:"classes,omitempty"`
+	// 学期人数
+	UserNum *int `json:"user_num,omitempty" db:"user_num,omitempty"`
+}

+ 34 - 0
vo/ters_paper_vo.go

@@ -0,0 +1,34 @@
+package vo
+
+import (
+	"time"
+)
+
+type TestPaperVo struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	Name     string     `json:"name,omitempty" db:"name,omitempty"`
+	CreateAt time.Time  `json:"create_at,omitempty" db:"create_at,omitempty"`
+	UpdateAt time.Time  `json:"update_at,omitempty" db:"update_at,omitempty"`
+	DeleteAt *time.Time `json:"-" db:"delete_at,omitempty"`
+	Status   *int        `json:"status,omitempty" db:"status,omitempty"`
+	Score    *int         `json:"score,omitempty" db:"score,omitempty"`
+	Answer   *string	`json:"answer,omitempty" db:"answer,omitempty"`
+	Chapters []*ChapterVo `json:"chapters,omitempty" db:"chapters,omitempty"`
+	// 及格
+	PassScore *int		`json:"pass_score,omitempty" db:"pass_score,omitempty"`
+}
+
+
+type ChapterVo struct {
+	Id       int        `json:"id,omitempty" db:"id,omitempty"`
+	Name     string     `json:"name,omitempty" db:"name,omitempty"`
+	Description string	`json:"description,omitempty" db:"description,omitempty"`
+	SubList []*SubjectVo `json:"sub_list,omitempty" db:"-"`
+	SubIds string `json:"-" db:"sub_ids,omitempty"`
+	TpId	int 		`json:"tp_id,omitempty" db:"tp_id,omitempty"`
+	Index *int 			`json:"index,omitempty" db:"index,omitempty"`
+}
+
+
+
+

+ 10 - 0
vo/user_vo.go

@@ -0,0 +1,10 @@
+package vo
+
+import "exam_system/entity"
+
+type UserVo struct {
+	entity.User
+	RoleName  *string `json:"role_name,omitempty" db:"role_name,omitempty"`
+	TermName  *string `json:"term_name,omitempty" db:"term_name,omitempty"`
+	ClassName *string `json:"class_name,omitempty" db:"class_name,omitempty"`
+}